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:
parent
a55544681d
commit
a0c8834c70
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public new TModel Model
|
||||
{
|
||||
get
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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', " +
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue