From a0c8834c703ee98e36a056f0c43205fc986ec60e Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Fri, 8 Apr 2016 12:12:46 -0700 Subject: [PATCH] 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` - note existing functional tests did not need to change - remove `ViewDataDictionary(ViewDataDictionary, object)` constructor; use `new VDD(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 --- .../ViewComponents/ViewComponentContext.cs | 2 +- .../ViewFeatures/HtmlHelper.cs | 2 +- .../ViewFeatures/TemplateBuilder.cs | 11 +- .../ViewFeatures/ViewDataDictionary.cs | 214 ++++++++----- .../ViewFeatures/ViewDataDictionaryOfT.cs | 1 + .../HtmlGenerationTest.cs | 29 ++ ...tionWebSite.CheckViewData.AtViewModel.html | 54 ++++ ...onWebSite.CheckViewData.NullViewModel.html | 54 ++++ ...rationWebSite.CheckViewData.ViewModel.html | 93 ++++++ .../ControllerTest.cs | 44 +++ .../Rendering/DefaultTemplatesUtilities.cs | 12 +- .../ViewComponentContextTest.cs | 112 +++++++ .../ViewDataDictionaryOfTModelTest.cs | 161 ++++++++-- .../ViewFeatures/ViewDataDictionaryTest.cs | 285 ++++++++++-------- .../Components/CheckViewData - LackModel.cs | 42 +++ .../Components/CheckViewData.cs | 26 ++ .../Controllers/CheckViewData.cs | 26 ++ .../Models/PartialModel.cs | 9 + .../Models/SuperTemplateModel.cs | 9 + .../Models/SuperViewModel.cs | 9 + .../Models/TemplateModel.cs | 9 + .../HtmlGenerationWebSite/Models/ViewModel.cs | 14 + .../Views/CheckViewData/AtViewModel.cshtml | 37 +++ .../Components/CheckViewData/Default.cshtml | 15 + .../CheckViewData___LackModel/Default.cshtml | 14 + .../DisplayTemplates/Int32 - LackModel.cshtml | 27 ++ .../DisplayTemplates/Int32.cshtml | 15 + .../DisplayTemplates/Int64 - LackModel.cshtml | 22 ++ .../DisplayTemplates/Int64.cshtml | 15 + .../DisplayTemplates/LackModel.cshtml | 14 + .../DisplayTemplates/TemplateModel.cshtml | 15 + .../DisplayTemplates/ViewModel.cshtml | 15 + .../CheckViewData/PartialForViewModel.cshtml | 15 + .../Views/CheckViewData/ViewModel.cshtml | 40 +++ 34 files changed, 1212 insertions(+), 250 deletions(-) create mode 100644 test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.CheckViewData.AtViewModel.html create mode 100644 test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.CheckViewData.NullViewModel.html create mode 100644 test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.CheckViewData.ViewModel.html create mode 100644 test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/ViewComponentContextTest.cs create mode 100644 test/WebSites/HtmlGenerationWebSite/Components/CheckViewData - LackModel.cs create mode 100644 test/WebSites/HtmlGenerationWebSite/Components/CheckViewData.cs create mode 100644 test/WebSites/HtmlGenerationWebSite/Controllers/CheckViewData.cs create mode 100644 test/WebSites/HtmlGenerationWebSite/Models/PartialModel.cs create mode 100644 test/WebSites/HtmlGenerationWebSite/Models/SuperTemplateModel.cs create mode 100644 test/WebSites/HtmlGenerationWebSite/Models/SuperViewModel.cs create mode 100644 test/WebSites/HtmlGenerationWebSite/Models/TemplateModel.cs create mode 100644 test/WebSites/HtmlGenerationWebSite/Models/ViewModel.cs create mode 100644 test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/AtViewModel.cshtml create mode 100644 test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/Components/CheckViewData/Default.cshtml create mode 100644 test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/Components/CheckViewData___LackModel/Default.cshtml create mode 100644 test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/Int32 - LackModel.cshtml create mode 100644 test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/Int32.cshtml create mode 100644 test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/Int64 - LackModel.cshtml create mode 100644 test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/Int64.cshtml create mode 100644 test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/LackModel.cshtml create mode 100644 test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/TemplateModel.cshtml create mode 100644 test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/ViewModel.cshtml create mode 100644 test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/PartialForViewModel.cshtml create mode 100644 test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/ViewModel.cshtml diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentContext.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentContext.cs index e45ba82154..0ef48b50d1 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentContext.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentContext.cs @@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents ViewContext = new ViewContext( viewContext, viewContext.View, - new ViewDataDictionary(viewContext.ViewData), + new ViewDataDictionary(viewContext.ViewData), writer); } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs index a3a61a2191..f19653f629 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs @@ -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(baseViewData, model); var viewContext = new ViewContext(ViewContext, view, newViewData, writer); await viewEngineResult.View.RenderAsync(viewContext); diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateBuilder.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateBuilder.cs index a8f586f297..d1d9421a75 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateBuilder.cs @@ -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(_viewData, model: null); + // Create VDD of type object so any model type is allowed. + var viewData = new ViewDataDictionary(_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; diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewDataDictionary.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewDataDictionary.cs index 9a3a7b055e..e506d992f9 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewDataDictionary.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewDataDictionary.cs @@ -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 class. /// /// - /// instance used to calculate - /// values. + /// instance used to create + /// instances. /// /// instance for this scope. /// For use when creating a for a new top-level scope. @@ -41,38 +42,27 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures /// /// instance to copy initial values from. /// + /// /// For use when copying a instance and the declared /// will not change e.g. when copying from a /// instance to a base instance. + /// + /// + /// This constructor should not be used in any context where may be set to a value + /// incompatible with the declared type of . + /// /// public ViewDataDictionary(ViewDataDictionary source) : this(source, source.Model, source._declaredModelType) { } - /// - /// Initializes a new instance of the class based in part on an existing - /// instance. This constructor is careful to avoid exceptions may throw when - /// is null. - /// - /// instance to copy initial values from. - /// Value for the property. - /// - /// For use when the new instance's declared is unknown but its - /// is known. In this case, is the best possible guess about the - /// declared type when is null. - /// - public ViewDataDictionary(ViewDataDictionary source, object model) - : this(source, model, declaredModelType: typeof(object)) - { - } - /// /// Initializes a new instance of the class. /// /// - /// instance used to calculate - /// values. + /// instance used to create + /// instances. /// /// Internal for testing. internal ViewDataDictionary(IModelMetadataProvider metadataProvider) @@ -84,12 +74,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures /// Initializes a new instance of the class. /// /// - /// instance used to calculate - /// values. + /// instance used to create + /// instances. /// /// - /// of values expected. Used to set - /// when is null. + /// of values expected. Used to set . /// /// /// For use when creating a derived for a new top-level scope. @@ -105,17 +94,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures /// Initializes a new instance of the class. /// /// - /// instance used to calculate - /// values. + /// instance used to create + /// instances. /// /// instance for this scope. /// - /// of values expected. Used to set - /// when is null. + /// of values expected. Used to set . /// /// /// For use when creating a derived for a new top-level scope. /// + // 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 /// /// instance to copy initial values from. /// - /// of values expected. Used to set - /// when is null. + /// of values expected. Used to set . /// /// /// @@ -180,8 +168,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures /// instance to copy initial values from. /// Value for the property. /// - /// of values expected. Used to set - /// when is null. + /// of values expected. Used to set . /// /// /// @@ -193,6 +180,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures /// . /// /// + // 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> holding information about an IEnumerable 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 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 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 where TModel is a non-Nullable value type. This may for + // example occur when activating a RazorPage and the container is null. + // - Constructing a ViewDataDictionary 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; } + /// + /// Gets or sets the current model. + /// 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); } } + /// + /// Gets the . + /// public ModelStateDictionary ModelState { get; } /// - /// for the current value or the declared if - /// is null. + /// Gets the for an expression, the (if + /// non-null), or the declared . /// /// /// Value is never null but may describe the class in some cases. This may for - /// example occur in controllers if is null. + /// example occur in controllers. /// public ModelMetadata ModelMetadata { @@ -274,13 +301,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures } /// - /// Gets or sets the for the . + /// Gets or sets the for the . /// public ModelExplorer ModelExplorer { get; set; } + /// + /// Gets the . + /// public TemplateInfo TemplateInfo { get; } #region IDictionary properties + /// // 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 } } + /// public int Count { get { return _data.Count; } } + /// public bool IsReadOnly { get { return _data.IsReadOnly; } } + /// public ICollection Keys { get { return _data.Keys; } } + /// public ICollection Values { get { return _data.Values; } @@ -360,6 +395,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures return FormatValue(value, format); } + /// + /// Formats the given using given . + /// + /// The value to format. + /// + /// The composite format (see http://msdn.microsoft.com/en-us/library/txafckwd.aspx). + /// + /// The formatted . 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. + /// + /// Set to ensure and + /// reflect the new . + /// + /// New value. 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 + /// public void Add(string key, object value) { if (key == null) @@ -479,6 +537,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures _data.Add(key, value); } + /// public bool ContainsKey(string key) { if (key == null) @@ -489,6 +548,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures return _data.ContainsKey(key); } + /// public bool Remove(string key) { if (key == null) @@ -499,6 +559,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures return _data.Remove(key); } + /// 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); } + /// public void Add(KeyValuePair item) { _data.Add(item); } + /// public void Clear() { _data.Clear(); } + /// public bool Contains(KeyValuePair item) { return _data.Contains(item); } + /// public void CopyTo(KeyValuePair[] array, int arrayIndex) { if (array == null) @@ -534,16 +599,19 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures _data.CopyTo(array, arrayIndex); } + /// public bool Remove(KeyValuePair item) { return _data.Remove(item); } + /// IEnumerator> IEnumerable>.GetEnumerator() { return _data.GetEnumerator(); } + /// IEnumerator IEnumerable.GetEnumerator() { return _data.GetEnumerator(); diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewDataDictionaryOfT.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewDataDictionaryOfT.cs index 447e6233fe..5cd4f4391b 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewDataDictionaryOfT.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewDataDictionaryOfT.cs @@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures { } + /// public new TModel Model { get diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs index dfdbb4b599..0bc7b6e328 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs @@ -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() { diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.CheckViewData.AtViewModel.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.CheckViewData.AtViewModel.html new file mode 100644 index 0000000000..21f738a390 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.CheckViewData.AtViewModel.html @@ -0,0 +1,54 @@ +
+

