Add `ModelMetadata` to `ViewData`

- demonstrate `ModelMetadata` is available in a view

Also
- simplify `View()` overloads in `Controller`, pending #110 decisions
- make `Model` in `RazorView<T>` readonly
This commit is contained in:
dougbu 2014-03-19 10:25:57 -07:00
parent 857a239990
commit 32d031c6eb
12 changed files with 189 additions and 36 deletions

View File

@ -11,7 +11,26 @@ namespace MvcSample.Web
}
/// <summary>
/// Action that exercises query\form based model binding.
/// Action that shows metadata when model is <c>null</c>.
/// </summary>
public IActionResult Create()
{
return View();
}
/// <summary>
/// Action that shows metadata when model is non-<c>null</c>.
/// </summary>
/// <returns></returns>
public IActionResult Edit()
{
ViewBag.Gift = "the banana";
ViewData.Model = new User { Name = "Name", Address = "Address in a State", Age = 37, };
return View("Create");
}
/// <summary>
/// Action that exercises query\form based model binding.
/// </summary>
public IActionResult SaveUser(User user)
{
@ -55,6 +74,9 @@ namespace MvcSample.Web
return user;
}
/// <summary>
/// Action that exercises default view names.
/// </summary>
public IActionResult MyView()
{
return View(User());

View File

@ -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";
}
}
<div class="row">
<h2 title="@ViewBag.Title" class="@nullValue">@ViewBag.Title</h2>
<h3 title="Thanks" class="@nullValue">Thanks for @ViewBag.Gift</h3>
@if (Model == null)
{
<h4 title ="Null Model" class="@nullValue">Howdy, your model is null.</h4>
}
else
{
<h4 title="@Model.Name" class="@nullValue">Hello @Model.Name! Happy @Model.Age birthday.</h4>
}
@{
var metadata = ViewData.ModelMetadata;
if (metadata != null)
{
var typeName = metadata.ModelType.Name;
var description = metadata.Description;
<p>@typeName has description '@description' and contains</p>
<ul>
@foreach (var property in metadata.Properties)
{
var propertyName = property.PropertyName;
var propertyTypeName = property.ModelType.Name;
var propertyDescription = property.Description;
<li>Property @propertyName has type @propertyTypeName and description '@propertyDescription'</li>
}
</ul>
}
}
</div>

View File

@ -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,
};

View File

@ -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<object>();
ViewData = new ViewData<object>(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>(TModel model)
// TODO #110: May need <TModel> here and in the overload below.
public IActionResult View(object model)
{
return View(view: null, model: model);
}
public IActionResult View<TModel>(string view, TModel model)
public IActionResult View(string view, object model)
{
var viewDataDictionary = new ViewData<TModel>
// 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);
}
}
}

View File

@ -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;

View File

@ -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<TModel> : 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<TModel> ViewData { get; set; }
public ViewData<TModel> ViewData { get; private set; }
public HtmlHelper<TModel> 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<TModel>;
ViewData = viewData ?? new ViewData<TModel>(context.ViewData);
Model = ViewData.Model;
ViewData = context.ViewData as ViewData<TModel>;
if (ViewData == null)
{
if (context.ViewData != null)
{
ViewData = new ViewData<TModel>(context.ViewData);
}
else
{
var metadataProvider = context.ServiceProvider.GetService<IModelMetadataProvider>();
ViewData = new ViewData<TModel>(metadataProvider);
}
// Have new ViewData; make sure it's visible everywhere.
context.ViewData = ViewData;
}
InitHelpers(context);
return base.RenderAsync(context, writer);

View File

@ -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);
}
}

View File

@ -7,12 +7,11 @@ namespace Microsoft.AspNet.Mvc.Rendering
{
public class ViewContext
{
public ViewContext(IServiceProvider serviceProvider, HttpContext httpContext, IDictionary<string, object> viewEngineContext, ViewData viewData)
public ViewContext(IServiceProvider serviceProvider, HttpContext httpContext, IDictionary<string, object> 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<string, object> ViewEngineContext { get; private set; }

View File

@ -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<object, dynamic> _data;
private object _model;
private ModelMetadata _modelMetadata;
private IModelMetadataProvider _metadataProvider;
public ViewData()
public ViewData([NotNull] IModelMetadataProvider metadataProvider)
{
_data = new Dictionary<object, dynamic>();
_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;
}
}
/// <summary>
/// Provider for subclasses that need it to override <see cref="ModelMetadata"/>.
/// </summary>
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());
}
}
}
}

View File

@ -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<TModel> : 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<TModel>;
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);
}
}

View File

@ -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<int>();
var viewDataOfT = new ViewData<int>(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<string>();
var viewDataOfT = new ViewData<string>(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<object>();
var viewDataOfT = new ViewData<object>(new DataAnnotationsModelMetadataProvider());
ViewData viewData = viewDataOfT;
// Act

View File

@ -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-*",