diff --git a/samples/MvcSample.Web/HomeController.cs b/samples/MvcSample.Web/HomeController.cs index a0e88e211e..8b33fe2aee 100644 --- a/samples/MvcSample.Web/HomeController.cs +++ b/samples/MvcSample.Web/HomeController.cs @@ -11,7 +11,26 @@ namespace MvcSample.Web } /// - /// Action that exercises query\form based model binding. + /// Action that shows metadata when model is null. + /// + public IActionResult Create() + { + return View(); + } + + /// + /// Action that shows metadata when model is non-null. + /// + /// + public IActionResult Edit() + { + ViewBag.Gift = "the banana"; + ViewData.Model = new User { Name = "Name", Address = "Address in a State", Age = 37, }; + return View("Create"); + } + + /// + /// Action that exercises query\form based model binding. /// public IActionResult SaveUser(User user) { @@ -55,6 +74,9 @@ namespace MvcSample.Web return user; } + /// + /// Action that exercises default view names. + /// public IActionResult MyView() { return View(User()); diff --git a/samples/MvcSample.Web/Views/Home/Create.cshtml b/samples/MvcSample.Web/Views/Home/Create.cshtml new file mode 100644 index 0000000000..a0753ee117 --- /dev/null +++ b/samples/MvcSample.Web/Views/Home/Create.cshtml @@ -0,0 +1,42 @@ +@using MvcSample.Web.Models +@model User +@{ + string nullValue = null; + ViewBag.Title = (Model == null) ? "Create Page" : "Edit Page"; + if (ViewData["Gift"] == null) + { + ViewBag.Gift = "nothing"; + } +} + +
+

@ViewBag.Title

+

Thanks for @ViewBag.Gift