At Model index

+
MetadataKind: 'Type'
+
ModelType: 'SuperViewModel'
+
+ +
+ +
Template for ViewModel
+
MetadataKind: 'Type'
+
ModelType: 'SuperViewModel'
+ +
+
+ +
Partial for ViewModel
+
MetadataKind: 'Type'
+
ModelType: 'SuperViewModel'
+ +
+
+
Check View Data view component
+
MetadataKind: 'Type'
+
ModelType: 'SuperViewModel'
+ +
Check View Data view component's view
+
MetadataKind: 'Type'
+
ModelType: 'SuperViewModel'
+ +
+
+ +
Template for Int32
+
MetadataKind: 'Property'
+
ModelType: 'Int32'
+
PropertyName: 'Integer'
+ +
+
+ +
Template for Int64
+
MetadataKind: 'Property'
+
ModelType: 'Nullable`1'
+
PropertyName: 'NullableLong'
+ +
+
+ +
Template for TemplateModel
+
MetadataKind: 'Property'
+
ModelType: 'TemplateModel'
+
PropertyName: 'Template'
+ +
\ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.CheckViewData.NullViewModel.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.CheckViewData.NullViewModel.html new file mode 100644 index 0000000000..73bcf08e7f --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.CheckViewData.NullViewModel.html @@ -0,0 +1,54 @@ +
+

