Preserve existing metadata in `ViewDataDictionary` where possible

- #4116
- generalize rules for `ModelMetadata` creation; minimize metadata changes when Model is updated
 - down to a single special case in VDD for `Nullable<T>`
 - note existing functional tests did not need to change
- remove `ViewDataDictionary(ViewDataDictionary, object)` constructor; use `new VDD<object>(source, model)`
- allow all `Model` assignments in a view component
 - copy-constructed VDD in `ViewComponentContext` previously preserved the source's declared type

nits:
- do not call `virtual SetModel()` method from constructor; now mostly redundant
 - logic in copy constructor and `SetModel()` is consistent but different enough to keep code separate
- add some missing doc comments
- fix doc comment property versus type confusion; never need to specify `ViewDataDictionary.` prefix
- fix a few `TemplateBuilder` comments and remove unnecessary `model: null` argument to VDD constructor
This commit is contained in:
Doug Bunting 2016-04-08 12:12:46 -07:00
parent a55544681d
commit a0c8834c70
34 changed files with 1212 additions and 250 deletions

View File

@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
ViewContext = new ViewContext(
viewContext,
viewContext.View,
new ViewDataDictionary(viewContext.ViewData),
new ViewDataDictionary<object>(viewContext.ViewData),
writer);
}

View File

@ -589,7 +589,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
// Determine which ViewData we should use to construct a new ViewData
var baseViewData = viewData ?? ViewData;
var newViewData = new ViewDataDictionary(baseViewData, model);
var newViewData = new ViewDataDictionary<object>(baseViewData, model);
var viewContext = new ViewContext(ViewContext, view, newViewData, writer);
await viewEngineResult.View.RenderAsync(viewContext);

View File

@ -101,14 +101,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
return HtmlString.Empty;
}
// We need to copy the ModelExplorer to copy the model metadata. Otherwise we might
// lose track of the model type/property. Passing null here explicitly, because
// this might be a typed VDD, and the model value might not be compatible.
// Create VDD of type object so it retains the correct metadata when the model type is not known.
var viewData = new ViewDataDictionary<object>(_viewData, model: null);
// Create VDD of type object so any model type is allowed.
var viewData = new ViewDataDictionary<object>(_viewData);
// We're setting ModelExplorer in order to preserve the model metadata of the original
// _viewData even though _model may be null.
// Create a new ModelExplorer in order to preserve the model metadata of the original _viewData even
// though _model may have been reset to null. Otherwise we might lose track of the model type /property.
viewData.ModelExplorer = _modelExplorer.GetExplorerForModel(_model);
viewData.TemplateInfo.FormattedModelValue = formattedModelValue;

View File

@ -9,6 +9,7 @@ using System.Globalization;
using System.Reflection;
#endif
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
@ -23,8 +24,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// Initializes a new instance of the <see cref="ViewDataDictionary"/> class.
/// </summary>
/// <param name="metadataProvider">
/// <see cref = "IModelMetadataProvider" /> instance used to calculate
/// <see cref="ViewDataDictionary.ModelMetadata"/> values.
/// <see cref="IModelMetadataProvider"/> instance used to create <see cref="ViewFeatures.ModelExplorer"/>
/// instances.
/// </param>
/// <param name="modelState"><see cref="ModelStateDictionary"/> instance for this scope.</param>
/// <remarks>For use when creating a <see cref="ViewDataDictionary"/> for a new top-level scope.</remarks>
@ -41,38 +42,27 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// </summary>
/// <param name="source"><see cref="ViewDataDictionary"/> instance to copy initial values from.</param>
/// <remarks>
/// <para>
/// For use when copying a <see cref="ViewDataDictionary"/> instance and the declared <see cref="Model"/>
/// <see cref="Type"/> will not change e.g. when copying from a <see cref="ViewDataDictionary{TModel}"/>
/// instance to a base <see cref="ViewDataDictionary"/> instance.
/// </para>
/// <para>
/// This constructor should not be used in any context where <see cref="Model"/> may be set to a value
/// incompatible with the declared type of <paramref name="source"/>.
/// </para>
/// </remarks>
public ViewDataDictionary(ViewDataDictionary source)
: this(source, source.Model, source._declaredModelType)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ViewDataDictionary"/> class based in part on an existing
/// instance. This constructor is careful to avoid exceptions <see cref="SetModel"/> may throw when
/// <paramref name="model"/> is <c>null</c>.
/// </summary>
/// <param name="source"><see cref="ViewDataDictionary"/> instance to copy initial values from.</param>
/// <param name="model">Value for the <see cref="Model"/> property.</param>
/// <remarks>
/// For use when the new instance's declared <see cref="Model"/> <see cref="Type"/> is unknown but its
/// <see cref="Model"/> is known. In this case, <see cref="object"/> is the best possible guess about the
/// declared type when <paramref name="model"/> is <c>null</c>.
/// </remarks>
public ViewDataDictionary(ViewDataDictionary source, object model)
: this(source, model, declaredModelType: typeof(object))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ViewDataDictionary"/> class.
/// </summary>
/// <param name="metadataProvider">
/// <see cref="IModelMetadataProvider"/> instance used to calculate
/// <see cref="ViewDataDictionary.ModelMetadata"/> values.
/// <see cref="IModelMetadataProvider"/> instance used to create <see cref="ViewFeatures.ModelExplorer"/>
/// instances.
/// </param>
/// <remarks>Internal for testing.</remarks>
internal ViewDataDictionary(IModelMetadataProvider metadataProvider)
@ -84,12 +74,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// Initializes a new instance of the <see cref="ViewDataDictionary"/> class.
/// </summary>
/// <param name="metadataProvider">
/// <see cref = "IModelMetadataProvider" /> instance used to calculate
/// <see cref="ViewDataDictionary.ModelMetadata"/> values.
/// <see cref="IModelMetadataProvider"/> instance used to create <see cref="ViewFeatures.ModelExplorer"/>
/// instances.
/// </param>
/// <param name="declaredModelType">
/// <see cref="Type"/> of <see cref="Model"/> values expected. Used to set
/// <see cref="ViewDataDictionary.ModelMetadata"/> when <see cref="Model"/> is <c>null</c>.
/// <see cref="Type"/> of <see cref="Model"/> values expected. Used to set <see cref="ModelMetadata"/>.
/// </param>
/// <remarks>
/// For use when creating a derived <see cref="ViewDataDictionary"/> for a new top-level scope.
@ -105,17 +94,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// Initializes a new instance of the <see cref="ViewDataDictionary"/> class.
/// </summary>
/// <param name="metadataProvider">
/// <see cref = "IModelMetadataProvider" /> instance used to calculate
/// <see cref="ViewDataDictionary.ModelMetadata"/> values.
/// <see cref="IModelMetadataProvider"/> instance used to create <see cref="ViewFeatures.ModelExplorer"/>
/// instances.
/// </param>
/// <param name="modelState"><see cref="ModelStateDictionary"/> instance for this scope.</param>
/// <param name="declaredModelType">
/// <see cref="Type"/> of <see cref="Model"/> values expected. Used to set
/// <see cref="ViewDataDictionary.ModelMetadata"/> when <see cref="Model"/> is <c>null</c>.
/// <see cref="Type"/> of <see cref="Model"/> values expected. Used to set <see cref="ModelMetadata"/>.
/// </param>
/// <remarks>
/// For use when creating a derived <see cref="ViewDataDictionary"/> for a new top-level scope.
/// </remarks>
// This is the core constructor called when Model is unknown.
protected ViewDataDictionary(
IModelMetadataProvider metadataProvider,
ModelStateDictionary modelState,
@ -141,7 +130,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(declaredModelType));
}
// This is the core constructor called when Model is unknown. Base ModelMetadata on the declared type.
// Base ModelMetadata on the declared type.
ModelExplorer = _metadataProvider.GetModelExplorerForType(declaredModelType, model: null);
}
@ -151,8 +140,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// </summary>
/// <param name="source"><see cref="ViewDataDictionary"/> instance to copy initial values from.</param>
/// <param name="declaredModelType">
/// <see cref="Type"/> of <see cref="Model"/> values expected. Used to set
/// <see cref="ViewDataDictionary.ModelMetadata"/> when <see cref="Model"/> is <c>null</c>.
/// <see cref="Type"/> of <see cref="Model"/> values expected. Used to set <see cref="ModelMetadata"/>.
/// </param>
/// <remarks>
/// <para>
@ -180,8 +168,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// <param name="source"><see cref="ViewDataDictionary"/> instance to copy initial values from.</param>
/// <param name="model">Value for the <see cref="Model"/> property.</param>
/// <param name="declaredModelType">
/// <see cref="Type"/> of <see cref="Model"/> values expected. Used to set
/// <see cref="ViewDataDictionary.ModelMetadata"/> when <see cref="Model"/> is <c>null</c>.
/// <see cref="Type"/> of <see cref="Model"/> values expected. Used to set <see cref="ModelMetadata"/>.
/// </param>
/// <remarks>
/// <para>
@ -193,6 +180,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// <paramref name="declaredModelType"/>.
/// </para>
/// </remarks>
// This is the core constructor called when Model is known.
protected ViewDataDictionary(ViewDataDictionary source, object model, Type declaredModelType)
: this(source._metadataProvider,
source.ModelState,
@ -205,26 +193,59 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(source));
}
// This is the core constructor called when Model is known.
var modelType = GetModelType(model);
var metadataModelType = source.ModelMetadata.UnderlyingOrModelType;
if (modelType == metadataModelType && model == source.ModelExplorer.Model)
// A non-null Model must always be assignable to both _declaredModelType and ModelMetadata.ModelType.
//
// ModelMetadata.ModelType should also be assignable to _declaredModelType. Though corner cases exist such
// as a ViewDataDictionary<List<int>> holding information about an IEnumerable<int> property (because an
// @model directive matched the runtime type though the view's name did not), we'll throw away the property
// metadata in those cases -- preserving invariant that ModelType can be assigned to _declaredModelType.
//
// More generally, since defensive copies to base VDD and VDD<object> abound, it's important to preserve
// metadata despite _declaredModelType changes.
var modelType = model?.GetType();
var modelOrDeclaredType = modelType ?? declaredModelType;
if (source.ModelMetadata.MetadataKind == ModelMetadataKind.Type &&
source.ModelMetadata.ModelType == typeof(object) &&
modelOrDeclaredType != typeof(object))
{
// Preserve any customizations made to source.ModelExplorer.ModelMetadata if the Type
// that will be calculated in SetModel() and source.Model match new instance's values.
// Base ModelMetadata on new type when there's no property information to preserve and type changes to
// something besides typeof(object).
ModelExplorer = _metadataProvider.GetModelExplorerForType(modelOrDeclaredType, model);
}
else if (!declaredModelType.IsAssignableFrom(source.ModelMetadata.ModelType))
{
// Base ModelMetadata on new type when existing metadata is incompatible with the new declared type.
ModelExplorer = _metadataProvider.GetModelExplorerForType(modelOrDeclaredType, model);
}
else if (modelType != null && !source.ModelMetadata.ModelType.IsAssignableFrom(modelType))
{
// Base ModelMetadata on new type when new model is incompatible with the existing metadata.
ModelExplorer = _metadataProvider.GetModelExplorerForType(modelType, model);
}
else if (object.ReferenceEquals(model, source.ModelExplorer.Model))
{
// Source's ModelExplorer is already exactly correct.
ModelExplorer = source.ModelExplorer;
}
else if (model == null)
else
{
// Ensure ModelMetadata is never null though SetModel() isn't called below.
ModelExplorer = _metadataProvider.GetModelExplorerForType(_declaredModelType, model: null);
// The existing metadata is compatible with the value and declared type but it's a new value.
ModelExplorer = new ModelExplorer(
_metadataProvider,
source.ModelExplorer.Container,
source.ModelMetadata,
model);
}
// If we're constructing a ViewDataDictionary<TModel> where TModel is a non-Nullable value type,
// SetModel() will throw if we try to call it with null. We should not throw in that case.
// Ensure the given Model is compatible with _declaredModelType. Do not do this one of the following
// special cases:
// - Constructing a ViewDataDictionary<TModel> where TModel is a non-Nullable value type. This may for
// example occur when activating a RazorPage<int> and the container is null.
// - Constructing a ViewDataDictionary<object> immediately before overwriting ModelExplorer with correct
// information. See TemplateBuilder.Build().
if (model != null)
{
SetModel(model);
EnsureCompatible(model);
}
}
@ -242,6 +263,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
TemplateInfo = templateInfo;
}
/// <summary>
/// Gets or sets the current model.
/// </summary>
public object Model
{
get
@ -250,20 +274,23 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
set
{
// Reset ModelExplorer to ensure Model and ModelMetadata.Model remain equal.
// Reset ModelExplorer to ensure Model and ModelExplorer.Model remain equal.
SetModel(value);
}
}
/// <summary>
/// Gets the <see cref="ModelStateDictionary"/>.
/// </summary>
public ModelStateDictionary ModelState { get; }
/// <summary>
/// <see cref="ModelMetadata"/> for the current <see cref="Model"/> value or the declared <see cref="Type"/> if
/// <see cref="Model"/> is <c>null</c>.
/// Gets the <see cref="ModelBinding.ModelMetadata"/> for an expression, the <see cref="Model"/> (if
/// non-<c>null</c>), or the declared <see cref="Type"/>.
/// </summary>
/// <remarks>
/// Value is never <c>null</c> but may describe the <see cref="object"/> class in some cases. This may for
/// example occur in controllers if <see cref="Model"/> is <c>null</c>.
/// example occur in controllers.
/// </remarks>
public ModelMetadata ModelMetadata
{
@ -274,13 +301,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
/// <summary>
/// Gets or sets the <see cref="ModelExplorer"/> for the <see cref="Model"/>.
/// Gets or sets the <see cref="ViewFeatures.ModelExplorer"/> for the <see cref="Model"/>.
/// </summary>
public ModelExplorer ModelExplorer { get; set; }
/// <summary>
/// Gets the <see cref="ViewFeatures.TemplateInfo"/>.
/// </summary>
public TemplateInfo TemplateInfo { get; }
#region IDictionary properties
/// <inheritdoc />
// Do not just pass through to _data: Indexer should not throw a KeyNotFoundException.
public object this[string index]
{
@ -296,21 +327,25 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
}
/// <inheritdoc />
public int Count
{
get { return _data.Count; }
}
/// <inheritdoc />
public bool IsReadOnly
{
get { return _data.IsReadOnly; }
}
/// <inheritdoc />
public ICollection<string> Keys
{
get { return _data.Keys; }
}
/// <inheritdoc />
public ICollection<object> Values
{
get { return _data.Values; }
@ -360,6 +395,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return FormatValue(value, format);
}
/// <summary>
/// Formats the given <paramref name="value"/> using given <paramref name="format"/>.
/// </summary>
/// <param name="value">The value to format.</param>
/// <param name="format">
/// The composite format <see cref="string"/> (see http://msdn.microsoft.com/en-us/library/txafckwd.aspx).
/// </param>
/// <returns>The formatted <see cref="string"/>.</returns>
public static string FormatValue(object value, string format)
{
if (value == null)
@ -395,37 +438,54 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return ViewDataEvaluator.Eval(this, expression);
}
// This method will execute before the derived type's instance constructor executes. Derived types must
// be aware of this and should plan accordingly. For example, the logic in SetModel() should be simple
// enough so as not to depend on the "this" pointer referencing a fully constructed object.
/// <summary>
/// Set <see cref="ModelExplorer"/> to ensure <see cref="Model"/> and <see cref="ModelExplorer.Model"/>
/// reflect the new <paramref name="value"/>.
/// </summary>
/// <param name="value">New <see cref="Model"/> value.</param>
protected virtual void SetModel(object value)
{
EnsureCompatible(value);
// Reset or override ModelMetadata based on runtime value type. Fall back to declared type if value is
// null. When called from a constructor, current ModelExplorer may already be set to preserve
// customizations made in parent scope. But ModelExplorer is never null after instance is initialized.
var modelType = GetModelType(value);
Type metadataModelType = null;
if (ModelExplorer != null)
// Update ModelExplorer to reflect the new value. When possible, preserve ModelMetadata to avoid losing
// property information.
var modelType = value?.GetType();
if (ModelMetadata.MetadataKind == ModelMetadataKind.Type &&
ModelMetadata.ModelType == typeof(object) &&
modelType != null &&
modelType != typeof(object))
{
metadataModelType = ModelMetadata.UnderlyingOrModelType;
// Base ModelMetadata on new type when there's no property information to preserve and type changes to
// something besides typeof(object).
ModelExplorer = _metadataProvider.GetModelExplorerForType(modelType, value);
}
if (metadataModelType != modelType)
else if (modelType != null && !ModelMetadata.ModelType.IsAssignableFrom(modelType))
{
// Base ModelMetadata on new type when new model is incompatible with the existing metadata. The most
// common case is _declaredModelType==typeof(object), metadata was copied from another VDD, and user
// code sets the Model to a new type e.g. within a view component or a view that lacks an @model
// directive.
ModelExplorer = _metadataProvider.GetModelExplorerForType(modelType, value);
}
else if (object.ReferenceEquals(value, Model))
{
// The metadata already matches, and the model is literally the same, nothing
// to do here. This will likely occur when using one of the copy constructors.
// The metadata matches and the model is literally the same; usually nothing to do here.
if (value == null &&
!ModelMetadata.IsReferenceOrNullableType &&
_declaredModelType != ModelMetadata.ModelType)
{
// Base ModelMetadata on declared type when setting Model to null, source VDD's Model was never
// set, and source VDD had a non-Nullable value type. Though _declaredModelType might also be a
// non-Nullable value type, would need to duplicate logic behind
// ModelMetadata.IsReferenceOrNullableType to avoid this allocation in the error case.
ModelExplorer = _metadataProvider.GetModelExplorerForType(_declaredModelType, value);
}
}
else
{
// The metadata matches, but it's a new value.
// The existing metadata is compatible with the value but it's a new value.
ModelExplorer = new ModelExplorer(_metadataProvider, ModelExplorer.Container, ModelMetadata, value);
}
EnsureCompatible(value);
}
// Throw if given value is incompatible with the declared Model Type.
@ -450,11 +510,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
}
private Type GetModelType(object value)
{
return (value == null) ? _declaredModelType : value.GetType();
}
// Call after updating the ModelExplorer because this uses both _declaredModelType and ModelMetadata. May
// otherwise get incorrect compatibility errors.
private bool IsCompatibleWithDeclaredType(object value)
{
if (value == null)
@ -469,6 +526,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
#region IDictionary methods
/// <inheritdoc />
public void Add(string key, object value)
{
if (key == null)
@ -479,6 +537,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
_data.Add(key, value);
}
/// <inheritdoc />
public bool ContainsKey(string key)
{
if (key == null)
@ -489,6 +548,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return _data.ContainsKey(key);
}
/// <inheritdoc />
public bool Remove(string key)
{
if (key == null)
@ -499,6 +559,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return _data.Remove(key);
}
/// <inheritdoc />
public bool TryGetValue(string key, out object value)
{
if (key == null)
@ -509,21 +570,25 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return _data.TryGetValue(key, out value);
}
/// <inheritdoc />
public void Add(KeyValuePair<string, object> item)
{
_data.Add(item);
}
/// <inheritdoc />
public void Clear()
{
_data.Clear();
}
/// <inheritdoc />
public bool Contains(KeyValuePair<string, object> item)
{
return _data.Contains(item);
}
/// <inheritdoc />
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
{
if (array == null)
@ -534,16 +599,19 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
_data.CopyTo(array, arrayIndex);
}
/// <inheritdoc />
public bool Remove(KeyValuePair<string, object> item)
{
return _data.Remove(item);
}
/// <inheritdoc />
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
{
return _data.GetEnumerator();
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return _data.GetEnumerator();

View File

@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
}
/// <inheritdoc />
public new TModel Model
{
get

View File

@ -189,6 +189,35 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
}
}
// Testing how ModelMetadata is handled as ViewDataDictionary instances are created.
[Theory]
[InlineData("AtViewModel")]
[InlineData("NullViewModel")]
[InlineData("ViewModel")]
public async Task CheckViewData_GeneratesExpectedResults(string action)
{
// Arrange
var expectedMediaType = MediaTypeHeaderValue.Parse("text/html; charset=utf-8");
var outputFile = "compiler/resources/HtmlGenerationWebSite.CheckViewData." + action + ".html";
var expectedContent =
await ResourceFile.ReadResourceAsync(_resourcesAssembly, outputFile, sourceFile: false);
// Act
var response = await Client.GetAsync("http://localhost/CheckViewData/" + action);
var responseContent = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedMediaType, response.Content.Headers.ContentType);
responseContent = responseContent.Trim();
#if GENERATE_BASELINES
ResourceFile.UpdateFile(_resourcesAssembly, outputFile, expectedContent, responseContent);
#else
Assert.Equal(expectedContent, responseContent, ignoreLineEndingDifferences: true);
#endif
}
[Fact]
public async Task ValidationTagHelpers_GeneratesExpectedSpansAndDivs()
{

View File

@ -0,0 +1,54 @@
<div class="row">
<h4>At Model index</h4>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'SuperViewModel'</div>
</div>
<div class="row">
<h5>Template for ViewModel</h5>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'SuperViewModel'</div>
</div>
<div class="row">
<h5>Partial for ViewModel</h5>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'SuperViewModel'</div>
</div>
<div class="row">
<h5>Check View Data view component</h5>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'SuperViewModel'</div>
<h5>Check View Data view component's view</h5>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'SuperViewModel'</div>
</div>
<div class="row">
<h5>Template for Int32</h5>
<div class="col-md-3">MetadataKind: 'Property'</div>
<div class="col-md-3">ModelType: 'Int32'</div>
<div class="col-md-3">PropertyName: 'Integer'</div>
</div>
<div class="row">
<h5>Template for Int64</h5>
<div class="col-md-3">MetadataKind: 'Property'</div>
<div class="col-md-3">ModelType: 'Nullable`1'</div>
<div class="col-md-3">PropertyName: 'NullableLong'</div>
</div>
<div class="row">
<h5>Template for TemplateModel</h5>
<div class="col-md-3">MetadataKind: 'Property'</div>
<div class="col-md-3">ModelType: 'TemplateModel'</div>
<div class="col-md-3">PropertyName: 'Template'</div>
</div>

View File

@ -0,0 +1,54 @@
<div class="row">
<h4>At Model index</h4>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'ViewModel'</div>
</div>
<div class="row">
<h5>Template for ViewModel</h5>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'ViewModel'</div>
</div>
<div class="row">
<h5>Partial for ViewModel</h5>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'ViewModel'</div>
</div>
<div class="row">
<h5>Check View Data view component</h5>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'ViewModel'</div>
<h5>Check View Data view component's view</h5>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'ViewModel'</div>
</div>
<div class="row">
<h5>Template for Int32</h5>
<div class="col-md-3">MetadataKind: 'Property'</div>
<div class="col-md-3">ModelType: 'Int32'</div>
<div class="col-md-3">PropertyName: 'Integer'</div>
</div>
<div class="row">
<h5>Template for Int64</h5>
<div class="col-md-3">MetadataKind: 'Property'</div>
<div class="col-md-3">ModelType: 'Nullable`1'</div>
<div class="col-md-3">PropertyName: 'NullableLong'</div>
</div>
<div class="row">
<h5>Template for TemplateModel</h5>
<div class="col-md-3">MetadataKind: 'Property'</div>
<div class="col-md-3">ModelType: 'TemplateModel'</div>
<div class="col-md-3">PropertyName: 'Template'</div>
</div>

View File

@ -0,0 +1,93 @@
<div class="row">
<h4>View Model index</h4>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'SuperViewModel'</div>
</div>
<div class="row">
<h5>Template / partial for ... - LackModel</h5>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'SuperViewModel'</div>
</div>
<div class="row">
<h5>Template / partial for ... - LackModel</h5>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'SuperViewModel'</div>
</div>
<div class="row">
<h5>Check View Data - LackModel view component</h5>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'SuperViewModel'</div>
<h5>Check View Data - LackModel view component after setting Model to 78.9</h5>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'Double'</div>
<h5>Check View Data - LackModel view component's view</h5>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'SuperTemplateModel'</div>
</div>
<div class="row">
<h5>Template for Int32 - LackModel</h5>
<div class="col-md-3">MetadataKind: 'Property'</div>
<div class="col-md-3">ModelType: 'Int32'</div>
<div class="col-md-3">PropertyName: 'Integer'</div>
<h5>Template for Int32 - LackModel after setting Model to 78.9</h5>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'Double'</div>
</div>
<div class="row">
<h5>Template for Int64</h5>
<div class="col-md-3">MetadataKind: 'Property'</div>
<div class="col-md-3">ModelType: 'Nullable`1'</div>
<div class="col-md-3">PropertyName: 'NullableLong'</div>
<div class="row">
<h5>Template / partial for ... - LackModel</h5>
<div class="col-md-3">MetadataKind: 'Property'</div>
<div class="col-md-3">ModelType: 'Nullable`1'</div>
<div class="col-md-3">PropertyName: 'NullableLong'</div>
</div>
<div class="row">
<h5>Check View Data - LackModel view component</h5>
<div class="col-md-3">MetadataKind: 'Property'</div>
<div class="col-md-3">ModelType: 'Nullable`1'</div>
<div class="col-md-3">PropertyName: 'NullableLong'</div>
<h5>Check View Data - LackModel view component after setting Model to 78.9</h5>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'Double'</div>
<h5>Check View Data - LackModel view component's view</h5>
<div class="col-md-3">MetadataKind: 'Type'</div>
<div class="col-md-3">ModelType: 'SuperTemplateModel'</div>
</div>
</div>
<div class="row">
<h5>Template / partial for ... - LackModel</h5>
<div class="col-md-3">MetadataKind: 'Property'</div>
<div class="col-md-3">ModelType: 'TemplateModel'</div>
<div class="col-md-3">PropertyName: 'Template'</div>
</div>

View File

@ -7,12 +7,14 @@ using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
using Moq;
using Newtonsoft.Json;
using Xunit;
@ -274,6 +276,48 @@ namespace Microsoft.AspNetCore.Mvc.Test
Assert.Equal(input, result);
}
public static TheoryData<object, Type> IncompatibleModelData
{
get
{
// Small grab bag of instances and expected types with no common base except typeof(object).
return new TheoryData<object, Type>
{
{ null, typeof(object) },
{ true, typeof(bool) },
{ 43.78, typeof(double) },
{ "test string", typeof(string) },
{ new List<int>(), typeof(List<int>) },
{ new List<string>(), typeof(List<string>) },
};
}
}
[Theory]
[MemberData(nameof(IncompatibleModelData))]
public void ViewDataModelSetter_DoesNotThrow(object model, Type expectedType)
{
// Arrange
var activator = new ViewDataDictionaryControllerPropertyActivator(new EmptyModelMetadataProvider());
var actionContext = new ActionContext(
new DefaultHttpContext(),
new RouteData(),
new ControllerActionDescriptor());
var controllerContext = new ControllerContext(actionContext);
var controller = new TestableController();
activator.Activate(controllerContext, controller);
// Guard
Assert.NotNull(controller.ViewData);
// Act (does not throw)
controller.ViewData.Model = model;
// Assert
Assert.NotNull(controller.ViewData.ModelMetadata);
Assert.Equal(expectedType, controller.ViewData.ModelMetadata.ModelType);
}
private static Controller GetController(IModelBinder binder, IValueProvider valueProvider)
{
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();

View File

@ -304,12 +304,6 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
return htmlHelper;
}
public static string FormatOutput(IHtmlHelper helper, object model)
{
var modelExplorer = helper.MetadataProvider.GetModelExplorerForType(model.GetType(), model);
return FormatOutput(modelExplorer);
}
private static ICompositeViewEngine CreateViewEngine()
{
var view = new Mock<IView>();
@ -335,6 +329,12 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
return viewEngine.Object;
}
public static string FormatOutput(IHtmlHelper helper, object model)
{
var modelExplorer = helper.MetadataProvider.GetModelExplorerForType(model.GetType(), model);
return FormatOutput(modelExplorer);
}
private static string FormatOutput(ModelExplorer modelExplorer)
{
var metadata = modelExplorer.Metadata;

View File

@ -0,0 +1,112 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.WebEncoders.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ViewComponents
{
public class ViewComponentContextTest
{
[Fact]
public void Constructor_PerformsDefensiveCopies()
{
// Arrange
var httpContext = new DefaultHttpContext();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider());
var viewContext = new ViewContext(
actionContext,
NullView.Instance,
viewData,
new TempDataDictionary(httpContext, new SessionStateTempDataProvider()),
TextWriter.Null,
new HtmlHelperOptions());
var viewComponentDescriptor = new ViewComponentDescriptor();
// Act
var viewComponentContext = new ViewComponentContext(
viewComponentDescriptor,
new Dictionary<string, object>(),
new HtmlTestEncoder(),
viewContext,
TextWriter.Null);
// Assert
// New ViewContext but initial View and TextWriter copied over.
Assert.NotSame(viewContext, viewComponentContext.ViewContext);
Assert.Same(viewContext.View, viewComponentContext.ViewContext.View);
Assert.Same(viewContext.Writer, viewComponentContext.ViewContext.Writer);
// Double-check the convenience properties.
Assert.Same(viewComponentContext.ViewContext.ViewData, viewComponentContext.ViewData);
Assert.Same(viewComponentContext.ViewContext.Writer, viewComponentContext.Writer);
// New VDD instance but initial ModelMetadata copied over.
Assert.NotSame(viewData, viewComponentContext.ViewData);
Assert.Same(viewData.ModelMetadata, viewComponentContext.ViewData.ModelMetadata);
}
public static TheoryData<object, Type> IncompatibleModelData
{
get
{
// Small "anything but int" grab bag of instances and expected types.
return new TheoryData<object, Type>
{
{ null, typeof(object) },
{ true, typeof(bool) },
{ 43.78, typeof(double) },
{ "test string", typeof(string) },
{ new List<int>(), typeof(List<int>) },
{ new List<string>(), typeof(List<string>) },
};
}
}
[Theory]
[MemberData(nameof(IncompatibleModelData))]
public void ViewDataModelSetter_DoesNotThrow_IfValueIncompatibleWithSourceDeclaredType(
object model,
Type expectedType)
{
// Arrange
var httpContext = new DefaultHttpContext();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var viewData = new ViewDataDictionary<int>(new EmptyModelMetadataProvider());
var viewContext = new ViewContext(
actionContext,
NullView.Instance,
viewData,
new TempDataDictionary(httpContext, new SessionStateTempDataProvider()),
TextWriter.Null,
new HtmlHelperOptions());
var viewComponentDescriptor = new ViewComponentDescriptor();
var viewComponentContext = new ViewComponentContext(
viewComponentDescriptor,
new Dictionary<string, object>(),
new HtmlTestEncoder(),
viewContext,
TextWriter.Null);
// Act (does not throw)
// Non-ints can be assigned despite type restrictions in the source ViewDataDictionary.
viewComponentContext.ViewData.Model = model;
// Assert
Assert.Equal(expectedType, viewComponentContext.ViewData.ModelMetadata.ModelType);
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Internal;
using Xunit;
@ -111,7 +112,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
[Fact]
public void CopyConstructors_InitalizeModelAndModelMetadataBasedOnSource_ModelOfSubclass()
public void CopyConstructor_InitalizesModelAndModelMetadataBasedOnSource_ModelOfSubclass()
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
@ -120,37 +121,36 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
Model = model,
};
source["foo"] = "bar";
source.TemplateInfo.HtmlFieldPrefix = "prefix";
// Act
var viewData1 = new ViewDataDictionary<TestModel>(source);
var viewData2 = new ViewDataDictionary(source);
var viewData = new ViewDataDictionary(source);
// Assert
Assert.NotNull(viewData1.ModelState);
Assert.NotNull(viewData1.TemplateInfo);
Assert.Equal("prefix", viewData1.TemplateInfo.HtmlFieldPrefix);
Assert.NotSame(source.TemplateInfo, viewData1.TemplateInfo);
Assert.Same(model, viewData1.Model);
Assert.NotNull(viewData1.ModelMetadata);
Assert.Equal(typeof(SupremeTestModel), viewData1.ModelMetadata.ModelType);
Assert.Same(source.ModelMetadata, viewData1.ModelMetadata);
Assert.Equal(source.Count, viewData1.Count);
Assert.Equal("bar", viewData1["foo"]);
Assert.IsType<CopyOnWriteDictionary<string, object>>(viewData1.Data);
Assert.Same(model, viewData.Model);
Assert.NotNull(viewData.ModelMetadata);
Assert.Equal(typeof(SupremeTestModel), viewData.ModelMetadata.ModelType);
Assert.Same(source.ModelMetadata, viewData.ModelMetadata);
}
Assert.NotNull(viewData2.ModelState);
Assert.NotNull(viewData2.TemplateInfo);
Assert.Equal("prefix", viewData2.TemplateInfo.HtmlFieldPrefix);
Assert.NotSame(source.TemplateInfo, viewData2.TemplateInfo);
Assert.Same(model, viewData2.Model);
Assert.NotNull(viewData2.ModelMetadata);
Assert.Equal(typeof(SupremeTestModel), viewData2.ModelMetadata.ModelType);
Assert.Same(source.ModelMetadata, viewData2.ModelMetadata);
Assert.Equal(source.Count, viewData2.Count);
Assert.Equal("bar", viewData2["foo"]);
Assert.IsType<CopyOnWriteDictionary<string, object>>(viewData2.Data);
[Fact]
public void CopyConstructor_InitializesModelBasedOnSource_ModelMetadataBasedOnTModel()
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
var model = new SupremeTestModel();
var source = new ViewDataDictionary(metadataProvider)
{
Model = model,
};
// Act
var viewData = new ViewDataDictionary<TestModel>(source);
// Assert
Assert.Same(model, viewData.Model);
Assert.NotNull(viewData.ModelMetadata);
Assert.Equal(typeof(SupremeTestModel), viewData.ModelMetadata.ModelType);
Assert.Same(source.ModelMetadata, viewData.ModelMetadata);
}
[Fact]
@ -204,7 +204,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
[Fact]
public void CopyConstructors_ThrowInvalidOperation_IfModelIncompatible()
public void CopyConstructors_ThrowInvalidOperation_IfModelIncompatibleWithDeclaredType()
{
// Arrange
var expectedMessage = "The model item passed into the ViewDataDictionary is of type 'System.Int32', " +
@ -223,6 +223,109 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
Assert.Equal(expectedMessage, exception.Message);
}
public static TheoryData<object, Type> IncompatibleModelData
{
get
{
// Small "anything but TestModel" grab bag of instances and expected types.
return new TheoryData<object, Type>
{
{ true, typeof(bool) },
{ 23, typeof(int) },
{ 43.78, typeof(double) },
{ "test string", typeof(string) },
{ new List<int>(), typeof(List<int>) },
{ new List<string>(), typeof(List<string>) },
{ new List<TestModel>(), typeof(List<TestModel>) },
};
}
}
[Theory]
[MemberData(nameof(IncompatibleModelData))]
public void CopyConstructorToObject_DoesNotThrow_IfModelIncompatibleWithDeclaredType(
object model,
Type expectedType)
{
// Arrange
var source = new ViewDataDictionary<TestModel>(new EmptyModelMetadataProvider());
// Act
var viewData = new ViewDataDictionary<object>(source, model);
// Assert
Assert.NotNull(viewData.ModelExplorer);
Assert.NotSame(source.ModelExplorer, viewData.ModelExplorer);
Assert.NotNull(viewData.ModelMetadata);
Assert.NotSame(source.ModelMetadata, viewData.ModelMetadata);
Assert.Equal(expectedType, viewData.ModelMetadata.ModelType);
}
[Theory]
[InlineData(null)]
[InlineData(23)]
public void CopyConstructor_DoesNotChangeMetadata_WhenValueCompatibleWithSourceMetadata(int? model)
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
var source = new ViewDataDictionary<int?>(metadataProvider)
{
Model = -48,
};
// Act
var viewData = new ViewDataDictionary<int?>(source, model);
// Assert
Assert.NotNull(viewData.ModelExplorer);
Assert.NotSame(source.ModelExplorer, viewData.ModelExplorer);
Assert.Same(source.ModelMetadata, viewData.ModelMetadata);
Assert.Equal(typeof(int?), viewData.ModelMetadata.ModelType);
Assert.Equal(viewData.Model, viewData.ModelExplorer.Model);
Assert.Equal(model, viewData.Model);
}
[Fact]
public void CopyConstructor_UpdatesMetadata_IfDeclaredTypeChangesIncompatibly()
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
var source = new ViewDataDictionary<string>(metadataProvider);
// Act
var viewData = new ViewDataDictionary<int?>(source);
// Assert
Assert.NotNull(viewData.ModelExplorer);
Assert.NotSame(source.ModelExplorer, viewData.ModelExplorer);
Assert.NotSame(source.ModelMetadata, viewData.ModelMetadata);
Assert.NotEqual(source.ModelMetadata.ModelType, viewData.ModelMetadata.ModelType);
Assert.Equal(typeof(int?), viewData.ModelMetadata.ModelType);
}
[Fact]
public void CopyConstructor_PreservesModelExplorer_WhenPassedIdenticalModel()
{
// Arrange
var model = new TestModel();
var metadataProvider = new EmptyModelMetadataProvider();
var source = new ViewDataDictionary<TestModel>(metadataProvider)
{
Model = model,
};
// Act
var viewData = new ViewDataDictionary<TestModel>(source, model);
// Assert
Assert.NotNull(viewData.ModelExplorer);
Assert.Same(source.ModelExplorer, viewData.ModelExplorer);
Assert.Same(source.ModelMetadata, viewData.ModelMetadata);
Assert.Equal(typeof(TestModel), viewData.ModelMetadata.ModelType);
Assert.Equal(viewData.Model, viewData.ModelExplorer.Model);
Assert.Equal(model, viewData.Model);
}
[Fact]
public void ModelSetters_AcceptCompatibleValue()
{
@ -270,7 +373,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
[Fact]
public void ModelSetters_ThrowInvalidOperation_IfModelIncompatible()
public void ModelSetters_ThrowInvalidOperation_IfModelIncompatibleWithDeclaredType()
{
// Arrange
var expectedMessage = "The model item passed into the ViewDataDictionary is of type 'System.Int32', " +

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
@ -50,20 +49,34 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
[Fact]
public void SetModelUsesPassedInModelMetadataProvider()
public void Constructor_GetsNewModelMetadata()
{
// Arrange
var metadataProvider = new Mock<IModelMetadataProvider>();
var metadataProvider = new Mock<IModelMetadataProvider>(MockBehavior.Strict);
metadataProvider
.Setup(m => m.GetMetadataForType(typeof(object)))
.Returns(new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)))
.Verifiable();
var modelState = new ModelStateDictionary();
// Act
var viewData = new ViewDataDictionary(metadataProvider.Object, modelState);
// Assert
Assert.NotNull(viewData.ModelMetadata);
metadataProvider.Verify(m => m.GetMetadataForType(typeof(object)), Times.Once());
}
[Fact]
public void SetModel_DoesNotGetNewModelMetadata_IfTypeCompatible()
{
// Arrange
var metadataProvider = new Mock<IModelMetadataProvider>(MockBehavior.Strict);
metadataProvider
.Setup(m => m.GetMetadataForType(typeof(TestModel)))
.Returns(new EmptyModelMetadataProvider().GetMetadataForType(typeof(TestModel)))
.Verifiable();
var modelState = new ModelStateDictionary();
var viewData = new TestViewDataDictionary(metadataProvider.Object, modelState);
var viewData = new TestViewDataDictionary(metadataProvider.Object, typeof(TestModel));
var model = new TestModel();
// Act
@ -71,12 +84,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
// Assert
Assert.NotNull(viewData.ModelMetadata);
metadataProvider.Verify();
metadataProvider.Verify(m => m.GetMetadataForType(typeof(TestModel)), Times.Once());
}
// When SetModel is called, only GetMetadataForType from MetadataProvider is expected to be called.
[Fact]
public void SetModelCallsGetMetadataForTypeExactlyOnce()
public void SetModel_GetsNewModelMetadata_IfSourceTypeIsObject()
{
// Arrange
var metadataProvider = new Mock<IModelMetadataProvider>(MockBehavior.Strict);
@ -88,8 +100,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
.Setup(m => m.GetMetadataForType(typeof(TestModel)))
.Returns(new EmptyModelMetadataProvider().GetMetadataForType(typeof(TestModel)))
.Verifiable();
var modelState = new ModelStateDictionary();
var viewData = new TestViewDataDictionary(metadataProvider.Object, modelState);
var viewData = new TestViewDataDictionary(metadataProvider.Object);
var model = new TestModel();
// Act
@ -97,15 +108,48 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
// Assert
Assert.NotNull(viewData.ModelMetadata);
// Verifies if the GetMetadataForType is called only once.
metadataProvider.Verify(
m => m.GetMetadataForType(typeof(object)), Times.Once());
// Verifies if GetMetadataForProperties and GetMetadataForProperty is not called.
metadataProvider.Verify(
m => m.GetMetadataForProperties(typeof(object)), Times.Never());
// For the constructor.
metadataProvider.Verify(m => m.GetMetadataForType(typeof(object)), Times.Once());
// For SetModel().
metadataProvider.Verify(m => m.GetMetadataForType(typeof(TestModel)), Times.Once());
}
public static TheoryData<object> SetModelData
public static TheoryData<object, Type> IncompatibleModelData
{
get
{
// Small "anything but TestModel" grab bag of instances and expected types.
return new TheoryData<object, Type>
{
{ true, typeof(bool) },
{ 23, typeof(int) },
{ 43.78, typeof(double) },
{ "test string", typeof(string) },
{ new List<int>(), typeof(List<int>) },
{ new List<string>(), typeof(List<string>) },
{ new List<TestModel>(), typeof(List<TestModel>) },
};
}
}
[Theory]
[MemberData(nameof(IncompatibleModelData))]
public void SetModel_Throws_IfModelIncompatibleWithDeclaredType(object model, Type expectedType)
{
// Arrange
var viewData = new TestViewDataDictionary(new EmptyModelMetadataProvider(), typeof(TestModel));
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => viewData.SetModelPublic(model));
Assert.Equal(
$"The model item passed into the ViewDataDictionary is of type '{ model.GetType() }', but this " +
$"ViewDataDictionary instance requires a model item of type '{ typeof(TestModel) }'.",
exception.Message);
}
public static TheoryData<object> EnumerableModelData
{
get
{
@ -128,8 +172,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
[Theory]
[MemberData(nameof(SetModelData))]
public void SetModelDoesNotThrowOnEnumerableModel(object model)
[MemberData(nameof(EnumerableModelData))]
public void ModelSetter_DoesNotThrowOnEnumerableModel(object model)
{
// Arrange
var vdd = new ViewDataDictionary(new EmptyModelMetadataProvider());
@ -149,7 +193,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
var model = new TestModel();
var source = new ViewDataDictionary(metadataProvider)
{
Model = model
ModelExplorer = metadataProvider.GetModelExplorerForType(typeof(TestModel), model),
};
source["foo"] = "bar";
source.TemplateInfo.HtmlFieldPrefix = "prefix";
@ -171,88 +215,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
Assert.IsType<CopyOnWriteDictionary<string, object>>(viewData.Data);
}
[Fact]
public void CopyConstructorUsesPassedInModel_DifferentModels()
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
var model = new TestModel();
var source = new ViewDataDictionary(metadataProvider)
{
Model = "string model"
};
source["key1"] = "value1";
source.TemplateInfo.HtmlFieldPrefix = "prefix";
// Act
var viewData = new ViewDataDictionary(source, model);
// Assert
Assert.NotNull(viewData.ModelState);
Assert.NotNull(viewData.TemplateInfo);
Assert.Equal("prefix", viewData.TemplateInfo.HtmlFieldPrefix);
Assert.NotSame(source.TemplateInfo, viewData.TemplateInfo);
Assert.Same(model, viewData.Model);
Assert.NotNull(viewData.ModelMetadata);
Assert.Equal(typeof(TestModel), viewData.ModelMetadata.ModelType);
Assert.NotSame(source.ModelMetadata, viewData.ModelMetadata);
Assert.Equal(source.Count, viewData.Count);
Assert.Equal("value1", viewData["key1"]);
Assert.IsType<CopyOnWriteDictionary<string, object>>(viewData.Data);
}
[Fact]
public void CopyConstructorUsesPassedInModel_SameModel()
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
var model = new TestModel();
var source = new ViewDataDictionary(metadataProvider)
{
Model = model
};
source["key1"] = "value1";
// Act
var viewData = new ViewDataDictionary(source, model);
// Assert
Assert.NotNull(viewData.ModelState);
Assert.NotNull(viewData.TemplateInfo);
Assert.NotSame(source.TemplateInfo, viewData.TemplateInfo);
Assert.Same(model, viewData.Model);
Assert.NotNull(viewData.ModelMetadata);
Assert.Equal(typeof(TestModel), viewData.ModelMetadata.ModelType);
Assert.Same(source.ModelMetadata, viewData.ModelMetadata);
Assert.Equal(source.Count, viewData.Count);
Assert.Equal("value1", viewData["key1"]);
Assert.IsType<CopyOnWriteDictionary<string, object>>(viewData.Data);
}
[Fact]
public void CopyConstructorDoesNotThrowOnNullModel()
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
var source = new ViewDataDictionary(metadataProvider);
source["key1"] = "value1";
// Act
var viewData = new ViewDataDictionary(source, model: null);
// Assert
Assert.NotNull(viewData.ModelState);
Assert.NotNull(viewData.TemplateInfo);
Assert.NotSame(source.TemplateInfo, viewData.TemplateInfo);
Assert.Null(viewData.Model);
Assert.NotNull(viewData.ModelMetadata);
Assert.Equal(typeof(object), viewData.ModelMetadata.ModelType);
Assert.Same(source.ModelMetadata, viewData.ModelMetadata);
Assert.Equal(source.Count, viewData.Count);
Assert.Equal("value1", viewData["key1"]);
Assert.IsType<CopyOnWriteDictionary<string, object>>(viewData.Data);
}
public static TheoryData<Type, object> CopyModelMetadataData
{
get
@ -275,7 +237,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
[Theory]
[MemberData(nameof(CopyModelMetadataData))]
public void CopyConstructors_CopyModelMetadata(Type type, object instance)
public void CopyConstructor_CopiesModelMetadata(Type type, object instance)
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
@ -285,56 +247,75 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
};
// Act
var viewData1 = new ViewDataDictionary(source);
var viewData2 = new ViewDataDictionary(source, model: instance);
var viewData = new ViewDataDictionary(source);
// Assert
Assert.Same(source.ModelMetadata, viewData1.ModelMetadata);
Assert.Same(source.ModelMetadata, viewData2.ModelMetadata);
Assert.Same(source.ModelMetadata, viewData.ModelMetadata);
}
[Fact]
public void CopyConstructors_CopyModelMetadata_ForTypeObject()
public void CopyConstructor_CopiesModelMetadata_ForTypeObject()
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
var source = new ViewDataDictionary(metadataProvider);
// Act
var viewData1 = new ViewDataDictionary(source);
var viewData2 = new ViewDataDictionary(source, model: null);
var viewData = new ViewDataDictionary(source);
// Assert
Assert.Same(viewData1.ModelMetadata, viewData2.ModelMetadata);
Assert.Same(source.ModelMetadata, viewData.ModelMetadata);
Assert.Equal(typeof(object), viewData.ModelMetadata.ModelType);
}
[Theory]
[InlineData(typeof(int), "test string", typeof(string))]
[InlineData(typeof(string), 23, typeof(int))]
[InlineData(typeof(IEnumerable<string>), new object[] { "1", "2", "3", }, typeof(object[]))]
[InlineData(typeof(List<string>), new object[] { 1, 2, 3, }, typeof(object[]))]
public void CopyConstructors_OverrideSourceMetadata_IfModelNonNull(
[InlineData(typeof(IEnumerable<string>), new object[] { "1", "2", "3" }, typeof(object[]))]
[InlineData(typeof(List<string>), new object[] { 1, 2, 3 }, typeof(object[]))]
public void ModelSetter_UpdatesModelMetadata_IfModelIncompatibleWithSourceMetadata(
Type sourceType,
object instance,
object model,
Type expectedType)
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
var source = new ViewDataDictionary(metadataProvider);
var source = new ViewDataDictionary(metadataProvider)
{
ModelExplorer = metadataProvider.GetModelExplorerForType(sourceType, model: null),
};
var sourceMetadata = source.ModelMetadata;
var viewData = new ViewDataDictionary(source);
// Act
var viewData1 = new ViewDataDictionary(source)
{
Model = instance,
};
var viewData2 = new ViewDataDictionary(source, model: instance);
viewData.Model = model;
// Assert
Assert.NotNull(viewData1.ModelMetadata);
Assert.Equal(expectedType, viewData1.ModelMetadata.ModelType);
Assert.NotSame(source.ModelExplorer, viewData.ModelExplorer);
Assert.NotSame(source.ModelMetadata, viewData.ModelMetadata);
Assert.Equal(expectedType, viewData.ModelMetadata.ModelType);
}
Assert.NotNull(viewData2.ModelMetadata);
Assert.Equal(expectedType, viewData2.ModelMetadata.ModelType);
[Theory]
[InlineData(typeof(int), 23)]
[InlineData(typeof(string), "test string")]
[InlineData(typeof(IEnumerable<string>), new string[] { "1", "2", "3" })]
public void ModelSetter_PreservesSourceMetadata_IfModelCompatible(Type sourceType, object model)
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
var source = new ViewDataDictionary(metadataProvider)
{
ModelExplorer = metadataProvider.GetModelExplorerForType(sourceType, model: null),
};
var viewData = new ViewDataDictionary(source);
// Act
viewData.Model = model;
// Assert
Assert.NotSame(source.ModelExplorer, viewData.ModelExplorer);
Assert.Same(source.ModelMetadata, viewData.ModelMetadata);
}
[Fact]
@ -362,6 +343,33 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
Assert.NotSame(originalExplorer, viewData.ModelExplorer);
}
[Theory]
[InlineData(typeof(object))]
[InlineData(typeof(string))]
public void ModelSetter_DifferentType_UpdatesModelMetadata(Type originalMetadataType)
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
var metadata = metadataProvider.GetMetadataForType(originalMetadataType);
var explorer = new ModelExplorer(metadataProvider, metadata, model: null);
var viewData = new TestViewDataDictionary(metadataProvider)
{
ModelExplorer = explorer,
};
// Act
viewData.Model = true;
// Assert
Assert.NotNull(viewData.ModelExplorer);
Assert.NotNull(viewData.ModelMetadata);
Assert.NotSame(explorer, viewData.ModelExplorer);
Assert.Equal(typeof(bool), viewData.ModelMetadata.ModelType);
var model = Assert.IsType<bool>(viewData.Model);
Assert.True(model);
}
[Fact]
public void ModelSetter_SetNullableNonNull_UpdatesModelExplorer()
{
@ -381,7 +389,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
Assert.NotNull(viewData.ModelMetadata);
Assert.NotNull(viewData.ModelExplorer);
Assert.Same(metadata, viewData.ModelMetadata);
Assert.Same(metadata.ModelType, explorer.ModelType);
Assert.NotSame(explorer, viewData.ModelExplorer);
Assert.Equal(viewData.Model, viewData.ModelExplorer.Model);
@ -389,6 +396,20 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
Assert.True(model);
}
[Fact]
public void ModelSetter_SetNonNullableToNull_Throws()
{
// Arrange
var viewData = new TestViewDataDictionary(new EmptyModelMetadataProvider(), typeof(int));
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => viewData.SetModelPublic(value: null));
Assert.Equal(
"The model item passed is null, but this ViewDataDictionary instance requires a non-null model item " +
$"of type '{ typeof(int) }'.",
exception.Message);
}
[Fact]
public void ModelSetter_SameType_BoxedValueTypeUpdatesModelExplorer()
{
@ -639,7 +660,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
// Arrange
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider());
var model = new object();
viewData = new ViewDataDictionary(viewData, model);
viewData.Model = model;
// Act
var result = viewData.Eval(expression);
@ -799,15 +820,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
private class TestViewDataDictionary : ViewDataDictionary
{
public TestViewDataDictionary(
IModelMetadataProvider modelMetadataProvider,
ModelStateDictionary modelState)
: base(modelMetadataProvider, modelState)
public TestViewDataDictionary(IModelMetadataProvider metadataProvider)
: base(metadataProvider)
{
}
public TestViewDataDictionary(ViewDataDictionary source)
: base(source)
public TestViewDataDictionary(IModelMetadataProvider metadataProvider, Type declaredModelType)
: base(metadataProvider, declaredModelType)
{
}

View File

@ -0,0 +1,42 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using HtmlGenerationWebSite.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
namespace HtmlGenerationWebSite.Components
{
public class CheckViewData___LackModel : ViewComponent
{
public IViewComponentResult Invoke()
{
var metadata = ViewData.ModelMetadata;
var writer = ViewContext.Writer;
writer.WriteLine("<h5>Check View Data - LackModel view component</h5>");
writer.WriteLine($"<div class=\"col-md-3\">MetadataKind: '{ metadata.MetadataKind }'</div>");
writer.WriteLine($"<div class=\"col-md-3\">ModelType: '{ metadata.ModelType.Name }'</div>");
if (metadata.MetadataKind == ModelMetadataKind.Property)
{
writer.WriteLine($"<div class=\"col-md-3\">PropertyName: '{ metadata.PropertyName }'</div>");
}
// Confirm view component is able to set the model to anything.
ViewData.Model = 78.9;
// Expected metadata is for typeof(object).
metadata = ViewData.ModelMetadata;
writer.WriteLine("<h5>Check View Data - LackModel view component after setting Model to 78.9</h5>");
writer.WriteLine($"<div class=\"col-md-3\">MetadataKind: '{ metadata.MetadataKind }'</div>");
writer.WriteLine($"<div class=\"col-md-3\">ModelType: '{ metadata.ModelType.Name }'</div>");
if (metadata.MetadataKind == ModelMetadataKind.Property)
{
writer.WriteLine($"<div class=\"col-md-3\">PropertyName: '{ metadata.PropertyName }'</div>");
}
TemplateModel templateModel = new SuperTemplateModel();
return View(templateModel);
}
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
namespace HtmlGenerationWebSite.Components
{
public class CheckViewData : ViewComponent
{
public IViewComponentResult Invoke()
{
var metadata = ViewData.ModelMetadata;
var writer = ViewContext.Writer;
writer.WriteLine("<h5>Check View Data view component</h5>");
writer.WriteLine($"<div class=\"col-md-3\">MetadataKind: '{ metadata.MetadataKind }'</div>");
writer.WriteLine($"<div class=\"col-md-3\">ModelType: '{ metadata.ModelType.Name }'</div>");
if (metadata.MetadataKind == ModelMetadataKind.Property)
{
writer.WriteLine($"<div class=\"col-md-3\">PropertyName: '{ metadata.PropertyName }'</div>");
}
return View();
}
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using HtmlGenerationWebSite.Models;
using Microsoft.AspNetCore.Mvc;
namespace HtmlGenerationWebSite.Controllers
{
public class CheckViewData : Controller
{
public IActionResult AtViewModel()
{
return View(new SuperViewModel());
}
public IActionResult NullViewModel()
{
return View("AtViewModel");
}
public IActionResult ViewModel()
{
return View(new SuperViewModel());
}
}
}

View File

@ -0,0 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace HtmlGenerationWebSite.Models
{
public class PartialModel
{
}
}

View File

@ -0,0 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace HtmlGenerationWebSite.Models
{
public class SuperTemplateModel : TemplateModel
{
}
}

View File

@ -0,0 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace HtmlGenerationWebSite.Models
{
public class SuperViewModel : ViewModel
{
}
}

View File

@ -0,0 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace HtmlGenerationWebSite.Models
{
public class TemplateModel
{
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace HtmlGenerationWebSite.Models
{
public class ViewModel
{
public int Integer { get; set; } = 23;
public long? NullableLong { get; set; } = 24L;
public TemplateModel Template { get; set; } = new SuperTemplateModel();
}
}

View File

@ -0,0 +1,37 @@
@using HtmlGenerationWebSite.Components
@using HtmlGenerationWebSite.Models
@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
@model ViewModel
@{
var metadata = ViewData.ModelMetadata;
}
<div class="row">
<h4>At Model index</h4>
<div class="col-md-3">MetadataKind: '@metadata.MetadataKind'</div>
<div class="col-md-3">ModelType: '@metadata.ModelType.Name'</div>
@if (metadata.MetadataKind == ModelMetadataKind.Property)
{
<div class="col-md-3">PropertyName: '@metadata.PropertyName'</div>
}
</div>
<div class="row">
@Html.DisplayFor(m => m)
</div>
<div class="row">
@Html.Partial("PartialForViewModel")
</div>
<div class="row">
@(await Component.InvokeAsync<CheckViewData>())
</div>
<div class="row">
@Html.DisplayFor(m => m.Integer)
</div>
<div class="row">
@Html.DisplayFor(m => m.NullableLong)
</div>
<div class="row">
@Html.DisplayFor(m => m.Template)
</div>

View File

@ -0,0 +1,15 @@
@using HtmlGenerationWebSite.Models
@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
@model ViewModel
@{
var metadata = ViewData.ModelMetadata;
}
<h5>Check View Data view component's view</h5>
<div class="col-md-3">MetadataKind: '@metadata.MetadataKind'</div>
<div class="col-md-3">ModelType: '@metadata.ModelType.Name'</div>
@if (metadata.MetadataKind == ModelMetadataKind.Property)
{
<div class="col-md-3">PropertyName: '@metadata.PropertyName'</div>
}

View File

@ -0,0 +1,14 @@
@using HtmlGenerationWebSite.Models
@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
@{
var metadata = ViewData.ModelMetadata;
}
<h5>Check View Data - LackModel view component's view</h5>
<div class="col-md-3">MetadataKind: '@metadata.MetadataKind'</div>
<div class="col-md-3">ModelType: '@metadata.ModelType.Name'</div>
@if (metadata.MetadataKind == ModelMetadataKind.Property)
{
<div class="col-md-3">PropertyName: '@metadata.PropertyName'</div>
}

View File

@ -0,0 +1,27 @@
@using HtmlGenerationWebSite.Models
@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
@{
var metadata = ViewData.ModelMetadata;
}
<h5>Template for Int32 - LackModel</h5>
<div class="col-md-3">MetadataKind: '@metadata.MetadataKind'</div>
<div class="col-md-3">ModelType: '@metadata.ModelType.Name'</div>
@if (metadata.MetadataKind == ModelMetadataKind.Property)
{
<div class="col-md-3">PropertyName: '@metadata.PropertyName'</div>
}
@{
ViewData.Model = 78.9;
metadata = ViewData.ModelMetadata;
}
<h5>Template for Int32 - LackModel after setting Model to 78.9</h5>
<div class="col-md-3">MetadataKind: '@metadata.MetadataKind'</div>
<div class="col-md-3">ModelType: '@metadata.ModelType.Name'</div>
@if (metadata.MetadataKind == ModelMetadataKind.Property)
{
<div class="col-md-3">PropertyName: '@metadata.PropertyName'</div>
}

View File

@ -0,0 +1,15 @@
@using HtmlGenerationWebSite.Models
@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
@model int?
@{
var metadata = ViewData.ModelMetadata;
}
<h5>Template for Int32</h5>
<div class="col-md-3">MetadataKind: '@metadata.MetadataKind'</div>
<div class="col-md-3">ModelType: '@metadata.ModelType.Name'</div>
@if (metadata.MetadataKind == ModelMetadataKind.Property)
{
<div class="col-md-3">PropertyName: '@metadata.PropertyName'</div>
}

View File

@ -0,0 +1,22 @@
@using HtmlGenerationWebSite.Components
@using HtmlGenerationWebSite.Models
@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
@{
var metadata = ViewData.ModelMetadata;
}
<h5>Template for Int64</h5>
<div class="col-md-3">MetadataKind: '@metadata.MetadataKind'</div>
<div class="col-md-3">ModelType: '@metadata.ModelType.Name'</div>
@if (metadata.MetadataKind == ModelMetadataKind.Property)
{
<div class="col-md-3">PropertyName: '@metadata.PropertyName'</div>
}
<div class="row">
@Html.Partial(partialViewName: "LackModel.cshtml")
</div>
<div class="row">
@(await Component.InvokeAsync<CheckViewData___LackModel>())
</div>

View File

@ -0,0 +1,15 @@
@using HtmlGenerationWebSite.Models
@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
@model long?
@{
var metadata = ViewData.ModelMetadata;
}
<h5>Template for Int64</h5>
<div class="col-md-3">MetadataKind: '@metadata.MetadataKind'</div>
<div class="col-md-3">ModelType: '@metadata.ModelType.Name'</div>
@if (metadata.MetadataKind == ModelMetadataKind.Property)
{
<div class="col-md-3">PropertyName: '@metadata.PropertyName'</div>
}

View File

@ -0,0 +1,14 @@
@using HtmlGenerationWebSite.Models
@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
@{
var metadata = ViewData.ModelMetadata;
}
<h5>Template / partial for ... - LackModel</h5>
<div class="col-md-3">MetadataKind: '@metadata.MetadataKind'</div>
<div class="col-md-3">ModelType: '@metadata.ModelType.Name'</div>
@if (metadata.MetadataKind == ModelMetadataKind.Property)
{
<div class="col-md-3">PropertyName: '@metadata.PropertyName'</div>
}

View File

@ -0,0 +1,15 @@
@using HtmlGenerationWebSite.Models
@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
@model TemplateModel
@{
var metadata = ViewData.ModelMetadata;
}
<h5>Template for TemplateModel</h5>
<div class="col-md-3">MetadataKind: '@metadata.MetadataKind'</div>
<div class="col-md-3">ModelType: '@metadata.ModelType.Name'</div>
@if (metadata.MetadataKind == ModelMetadataKind.Property)
{
<div class="col-md-3">PropertyName: '@metadata.PropertyName'</div>
}

View File

@ -0,0 +1,15 @@
@using HtmlGenerationWebSite.Models
@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
@model ViewModel
@{
var metadata = ViewData.ModelMetadata;
}
<h5>Template for ViewModel</h5>
<div class="col-md-3">MetadataKind: '@metadata.MetadataKind'</div>
<div class="col-md-3">ModelType: '@metadata.ModelType.Name'</div>
@if (metadata.MetadataKind == ModelMetadataKind.Property)
{
<div class="col-md-3">PropertyName: '@metadata.PropertyName'</div>
}

View File

@ -0,0 +1,15 @@
@using HtmlGenerationWebSite.Models
@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
@model ViewModel
@{
var metadata = ViewData.ModelMetadata;
}
<h5>Partial for ViewModel</h5>
<div class="col-md-3">MetadataKind: '@metadata.MetadataKind'</div>
<div class="col-md-3">ModelType: '@metadata.ModelType.Name'</div>
@if (metadata.MetadataKind == ModelMetadataKind.Property)
{
<div class="col-md-3">PropertyName: '@metadata.PropertyName'</div>
}

View File

@ -0,0 +1,40 @@
@using HtmlGenerationWebSite.Components
@using HtmlGenerationWebSite.Models
@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
@* Need the model directive in top-level view. Otherwise the controller would have to set the ViewData property. *@
@* Put another way, Controller lacks the View<TModel>([...,] TModel) overloads that ViewComponent has. *@
@model ViewModel
@{
var metadata = ViewData.ModelMetadata;
}
<div class="row">
<h4>View Model index</h4>
<div class="col-md-3">MetadataKind: '@metadata.MetadataKind'</div>
<div class="col-md-3">ModelType: '@metadata.ModelType.Name'</div>
@if (metadata.MetadataKind == ModelMetadataKind.Property)
{
<div class="col-md-3">PropertyName: '@metadata.PropertyName'</div>
}
</div>
<div class="row">
@Html.DisplayFor(m => m, templateName: "LackModel")
</div>
<div class="row">
@Html.Partial(partialViewName: "DisplayTemplates/LackModel.cshtml")
</div>
<div class="row">
@(await Component.InvokeAsync<CheckViewData___LackModel>())
</div>
<div class="row">
@Html.DisplayFor(m => m.Integer, templateName: "Int32 - LackModel")
</div>
<div class="row">
@Html.DisplayFor(m => m.NullableLong, templateName: "Int64 - LackModel")
</div>
<div class="row">
@Html.DisplayFor(m => m.Template, templateName: "LackModel")
</div>