+ @if (Model == null) + { +

Howdy, your model is null.

+ } + else + { +

Hello @Model.Name! Happy @Model.Age birthday.

+ } + + @{ + var metadata = ViewData.ModelMetadata; + if (metadata != null) + { + var typeName = metadata.ModelType.Name; + var description = metadata.Description; +

@typeName has description '@description' and contains

+ + } + } +
\ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs index b083efb127..4363711e24 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs @@ -33,9 +33,10 @@ namespace Microsoft.AspNet.Mvc context.HttpContext.Response.ContentType = "text/html"; using (var writer = new StreamWriter(context.HttpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true)) { - var viewContext = new ViewContext(_serviceProvider, context.HttpContext, context.RouteValues, ViewData) + var viewContext = new ViewContext(_serviceProvider, context.HttpContext, context.RouteValues) { Url = new UrlHelper(context.HttpContext, context.Router, context.RouteValues), + ViewData = ViewData, Writer = writer, }; diff --git a/src/Microsoft.AspNet.Mvc.Core/Controller.cs b/src/Microsoft.AspNet.Mvc.Core/Controller.cs index 4f0a870f72..7b76daa6ed 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Controller.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Controller.cs @@ -1,14 +1,15 @@ using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Rendering; namespace Microsoft.AspNet.Mvc { public class Controller { - public void Initialize(IActionResultHelper actionResultHelper) + public void Initialize(IActionResultHelper actionResultHelper, IModelMetadataProvider metadataProvider) { Result = actionResultHelper; - ViewData = new ViewData(); + ViewData = new ViewData(metadataProvider); } public IActionResultHelper Result { get; private set; } @@ -31,21 +32,24 @@ namespace Microsoft.AspNet.Mvc public IActionResult View(string view) { - return View(view, model: (object)null); + return View(view, model: null); } - public IActionResult View(TModel model) + // TODO #110: May need here and in the overload below. + public IActionResult View(object model) { return View(view: null, model: model); } - public IActionResult View(string view, TModel model) + public IActionResult View(string view, object model) { - var viewDataDictionary = new ViewData + // Do not override ViewData.Model unless passed a non-null value. + if (model != null) { - Model = model - }; - return Result.View(view, viewDataDictionary); + ViewData.Model = model; + } + + return Result.View(view, ViewData); } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs index 5c2ed38a9a..92833890a2 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNet.Mvc.Razor private string BodyContent { get; set; } - public virtual async Task RenderAsync(ViewContext context, TextWriter writer) + public virtual async Task RenderAsync([NotNull] ViewContext context, [NotNull] TextWriter writer) { Context = context; diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorViewOfT.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorViewOfT.cs index 522a72570a..3e0f85dbad 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorViewOfT.cs @@ -1,27 +1,49 @@ using System.IO; using System.Threading.Tasks; +using Microsoft.AspNet.DependencyInjection; +using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Rendering; namespace Microsoft.AspNet.Mvc.Razor { public abstract class RazorView : RazorView { - public TModel Model { get; set; } + public TModel Model + { + get + { + return ViewData == null ? default(TModel) : ViewData.Model; + } + } public dynamic ViewBag { get { return ViewData; } } - public ViewData ViewData { get; set; } + public ViewData ViewData { get; private set; } public HtmlHelper Html { get; set; } - public override Task RenderAsync(ViewContext context, TextWriter writer) + public override Task RenderAsync([NotNull] ViewContext context, [NotNull] TextWriter writer) { - var viewData = context.ViewData as ViewData; - ViewData = viewData ?? new ViewData(context.ViewData); - Model = ViewData.Model; + ViewData = context.ViewData as ViewData; + if (ViewData == null) + { + if (context.ViewData != null) + { + ViewData = new ViewData(context.ViewData); + } + else + { + var metadataProvider = context.ServiceProvider.GetService(); + ViewData = new ViewData(metadataProvider); + } + + // Have new ViewData; make sure it's visible everywhere. + context.ViewData = ViewData; + } + InitHelpers(context); return base.RenderAsync(context, writer); diff --git a/src/Microsoft.AspNet.Mvc.Rendering/View/IView.cs b/src/Microsoft.AspNet.Mvc.Rendering/View/IView.cs index f1ea7a916c..863079c231 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/View/IView.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/View/IView.cs @@ -5,6 +5,6 @@ namespace Microsoft.AspNet.Mvc.Rendering { public interface IView { - Task RenderAsync(ViewContext context, TextWriter writer); + Task RenderAsync([NotNull] ViewContext context, [NotNull] TextWriter writer); } } diff --git a/src/Microsoft.AspNet.Mvc.Rendering/View/ViewContext.cs b/src/Microsoft.AspNet.Mvc.Rendering/View/ViewContext.cs index 5f3ef730e3..f0dea94c1d 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/View/ViewContext.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/View/ViewContext.cs @@ -7,12 +7,11 @@ namespace Microsoft.AspNet.Mvc.Rendering { public class ViewContext { - public ViewContext(IServiceProvider serviceProvider, HttpContext httpContext, IDictionary viewEngineContext, ViewData viewData) + public ViewContext(IServiceProvider serviceProvider, HttpContext httpContext, IDictionary viewEngineContext) { ServiceProvider = serviceProvider; HttpContext = httpContext; ViewEngineContext = viewEngineContext; - ViewData = viewData; } public HttpContext HttpContext { get; private set; } @@ -21,7 +20,7 @@ namespace Microsoft.AspNet.Mvc.Rendering public IUrlHelper Url { get; set; } - public ViewData ViewData { get; private set; } + public ViewData ViewData { get; set; } public IDictionary ViewEngineContext { get; private set; } diff --git a/src/Microsoft.AspNet.Mvc.Rendering/View/ViewData.cs b/src/Microsoft.AspNet.Mvc.Rendering/View/ViewData.cs index e2919c7db8..1d08f9f6a8 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/View/ViewData.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/View/ViewData.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Dynamic; +using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc.Rendering { @@ -8,15 +9,20 @@ namespace Microsoft.AspNet.Mvc.Rendering { private readonly Dictionary _data; private object _model; + private ModelMetadata _modelMetadata; + private IModelMetadataProvider _metadataProvider; - public ViewData() + public ViewData([NotNull] IModelMetadataProvider metadataProvider) { _data = new Dictionary(); + _metadataProvider = metadataProvider; } public ViewData([NotNull] ViewData source) { _data = source._data; + _modelMetadata = source.ModelMetadata; + _metadataProvider = source.MetadataProvider; SetModel(source.Model); } @@ -44,13 +50,34 @@ namespace Microsoft.AspNet.Mvc.Rendering } set { - _data[index] = (dynamic)value; + _data[index] = value; } } + public virtual ModelMetadata ModelMetadata + { + get + { + return _modelMetadata; + } + set + { + _modelMetadata = value; + } + } + + /// + /// Provider for subclasses that need it to override . + /// + protected IModelMetadataProvider MetadataProvider + { + get { return _metadataProvider; } + } + public override bool TryGetMember(GetMemberBinder binder, out object result) { result = _data[binder.Name]; + // We return true here because ViewDataDictionary returns null if the key is not // in the dictionary, so we simply pass on the returned value. return true; @@ -58,7 +85,7 @@ namespace Microsoft.AspNet.Mvc.Rendering public override bool TrySetMember(SetMemberBinder binder, object value) { - // This cast should always succeed assuming TValue is dynamic. + // This cast should always succeed. dynamic v = value; _data[binder.Name] = v; return true; @@ -84,7 +111,8 @@ namespace Microsoft.AspNet.Mvc.Rendering } object index = indexes[0]; - // This cast should always succeed assuming TValue is dynamic. + + // This cast should always succeed. this[(string)index] = value; return true; } @@ -95,6 +123,16 @@ namespace Microsoft.AspNet.Mvc.Rendering protected virtual void SetModel(object value) { _model = value; + if (value == null) + { + // Unable to determine model metadata. + _modelMetadata = null; + } + else if (_modelMetadata == null || value.GetType() != ModelMetadata.ModelType) + { + // Reset or override model metadata based on new value type. + _modelMetadata = _metadataProvider.GetMetadataForType(() => value, value.GetType()); + } } } } diff --git a/src/Microsoft.AspNet.Mvc.Rendering/View/ViewDataOfTModel.cs b/src/Microsoft.AspNet.Mvc.Rendering/View/ViewDataOfTModel.cs index 133a04a482..b84e356674 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/View/ViewDataOfTModel.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/View/ViewDataOfTModel.cs @@ -1,18 +1,32 @@ -using System; -using Microsoft.AspNet.Mvc.ModelBinding.Internal; +using System; +using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc.Rendering { public class ViewData : ViewData { - public ViewData() - : base() + // Fallback ModelMetadata based on TModel. Used when Model is null and base ViewData class is unable to + // determine the correct metadata. + private readonly ModelMetadata _defaultModelMetadata; + + public ViewData([NotNull] IModelMetadataProvider metadataProvider) + : base(metadataProvider) { + _defaultModelMetadata = MetadataProvider.GetMetadataForType(null, typeof(TModel)); } - public ViewData(ViewData source) : - base(source) + public ViewData(ViewData source) + : base(source) { + var original = source as ViewData; + if (original != null) + { + _defaultModelMetadata = original._defaultModelMetadata; + } + else + { + _defaultModelMetadata = MetadataProvider.GetMetadataForType(null, typeof(TModel)); + } } public new TModel Model @@ -21,6 +35,14 @@ namespace Microsoft.AspNet.Mvc.Rendering set { SetModel(value); } } + public override ModelMetadata ModelMetadata + { + get + { + return base.ModelMetadata ?? _defaultModelMetadata; + } + } + protected override void SetModel(object value) { // IsCompatibleObject verifies if the value is either an instance of TModel or (if value is null) that @@ -42,6 +64,7 @@ namespace Microsoft.AspNet.Mvc.Rendering { message = Resources.FormatViewData_WrongTModelType(value.GetType(), typeof(TModel)); } + throw new InvalidOperationException(message); } } diff --git a/test/Microsoft.AspNet.Mvc.Rendering.Test/ViewDataOfTTest.cs b/test/Microsoft.AspNet.Mvc.Rendering.Test/ViewDataOfTTest.cs index a7c4221cbb..aaba444b23 100644 --- a/test/Microsoft.AspNet.Mvc.Rendering.Test/ViewDataOfTTest.cs +++ b/test/Microsoft.AspNet.Mvc.Rendering.Test/ViewDataOfTTest.cs @@ -1,7 +1,8 @@ using System; +using Microsoft.AspNet.Mvc.ModelBinding; using Xunit; -namespace Microsoft.AspNet.Mvc.Rendering.Test +namespace Microsoft.AspNet.Mvc.Rendering { public class ViewDataOfTTest { @@ -9,7 +10,7 @@ namespace Microsoft.AspNet.Mvc.Rendering.Test public void SettingModelThrowsIfTheModelIsNull() { // Arrange - var viewDataOfT = new ViewData(); + var viewDataOfT = new ViewData(new DataAnnotationsModelMetadataProvider()); ViewData viewData = viewDataOfT; // Act and Assert @@ -21,7 +22,7 @@ namespace Microsoft.AspNet.Mvc.Rendering.Test public void SettingModelThrowsIfTheModelIsIncompatible() { // Arrange - var viewDataOfT = new ViewData(); + var viewDataOfT = new ViewData(new DataAnnotationsModelMetadataProvider()); ViewData viewData = viewDataOfT; // Act and Assert @@ -34,7 +35,7 @@ namespace Microsoft.AspNet.Mvc.Rendering.Test { // Arrange string value = "some value"; - var viewDataOfT = new ViewData(); + var viewDataOfT = new ViewData(new DataAnnotationsModelMetadataProvider()); ViewData viewData = viewDataOfT; // Act diff --git a/test/Microsoft.AspNet.Mvc.Rendering.Test/project.json b/test/Microsoft.AspNet.Mvc.Rendering.Test/project.json index 855d633485..5ba50da201 100644 --- a/test/Microsoft.AspNet.Mvc.Rendering.Test/project.json +++ b/test/Microsoft.AspNet.Mvc.Rendering.Test/project.json @@ -3,6 +3,7 @@ "dependencies": { "Microsoft.AspNet.Abstractions": "0.1-alpha-*", "Microsoft.AspNet.PipelineCore": "0.1-alpha-*", + "Microsoft.AspNet.Mvc.ModelBinding" : "", "Microsoft.AspNet.Mvc.Rendering" : "", "TestCommon" : "", "Xunit.KRunner": "0.1-alpha-*",