At Model index

+
MetadataKind: 'Type'
+
ModelType: 'ViewModel'
+
+ +
+ +
Template for ViewModel
+
MetadataKind: 'Type'
+
ModelType: 'ViewModel'
+ +
+
+ +
Partial for ViewModel
+
MetadataKind: 'Type'
+
ModelType: 'ViewModel'
+ +
+
+
Check View Data view component
+
MetadataKind: 'Type'
+
ModelType: 'ViewModel'
+ +
Check View Data view component's view
+
MetadataKind: 'Type'
+
ModelType: 'ViewModel'
+ +
+
+ +
Template for Int32
+
MetadataKind: 'Property'
+
ModelType: 'Int32'
+
PropertyName: 'Integer'
+ +
+
+ +
Template for Int64
+
MetadataKind: 'Property'
+
ModelType: 'Nullable`1'
+
PropertyName: 'NullableLong'
+ +
+
+ +
Template for TemplateModel
+
MetadataKind: 'Property'
+
ModelType: 'TemplateModel'
+
PropertyName: 'Template'
+ +
\ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.CheckViewData.ViewModel.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.CheckViewData.ViewModel.html new file mode 100644 index 0000000000..4e44113222 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.CheckViewData.ViewModel.html @@ -0,0 +1,93 @@ +
+

View Model index

+
MetadataKind: 'Type'
+
ModelType: 'SuperViewModel'
+
+ +
+ + +
Template / partial for ... - LackModel
+
MetadataKind: 'Type'
+
ModelType: 'SuperViewModel'
+ +
+
+ + +
Template / partial for ... - LackModel
+
MetadataKind: 'Type'
+
ModelType: 'SuperViewModel'
+ +
+
+
Check View Data - LackModel view component
+
MetadataKind: 'Type'
+
ModelType: 'SuperViewModel'
+
Check View Data - LackModel view component after setting Model to 78.9
+
MetadataKind: 'Type'
+
ModelType: 'Double'
+ + +
Check View Data - LackModel view component's view
+
MetadataKind: 'Type'
+
ModelType: 'SuperTemplateModel'
+ +
+
+ + +
Template for Int32 - LackModel
+
MetadataKind: 'Property'
+
ModelType: 'Int32'
+
PropertyName: 'Integer'
+ + +
Template for Int32 - LackModel after setting Model to 78.9
+
MetadataKind: 'Type'
+
ModelType: 'Double'
+ +
+
+ + +
Template for Int64
+
MetadataKind: 'Property'
+
ModelType: 'Nullable`1'
+
PropertyName: 'NullableLong'
+ +
+ + +
Template / partial for ... - LackModel
+
MetadataKind: 'Property'
+
ModelType: 'Nullable`1'
+
PropertyName: 'NullableLong'
+ +
+
+
Check View Data - LackModel view component
+
MetadataKind: 'Property'
+
ModelType: 'Nullable`1'
+
PropertyName: 'NullableLong'
+
Check View Data - LackModel view component after setting Model to 78.9
+
MetadataKind: 'Type'
+
ModelType: 'Double'
+ + +
Check View Data - LackModel view component's view
+
MetadataKind: 'Type'
+
ModelType: 'SuperTemplateModel'
+ +
+ +
+
+ + +
Template / partial for ... - LackModel
+
MetadataKind: 'Property'
+
ModelType: 'TemplateModel'
+
PropertyName: 'Template'
+ +
\ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ControllerTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ControllerTest.cs index 3967bbdf97..c4fc22eccf 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ControllerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ControllerTest.cs @@ -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 IncompatibleModelData + { + get + { + // Small grab bag of instances and expected types with no common base except typeof(object). + return new TheoryData + { + { null, typeof(object) }, + { true, typeof(bool) }, + { 43.78, typeof(double) }, + { "test string", typeof(string) }, + { new List(), typeof(List) }, + { new List(), typeof(List) }, + }; + } + } + + [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(); diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs index 8f742b2c9a..a2cc705b71 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs @@ -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(); @@ -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; diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/ViewComponentContextTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/ViewComponentContextTest.cs new file mode 100644 index 0000000000..8f8cbc7c3e --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/ViewComponentContextTest.cs @@ -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(), + 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 IncompatibleModelData + { + get + { + // Small "anything but int" grab bag of instances and expected types. + return new TheoryData + { + { null, typeof(object) }, + { true, typeof(bool) }, + { 43.78, typeof(double) }, + { "test string", typeof(string) }, + { new List(), typeof(List) }, + { new List(), typeof(List) }, + }; + } + } + + [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(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(), + 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); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/ViewDataDictionaryOfTModelTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/ViewDataDictionaryOfTModelTest.cs index 1ffe0fbf72..c656351cd5 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/ViewDataDictionaryOfTModelTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/ViewDataDictionaryOfTModelTest.cs @@ -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(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>(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>(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(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 IncompatibleModelData + { + get + { + // Small "anything but TestModel" grab bag of instances and expected types. + return new TheoryData + { + { true, typeof(bool) }, + { 23, typeof(int) }, + { 43.78, typeof(double) }, + { "test string", typeof(string) }, + { new List(), typeof(List) }, + { new List(), typeof(List) }, + { new List(), typeof(List) }, + }; + } + } + + [Theory] + [MemberData(nameof(IncompatibleModelData))] + public void CopyConstructorToObject_DoesNotThrow_IfModelIncompatibleWithDeclaredType( + object model, + Type expectedType) + { + // Arrange + var source = new ViewDataDictionary(new EmptyModelMetadataProvider()); + + // Act + var viewData = new ViewDataDictionary(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(metadataProvider) + { + Model = -48, + }; + + // Act + var viewData = new ViewDataDictionary(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(metadataProvider); + + // Act + var viewData = new ViewDataDictionary(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(metadataProvider) + { + Model = model, + }; + + // Act + var viewData = new ViewDataDictionary(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', " + diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/ViewDataDictionaryTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/ViewDataDictionaryTest.cs index c673f4e674..29119ffa75 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/ViewDataDictionaryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/ViewDataDictionaryTest.cs @@ -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(); + var metadataProvider = new Mock(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(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(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 SetModelData + public static TheoryData IncompatibleModelData + { + get + { + // Small "anything but TestModel" grab bag of instances and expected types. + return new TheoryData + { + { true, typeof(bool) }, + { 23, typeof(int) }, + { 43.78, typeof(double) }, + { "test string", typeof(string) }, + { new List(), typeof(List) }, + { new List(), typeof(List) }, + { new List(), typeof(List) }, + }; + } + } + + [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(() => 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 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>(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>(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>(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>(viewData.Data); - } - public static TheoryData 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), new object[] { "1", "2", "3", }, typeof(object[]))] - [InlineData(typeof(List), new object[] { 1, 2, 3, }, typeof(object[]))] - public void CopyConstructors_OverrideSourceMetadata_IfModelNonNull( + [InlineData(typeof(IEnumerable), new object[] { "1", "2", "3" }, typeof(object[]))] + [InlineData(typeof(List), 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), 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(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(() => 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) { } diff --git a/test/WebSites/HtmlGenerationWebSite/Components/CheckViewData - LackModel.cs b/test/WebSites/HtmlGenerationWebSite/Components/CheckViewData - LackModel.cs new file mode 100644 index 0000000000..b7e4668c5b --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Components/CheckViewData - LackModel.cs @@ -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("
Check View Data - LackModel view component
"); + writer.WriteLine($"
MetadataKind: '{ metadata.MetadataKind }'
"); + writer.WriteLine($"
ModelType: '{ metadata.ModelType.Name }'
"); + if (metadata.MetadataKind == ModelMetadataKind.Property) + { + writer.WriteLine($"
PropertyName: '{ metadata.PropertyName }'
"); + } + + // 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("
Check View Data - LackModel view component after setting Model to 78.9
"); + writer.WriteLine($"
MetadataKind: '{ metadata.MetadataKind }'
"); + writer.WriteLine($"
ModelType: '{ metadata.ModelType.Name }'
"); + if (metadata.MetadataKind == ModelMetadataKind.Property) + { + writer.WriteLine($"
PropertyName: '{ metadata.PropertyName }'
"); + } + + TemplateModel templateModel = new SuperTemplateModel(); + + return View(templateModel); + } + } +} diff --git a/test/WebSites/HtmlGenerationWebSite/Components/CheckViewData.cs b/test/WebSites/HtmlGenerationWebSite/Components/CheckViewData.cs new file mode 100644 index 0000000000..038d900db5 --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Components/CheckViewData.cs @@ -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("
Check View Data view component
"); + writer.WriteLine($"
MetadataKind: '{ metadata.MetadataKind }'
"); + writer.WriteLine($"
ModelType: '{ metadata.ModelType.Name }'
"); + if (metadata.MetadataKind == ModelMetadataKind.Property) + { + writer.WriteLine($"
PropertyName: '{ metadata.PropertyName }'
"); + } + + return View(); + } + } +} diff --git a/test/WebSites/HtmlGenerationWebSite/Controllers/CheckViewData.cs b/test/WebSites/HtmlGenerationWebSite/Controllers/CheckViewData.cs new file mode 100644 index 0000000000..ade765425d --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Controllers/CheckViewData.cs @@ -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()); + } + } +} diff --git a/test/WebSites/HtmlGenerationWebSite/Models/PartialModel.cs b/test/WebSites/HtmlGenerationWebSite/Models/PartialModel.cs new file mode 100644 index 0000000000..5cb34225cd --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Models/PartialModel.cs @@ -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 + { + } +} diff --git a/test/WebSites/HtmlGenerationWebSite/Models/SuperTemplateModel.cs b/test/WebSites/HtmlGenerationWebSite/Models/SuperTemplateModel.cs new file mode 100644 index 0000000000..e599bddc03 --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Models/SuperTemplateModel.cs @@ -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 + { + } +} diff --git a/test/WebSites/HtmlGenerationWebSite/Models/SuperViewModel.cs b/test/WebSites/HtmlGenerationWebSite/Models/SuperViewModel.cs new file mode 100644 index 0000000000..540cda73d5 --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Models/SuperViewModel.cs @@ -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 + { + } +} diff --git a/test/WebSites/HtmlGenerationWebSite/Models/TemplateModel.cs b/test/WebSites/HtmlGenerationWebSite/Models/TemplateModel.cs new file mode 100644 index 0000000000..f2e5f9f530 --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Models/TemplateModel.cs @@ -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 + { + } +} diff --git a/test/WebSites/HtmlGenerationWebSite/Models/ViewModel.cs b/test/WebSites/HtmlGenerationWebSite/Models/ViewModel.cs new file mode 100644 index 0000000000..564502b91d --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Models/ViewModel.cs @@ -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(); + } +} diff --git a/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/AtViewModel.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/AtViewModel.cshtml new file mode 100644 index 0000000000..3706c6cdd2 --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/AtViewModel.cshtml @@ -0,0 +1,37 @@ +@using HtmlGenerationWebSite.Components +@using HtmlGenerationWebSite.Models +@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata +@model ViewModel + +@{ + var metadata = ViewData.ModelMetadata; +} + +
+

At Model index

+
MetadataKind: '@metadata.MetadataKind'
+
ModelType: '@metadata.ModelType.Name'
+ @if (metadata.MetadataKind == ModelMetadataKind.Property) + { +
PropertyName: '@metadata.PropertyName'
+ } +
+ +
+ @Html.DisplayFor(m => m) +
+
+ @Html.Partial("PartialForViewModel") +
+
+ @(await Component.InvokeAsync()) +
+
+ @Html.DisplayFor(m => m.Integer) +
+
+ @Html.DisplayFor(m => m.NullableLong) +
+
+ @Html.DisplayFor(m => m.Template) +
\ No newline at end of file diff --git a/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/Components/CheckViewData/Default.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/Components/CheckViewData/Default.cshtml new file mode 100644 index 0000000000..44c2026cc1 --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/Components/CheckViewData/Default.cshtml @@ -0,0 +1,15 @@ +@using HtmlGenerationWebSite.Models +@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata +@model ViewModel + +@{ + var metadata = ViewData.ModelMetadata; +} + +
Check View Data view component's view
+
MetadataKind: '@metadata.MetadataKind'
+
ModelType: '@metadata.ModelType.Name'
+@if (metadata.MetadataKind == ModelMetadataKind.Property) +{ +
PropertyName: '@metadata.PropertyName'
+} \ No newline at end of file diff --git a/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/Components/CheckViewData___LackModel/Default.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/Components/CheckViewData___LackModel/Default.cshtml new file mode 100644 index 0000000000..31d992df85 --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/Components/CheckViewData___LackModel/Default.cshtml @@ -0,0 +1,14 @@ +@using HtmlGenerationWebSite.Models +@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata + +@{ + var metadata = ViewData.ModelMetadata; +} + +
Check View Data - LackModel view component's view
+
MetadataKind: '@metadata.MetadataKind'
+
ModelType: '@metadata.ModelType.Name'
+@if (metadata.MetadataKind == ModelMetadataKind.Property) +{ +
PropertyName: '@metadata.PropertyName'
+} \ No newline at end of file diff --git a/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/Int32 - LackModel.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/Int32 - LackModel.cshtml new file mode 100644 index 0000000000..ad948f6ca5 --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/Int32 - LackModel.cshtml @@ -0,0 +1,27 @@ +@using HtmlGenerationWebSite.Models +@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata + +@{ + var metadata = ViewData.ModelMetadata; +} + +
Template for Int32 - LackModel
+
MetadataKind: '@metadata.MetadataKind'
+
ModelType: '@metadata.ModelType.Name'
+@if (metadata.MetadataKind == ModelMetadataKind.Property) +{ +
PropertyName: '@metadata.PropertyName'
+} + +@{ + ViewData.Model = 78.9; + metadata = ViewData.ModelMetadata; +} + +
Template for Int32 - LackModel after setting Model to 78.9
+
MetadataKind: '@metadata.MetadataKind'
+
ModelType: '@metadata.ModelType.Name'
+@if (metadata.MetadataKind == ModelMetadataKind.Property) +{ +
PropertyName: '@metadata.PropertyName'
+} \ No newline at end of file diff --git a/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/Int32.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/Int32.cshtml new file mode 100644 index 0000000000..1ffc68323b --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/Int32.cshtml @@ -0,0 +1,15 @@ +@using HtmlGenerationWebSite.Models +@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata +@model int? + +@{ + var metadata = ViewData.ModelMetadata; +} + +
Template for Int32
+
MetadataKind: '@metadata.MetadataKind'
+
ModelType: '@metadata.ModelType.Name'
+@if (metadata.MetadataKind == ModelMetadataKind.Property) +{ +
PropertyName: '@metadata.PropertyName'
+} \ No newline at end of file diff --git a/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/Int64 - LackModel.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/Int64 - LackModel.cshtml new file mode 100644 index 0000000000..92c0267af8 --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/Int64 - LackModel.cshtml @@ -0,0 +1,22 @@ +@using HtmlGenerationWebSite.Components +@using HtmlGenerationWebSite.Models +@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata + +@{ + var metadata = ViewData.ModelMetadata; +} + +
Template for Int64
+
MetadataKind: '@metadata.MetadataKind'
+
ModelType: '@metadata.ModelType.Name'
+@if (metadata.MetadataKind == ModelMetadataKind.Property) +{ +
PropertyName: '@metadata.PropertyName'
+} + +
+ @Html.Partial(partialViewName: "LackModel.cshtml") +
+
+ @(await Component.InvokeAsync()) +
diff --git a/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/Int64.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/Int64.cshtml new file mode 100644 index 0000000000..f436e067af --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/Int64.cshtml @@ -0,0 +1,15 @@ +@using HtmlGenerationWebSite.Models +@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata +@model long? + +@{ + var metadata = ViewData.ModelMetadata; +} + +
Template for Int64
+
MetadataKind: '@metadata.MetadataKind'
+
ModelType: '@metadata.ModelType.Name'
+@if (metadata.MetadataKind == ModelMetadataKind.Property) +{ +
PropertyName: '@metadata.PropertyName'
+} \ No newline at end of file diff --git a/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/LackModel.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/LackModel.cshtml new file mode 100644 index 0000000000..4f15d2b22f --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/LackModel.cshtml @@ -0,0 +1,14 @@ +@using HtmlGenerationWebSite.Models +@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata + +@{ + var metadata = ViewData.ModelMetadata; +} + +
Template / partial for ... - LackModel
+
MetadataKind: '@metadata.MetadataKind'
+
ModelType: '@metadata.ModelType.Name'
+@if (metadata.MetadataKind == ModelMetadataKind.Property) +{ +
PropertyName: '@metadata.PropertyName'
+} \ No newline at end of file diff --git a/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/TemplateModel.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/TemplateModel.cshtml new file mode 100644 index 0000000000..12ea51887c --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/TemplateModel.cshtml @@ -0,0 +1,15 @@ +@using HtmlGenerationWebSite.Models +@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata +@model TemplateModel + +@{ + var metadata = ViewData.ModelMetadata; +} + +
Template for TemplateModel
+
MetadataKind: '@metadata.MetadataKind'
+
ModelType: '@metadata.ModelType.Name'
+@if (metadata.MetadataKind == ModelMetadataKind.Property) +{ +
PropertyName: '@metadata.PropertyName'
+} \ No newline at end of file diff --git a/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/ViewModel.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/ViewModel.cshtml new file mode 100644 index 0000000000..01bac15c3b --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/DisplayTemplates/ViewModel.cshtml @@ -0,0 +1,15 @@ +@using HtmlGenerationWebSite.Models +@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata +@model ViewModel + +@{ + var metadata = ViewData.ModelMetadata; +} + +
Template for ViewModel
+
MetadataKind: '@metadata.MetadataKind'
+
ModelType: '@metadata.ModelType.Name'
+@if (metadata.MetadataKind == ModelMetadataKind.Property) +{ +
PropertyName: '@metadata.PropertyName'
+} \ No newline at end of file diff --git a/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/PartialForViewModel.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/PartialForViewModel.cshtml new file mode 100644 index 0000000000..5e19e180f6 --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/PartialForViewModel.cshtml @@ -0,0 +1,15 @@ +@using HtmlGenerationWebSite.Models +@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata +@model ViewModel + +@{ + var metadata = ViewData.ModelMetadata; +} + +
Partial for ViewModel
+
MetadataKind: '@metadata.MetadataKind'
+
ModelType: '@metadata.ModelType.Name'
+@if (metadata.MetadataKind == ModelMetadataKind.Property) +{ +
PropertyName: '@metadata.PropertyName'
+} \ No newline at end of file diff --git a/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/ViewModel.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/ViewModel.cshtml new file mode 100644 index 0000000000..bf3f5e64d9 --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/CheckViewData/ViewModel.cshtml @@ -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) overloads that ViewComponent has. *@ +@model ViewModel + +@{ + var metadata = ViewData.ModelMetadata; +} + +
+

View Model index

+
MetadataKind: '@metadata.MetadataKind'
+
ModelType: '@metadata.ModelType.Name'
+ @if (metadata.MetadataKind == ModelMetadataKind.Property) + { +
PropertyName: '@metadata.PropertyName'
+ } +
+ +
+ @Html.DisplayFor(m => m, templateName: "LackModel") +
+
+ @Html.Partial(partialViewName: "DisplayTemplates/LackModel.cshtml") +
+
+ @(await Component.InvokeAsync()) +
+
+ @Html.DisplayFor(m => m.Integer, templateName: "Int32 - LackModel") +
+
+ @Html.DisplayFor(m => m.NullableLong, templateName: "Int64 - LackModel") +
+
+ @Html.DisplayFor(m => m.Template, templateName: "LackModel") +
\ No newline at end of file