From 9d5364cf9b5e1bd4961946ec26379500a4f7720f Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 18 Feb 2015 08:45:13 -0800 Subject: [PATCH] Removing ModelMetadata.Model --- .../Shared/DisplayTemplates/Decimal.cshtml | 4 +- .../Shared/EditorTemplates/Decimal.cshtml | 4 +- src/Microsoft.AspNet.Mvc.Core/Controller.cs | 7 +- .../DefaultControllerActionArgumentBinder.cs | 13 +- .../DefaultApiDescriptionProvider.cs | 5 +- .../ParameterBinding/ModelBindingHelper.cs | 10 +- .../Expressions/ExpressionMetadataProvider.cs | 138 +++--- .../Rendering/Html/DefaultDisplayTemplates.cs | 40 +- .../Rendering/Html/DefaultEditorTemplates.cs | 44 +- .../Rendering/Html/DefaultHtmlGenerator.cs | 85 ++-- .../Rendering/Html/HtmlHelper.cs | 110 ++--- .../Rendering/Html/HtmlHelperOfT.cs | 52 +-- .../Rendering/Html/IHtmlGenerator.cs | 38 +- .../Rendering/Html/TemplateBuilder.cs | 38 +- .../Rendering/Html/TemplateRenderer.cs | 5 +- .../Rendering/IHtmlHelper.cs | 2 +- .../Rendering/ModelExpression.cs | 40 +- src/Microsoft.AspNet.Mvc.Core/TemplateInfo.cs | 4 +- .../ViewDataDictionary.cs | 55 ++- .../Binders/CollectionModelBinder.cs | 14 +- .../Binders/ComplexModelDtoModelBinder.cs | 2 +- .../Binders/CompositeModelBinder.cs | 3 +- .../Binders/KeyValuePairModelBinder.cs | 26 +- .../Binders/MutableObjectModelBinder.cs | 95 +++-- .../IObjectModelValidator.cs | 2 +- .../Internal/ModelBindingHelper.cs | 6 +- .../Metadata/AssociatedMetadataProvider.cs | 45 +- .../CachedDataAnnotationsModelMetadata.cs | 20 +- .../Metadata/CachedModelMetadata.cs | 30 +- .../DataAnnotationsModelMetadataProvider.cs | 5 +- .../Metadata/EmptyModelMetadataProvider.cs | 5 +- .../Metadata/IModelMetadataProvider.cs | 14 +- .../Metadata/ModelExplorer.cs | 392 ++++++++++++++++++ .../Metadata/ModelExplorerExtensions.cs | 64 +++ .../Metadata/ModelMetadata.cs | 118 +----- .../ModelMetadataProviderExtensions.cs | 56 +++ .../ModelBindingContext.cs | 9 + .../Validation/CompareAttributeAdapter.cs | 7 +- .../DataAnnotationsModelValidator.cs | 13 +- .../Validation/DefaultObjectValidator.cs | 72 ++-- .../Validation/ModelValidationContext.cs | 32 +- .../Validation/ValidatableObjectAdapter.cs | 2 +- .../RazorPageOfT.cs | 6 +- .../InputTagHelper.cs | 61 +-- .../LabelTagHelper.cs | 2 +- .../SelectTagHelper.cs | 4 +- .../TextAreaTagHelper.cs | 2 +- .../ApiController.cs | 5 +- .../BodyModelBinderTests.cs | 2 +- .../Formatters/JsonInputFormatterTest.cs | 14 +- .../ControllerActionArgumentBinderTests.cs | 25 +- .../ModelBindingHelperTest.cs | 35 +- .../RemoteAttributeTest.cs | 5 +- .../Rendering/DefaultDisplayTemplatesTest.cs | 21 +- .../Rendering/DefaultEditorTemplatesTest.cs | 34 +- .../Rendering/DefaultTemplatesUtilities.cs | 35 +- .../ExpressionMetadataProviderTest.cs | 4 +- .../HtmlHelperDisplayNameExtensionsTest.cs | 162 ++------ .../Rendering/HtmlHelperDisplayTextTest.cs | 22 +- .../HtmlHelperLabelExtensionsTest.cs | 124 ++---- .../Rendering/HtmlHelperNameExtensionsTest.cs | 30 +- .../TestModelMetadataProvider.cs | 117 ++++++ .../ViewDataDictionaryOfTModelTest.cs | 6 +- .../ViewDataDictionaryTest.cs | 107 +++-- .../ModelBindingTest.cs | 2 +- .../Binders/ArrayModelBinderTest.cs | 2 +- ...nderTypeBasedModelBinderModelBinderTest.cs | 2 +- .../Binders/BindingSourceModelBinderTest.cs | 12 +- .../Binders/ByteArrayModelBinderTests.cs | 2 +- .../CancellationTokenModelBinderTests.cs | 2 +- .../Binders/CollectionModelBinderTest.cs | 12 +- .../Binders/ComplexModelDtoTest.cs | 2 +- .../Binders/CompositeModelBinderTest.cs | 15 +- .../Binders/DictionaryModelBinderTest.cs | 2 +- .../Binders/FormCollectionModelBinderTest.cs | 2 +- .../Binders/FormFileModelBinderTest.cs | 2 +- .../Binders/HeaderModelBinderTests.cs | 2 +- .../Binders/KeyValuePairModelBinderTest.cs | 2 +- .../Binders/ModelBindingContextTest.cs | 8 +- .../Binders/MutableObjectModelBinderTest.cs | 276 ++++++++---- .../Binders/TypeConverterModelBinderTest.cs | 2 +- .../AssociatedMetadataProviderTest.cs | 119 ++---- ...ataAnnotationsModelMetadataProviderTest.cs | 33 +- .../CachedDataAnnotationsModelMetadataTest.cs | 34 +- .../Metadata/ModelExplorerExtensionsTest.cs | 86 ++++ .../Metadata/ModelExplorerTest.cs | 150 +++++++ .../ModelMetadataProviderExtensionsTest.cs | 24 ++ .../Metadata/ModelMetadataTest.cs | 132 +----- .../AssociatedValidatorProviderTest.cs | 6 +- .../Validation/CompareAttributeAdapterTest.cs | 13 +- .../CompositeModelValidatorProviderTest.cs | 4 +- ...taAnnotationsModelValidatorProviderTest.cs | 23 +- .../DataAnnotationsModelValidatorTest.cs | 74 ++-- .../DataMemberModelValidatorProviderTest.cs | 8 +- .../Validation/DefaultObjectValidatorTests.cs | 47 ++- .../MaxLengthAttributeAdapterTest.cs | 4 +- .../MinLengthAttributeAdapterTest.cs | 4 +- .../Validation/RangeAttributeAdapterTest.cs | 2 +- .../RequiredAttributeAdapterTest.cs | 2 +- .../StringLengthAttributeAdapterTest.cs | 4 +- .../InputTagHelperTest.cs | 52 ++- .../LabelTagHelperTest.cs | 20 +- .../SelectTagHelperTest.cs | 27 +- .../TestableHtmlGenerator.cs | 2 +- .../TextAreaTagHelperTest.cs | 45 +- .../ValidationMessageTagHelperTest.cs | 15 +- ...ataContractSerializerInputFormatterTest.cs | 2 +- .../XmlSerializerInputFormatterTest.cs | 2 +- .../TagHelpers/RepeatContentTagHelper.cs | 2 +- .../Controllers/ModelMetadataController.cs | 8 +- 110 files changed, 2313 insertions(+), 1527 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelExplorer.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelExplorerExtensions.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadataProviderExtensions.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/TestModelMetadataProvider.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelExplorerExtensionsTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelExplorerTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataProviderExtensionsTest.cs diff --git a/samples/MvcSample.Web/Views/Shared/DisplayTemplates/Decimal.cshtml b/samples/MvcSample.Web/Views/Shared/DisplayTemplates/Decimal.cshtml index 8b2d217cef..e022aa5776 100644 --- a/samples/MvcSample.Web/Views/Shared/DisplayTemplates/Decimal.cshtml +++ b/samples/MvcSample.Web/Views/Shared/DisplayTemplates/Decimal.cshtml @@ -4,8 +4,8 @@ @functions { private object FormattedValue { get { - if (ViewData.TemplateInfo.FormattedModelValue == ViewData.ModelMetadata.Model) { - return String.Format(CultureInfo.CurrentCulture, "{0:0.00}", ViewData.ModelMetadata.Model); + if (ViewData.TemplateInfo.FormattedModelValue == ViewData.ModelExplorer.Model) { + return String.Format(CultureInfo.CurrentCulture, "{0:0.00}", ViewData.ModelExplorer.Model); } return ViewData.TemplateInfo.FormattedModelValue; } diff --git a/samples/MvcSample.Web/Views/Shared/EditorTemplates/Decimal.cshtml b/samples/MvcSample.Web/Views/Shared/EditorTemplates/Decimal.cshtml index bd5dbd5b24..a7d00a3938 100644 --- a/samples/MvcSample.Web/Views/Shared/EditorTemplates/Decimal.cshtml +++ b/samples/MvcSample.Web/Views/Shared/EditorTemplates/Decimal.cshtml @@ -4,8 +4,8 @@ @functions { private object FormattedValue { get { - if (ViewData.TemplateInfo.FormattedModelValue == ViewData.ModelMetadata.Model) { - return string.Format(CultureInfo.CurrentCulture, "{0:0.00}", ViewData.ModelMetadata.Model); + if (ViewData.TemplateInfo.FormattedModelValue == ViewData.ModelExplorer.Model) { + return string.Format(CultureInfo.CurrentCulture, "{0:0.00}", ViewData.ModelExplorer.Model); } return ViewData.TemplateInfo.FormattedModelValue; } diff --git a/src/Microsoft.AspNet.Mvc.Core/Controller.cs b/src/Microsoft.AspNet.Mvc.Core/Controller.cs index 5fe319e4cb..60cd3021ba 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Controller.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Controller.cs @@ -1209,17 +1209,14 @@ namespace Microsoft.AspNet.Mvc throw new InvalidOperationException(message); } - var modelMetadata = MetadataProvider.GetMetadataForType( - modelAccessor: () => model, - modelType: model.GetType()); + var modelExplorer = MetadataProvider.GetModelExplorerForType(model.GetType(), model); var modelName = prefix ?? string.Empty; var validationContext = new ModelValidationContext( modelName, BindingContext.ValidatorProvider, ModelState, - modelMetadata, - containerMetadata: null); + modelExplorer); ObjectValidator.Validate(validationContext); return ModelState.IsValid; diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs index 59661da018..249f59a606 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs @@ -47,7 +47,6 @@ namespace Microsoft.AspNet.Mvc foreach (var parameter in actionDescriptor.Parameters) { var metadata = _modelMetadataProvider.GetMetadataForParameter( - modelAccessor: null, methodInfo: actionDescriptor.MethodInfo, parameterName: parameter.Name); @@ -97,17 +96,19 @@ namespace Microsoft.AspNet.Mvc foreach (var parameter in parameterMetadata) { var parameterType = parameter.ModelType; + var modelBindingContext = GetModelBindingContext(parameter, modelState, operationBindingContext); var modelBindingResult = await bindingContext.ModelBinder.BindModelAsync(modelBindingContext); if (modelBindingResult != null && modelBindingResult.IsModelSet) { + var modelExplorer = new ModelExplorer(_modelMetadataProvider, parameter, modelBindingResult.Model); + arguments[parameter.PropertyName] = modelBindingResult.Model; var validationContext = new ModelValidationContext( - modelBindingResult.Key, - bindingContext.ValidatorProvider, - actionContext.ModelState, - parameter, - containerMetadata: null); + modelBindingResult.Key, + bindingContext.ValidatorProvider, + actionContext.ModelState, + modelExplorer); _validator.Validate(validationContext); } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs index 2fd75bea82..5af756c4ea 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs @@ -111,9 +111,7 @@ namespace Microsoft.AspNet.Mvc.Description { apiDescription.ResponseType = runtimeReturnType; - apiDescription.ResponseModelMetadata = _modelMetadataProvider.GetMetadataForType( - modelAccessor: null, - modelType: runtimeReturnType); + apiDescription.ResponseModelMetadata = _modelMetadataProvider.GetMetadataForType(runtimeReturnType); var formats = GetResponseFormats( action, @@ -436,7 +434,6 @@ namespace Microsoft.AspNet.Mvc.Description public void WalkParameter() { var modelMetadata = Context.MetadataProvider.GetMetadataForParameter( - modelAccessor: null, methodInfo: Context.ActionDescriptor.MethodInfo, parameterName: Parameter.Name); diff --git a/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs b/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs index c7b7a92caf..b4954a3fe4 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs @@ -252,9 +252,7 @@ namespace Microsoft.AspNet.Mvc throw new ArgumentException(message, nameof(modelType)); } - var modelMetadata = metadataProvider.GetMetadataForType( - modelAccessor: () => model, - modelType: modelType); + var modelMetadata = metadataProvider.GetMetadataForType(modelType); var operationBindingContext = new OperationBindingContext { @@ -266,6 +264,7 @@ namespace Microsoft.AspNet.Mvc var modelBindingContext = new ModelBindingContext { + Model = model, ModelMetadata = modelMetadata, ModelName = prefix, ModelState = modelState, @@ -278,7 +277,8 @@ namespace Microsoft.AspNet.Mvc var modelBindingResult = await modelBinder.BindModelAsync(modelBindingContext); if (modelBindingResult != null) { - var modelValidationContext = new ModelValidationContext(modelBindingContext, modelMetadata); + var modelExplorer = new ModelExplorer(metadataProvider, modelMetadata, modelBindingResult.Model); + var modelValidationContext = new ModelValidationContext(modelBindingContext, modelExplorer); modelValidationContext.RootPrefix = prefix; objectModelValidator.Validate(modelValidationContext); return modelState.IsValid; @@ -382,4 +382,4 @@ namespace Microsoft.AspNet.Mvc } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Expressions/ExpressionMetadataProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Expressions/ExpressionMetadataProvider.cs index 067f2e2ddc..a728c5dace 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Expressions/ExpressionMetadataProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Expressions/ExpressionMetadataProvider.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.Globalization; using System.Linq.Expressions; using System.Reflection; using Microsoft.AspNet.Mvc.Core; @@ -12,7 +13,7 @@ namespace Microsoft.AspNet.Mvc.Rendering.Expressions { public static class ExpressionMetadataProvider { - public static ModelMetadata FromLambdaExpression( + public static ModelExplorer FromLambdaExpression( [NotNull] Expression> expression, [NotNull] ViewDataDictionary viewData, IModelMetadataProvider metadataProvider) @@ -55,12 +56,11 @@ namespace Microsoft.AspNet.Mvc.Rendering.Expressions throw new InvalidOperationException(Resources.TemplateHelpers_TemplateLimitations); } - var container = viewData.Model; - Func modelAccessor = () => + Func modelAccessor = (container) => { try { - return CachedExpressionCompiler.Process(expression)(container); + return CachedExpressionCompiler.Process(expression)((TModel)container); } catch (NullReferenceException) { @@ -68,18 +68,31 @@ namespace Microsoft.AspNet.Mvc.Rendering.Expressions } }; - return GetMetadataFromProvider( - modelAccessor, - typeof(TResult), - propertyName, - container, - containerType, - metadataProvider); + ModelMetadata metadata; + if (propertyName == null) + { + // Ex: + // m => 5 (arbitrary expression) + // m => foo (arbitrary expression) + // m => m.Widgets[0] (expression ending with non-property-access) + metadata = metadataProvider.GetMetadataForType(typeof(TResult)); + } + else + { + // Ex: + // m => m.Color (simple property access) + // m => m.Color.Red (nested property access) + // m => m.Widgets[0].Size (expression ending with property-access) + metadata = metadataProvider.GetMetadataForType(containerType).Properties[propertyName]; + } + + return viewData.ModelExplorer.GetExplorerForExpression(metadata, modelAccessor); } - public static ModelMetadata FromStringExpression(string expression, - [NotNull] ViewDataDictionary viewData, - IModelMetadataProvider metadataProvider) + public static ModelExplorer FromStringExpression( + string expression, + [NotNull] ViewDataDictionary viewData, + IModelMetadataProvider metadataProvider) { if (string.IsNullOrEmpty(expression)) { @@ -88,93 +101,64 @@ namespace Microsoft.AspNet.Mvc.Rendering.Expressions } var viewDataInfo = ViewDataEvaluator.Eval(viewData, expression); - Type containerType = null; - Type modelType = null; - Func modelAccessor = null; - string propertyName = null; - object container = null; + + if (viewDataInfo == null) + { + // Try getting a property from ModelMetadata if we couldn't find an answer in ViewData + var propertyExplorer = viewData.ModelExplorer.GetExplorerForProperty(expression); + if (propertyExplorer != null) + { + return propertyExplorer; + } + } if (viewDataInfo != null) { + ModelExplorer containerExplorer = viewData.ModelExplorer; if (viewDataInfo.Container != null) { - containerType = viewDataInfo.Container.GetType(); - container = viewDataInfo.Container; + containerExplorer = metadataProvider.GetModelExplorerForType( + viewDataInfo.Container.GetType(), + viewDataInfo.Container); } - modelAccessor = () => viewDataInfo.Value; - if (viewDataInfo.PropertyInfo != null) { - propertyName = viewDataInfo.PropertyInfo.Name; - modelType = viewDataInfo.PropertyInfo.PropertyType; + // We've identified a property access, which provides us with accurate metadata. + var containerType = viewDataInfo.Container?.GetType() ?? viewDataInfo.PropertyInfo.DeclaringType; + var containerMetadata = metadataProvider.GetMetadataForType(viewDataInfo.Container.GetType()); + var propertyMetadata = containerMetadata.Properties[viewDataInfo.PropertyInfo.Name]; + + Func modelAccessor = (ignore) => viewDataInfo.Value; + return containerExplorer.GetExplorerForExpression(propertyMetadata, modelAccessor); } else if (viewDataInfo.Value != null) { - // We only need to delay accessing properties (for LINQ to SQL) - modelType = viewDataInfo.Value.GetType(); - } - } - else - { - // Try getting a property from ModelMetadata if we couldn't find an answer in ViewData - var propertyMetadata = viewData.ModelMetadata.Properties[expression]; - if (propertyMetadata != null) - { - return propertyMetadata; + // We have a value, even though we may not know where it came from. + var valueMetadata = metadataProvider.GetMetadataForType(viewDataInfo.Value.GetType()); + return containerExplorer.GetExplorerForExpression(valueMetadata, viewDataInfo.Value); } } - return GetMetadataFromProvider(modelAccessor, - modelType ?? typeof(string), - propertyName, - container, - containerType, - metadataProvider); + // Treat the expression as string if we don't find anything better. + var stringMetadata = metadataProvider.GetMetadataForType(typeof(string)); + return viewData.ModelExplorer.GetExplorerForExpression(stringMetadata, modelAccessor: null); } - private static ModelMetadata FromModel([NotNull] ViewDataDictionary viewData, - IModelMetadataProvider metadataProvider) + private static ModelExplorer FromModel( + [NotNull] ViewDataDictionary viewData, + IModelMetadataProvider metadataProvider) { if (viewData.ModelMetadata.ModelType == typeof(object)) { // Use common simple type rather than object so e.g. Editor() at least generates a TextBox. - return GetMetadataFromProvider( - modelAccessor: null, - modelType: typeof(string), - propertyName: null, - container: null, - containerType: null, - metadataProvider: metadataProvider); + var model = viewData.Model == null ? null : Convert.ToString(viewData.Model, CultureInfo.CurrentCulture); + return metadataProvider.GetModelExplorerForType(typeof(string), model); } else { - return viewData.ModelMetadata; + return viewData.ModelExplorer; } } - - // An IModelMetadataProvider is not required unless this method is called. Therefore other methods in this - // class lack [NotNull] attributes for their corresponding parameter. - private static ModelMetadata GetMetadataFromProvider(Func modelAccessor, - Type modelType, - string propertyName, - object container, - Type containerType, - [NotNull] IModelMetadataProvider metadataProvider) - { - if (containerType != null && !string.IsNullOrEmpty(propertyName)) - { - var metadata = - metadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName); - if (metadata != null) - { - metadata.Container = container; - } - - return metadata; - } - - return metadataProvider.GetMetadataForType(modelAccessor, modelType); - } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultDisplayTemplates.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultDisplayTemplates.cs index 8949f76934..626b4782a2 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultDisplayTemplates.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultDisplayTemplates.cs @@ -91,7 +91,7 @@ namespace Microsoft.AspNet.Mvc.Rendering public static string CollectionTemplate(IHtmlHelper htmlHelper) { - var model = htmlHelper.ViewData.ModelMetadata.Model; + var model = htmlHelper.ViewData.Model; if (model == null) { return string.Empty; @@ -136,14 +136,14 @@ namespace Microsoft.AspNet.Mvc.Rendering itemType = item.GetType(); } - var metadata = metadataProvider.GetMetadataForType(() => item, itemType); + var modelExplorer = metadataProvider.GetModelExplorerForType(itemType, item); var fieldName = string.Format(CultureInfo.InvariantCulture, "{0}[{1}]", fieldNameBase, index++); var templateBuilder = new TemplateBuilder( viewEngine, htmlHelper.ViewContext, htmlHelper.ViewData, - metadata, + modelExplorer, htmlFieldName: fieldName, templateName: null, readOnly: true, @@ -163,10 +163,10 @@ namespace Microsoft.AspNet.Mvc.Rendering public static string DecimalTemplate(IHtmlHelper htmlHelper) { - if (htmlHelper.ViewData.TemplateInfo.FormattedModelValue == htmlHelper.ViewData.ModelMetadata.Model) + if (htmlHelper.ViewData.TemplateInfo.FormattedModelValue == htmlHelper.ViewData.Model) { htmlHelper.ViewData.TemplateInfo.FormattedModelValue = - string.Format(CultureInfo.CurrentCulture, "{0:0.00}", htmlHelper.ViewData.ModelMetadata.Model); + string.Format(CultureInfo.CurrentCulture, "{0:0.00}", htmlHelper.ViewData.Model); } return StringTemplate(htmlHelper); @@ -203,18 +203,18 @@ namespace Microsoft.AspNet.Mvc.Rendering { var viewData = htmlHelper.ViewData; var templateInfo = viewData.TemplateInfo; - var modelMetadata = viewData.ModelMetadata; + var modelExplorer = viewData.ModelExplorer; var builder = new StringBuilder(); - if (modelMetadata.Model == null) + if (modelExplorer.Model == null) { - return modelMetadata.NullDisplayText; + return modelExplorer.Metadata.NullDisplayText; } if (templateInfo.TemplateDepth > 1) { - var text = modelMetadata.SimpleDisplayText; - if (modelMetadata.HtmlEncode) + var text = modelExplorer.GetSimpleDisplayText(); + if (modelExplorer.Metadata.HtmlEncode) { text = htmlHelper.Encode(text); } @@ -224,9 +224,15 @@ namespace Microsoft.AspNet.Mvc.Rendering var serviceProvider = htmlHelper.ViewContext.HttpContext.RequestServices; var viewEngine = serviceProvider.GetRequiredService(); - var properties = modelMetadata.Properties.Where(metadata => ShouldShow(metadata, templateInfo)); - foreach (var propertyMetadata in properties) + + foreach (var propertyExplorer in modelExplorer.Properties) { + var propertyMetadata = propertyExplorer.Metadata; + if (!ShouldShow(propertyExplorer, templateInfo)) + { + continue; + } + var divTag = new TagBuilder("div"); if (!propertyMetadata.HideSurroundingHtml) @@ -250,7 +256,7 @@ namespace Microsoft.AspNet.Mvc.Rendering viewEngine, htmlHelper.ViewContext, htmlHelper.ViewData, - propertyMetadata, + propertyExplorer, htmlFieldName: propertyMetadata.PropertyName, templateName: null, readOnly: true, @@ -267,12 +273,12 @@ namespace Microsoft.AspNet.Mvc.Rendering return builder.ToString(); } - private static bool ShouldShow(ModelMetadata metadata, TemplateInfo templateInfo) + private static bool ShouldShow(ModelExplorer modelExplorer, TemplateInfo templateInfo) { return - metadata.ShowForDisplay && - !metadata.IsComplexType && - !templateInfo.Visited(metadata); + modelExplorer.Metadata.ShowForDisplay && + !modelExplorer.Metadata.IsComplexType && + !templateInfo.Visited(modelExplorer); } public static string StringTemplate(IHtmlHelper htmlHelper) diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultEditorTemplates.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultEditorTemplates.cs index c5539ff742..9cbadc2067 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultEditorTemplates.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultEditorTemplates.cs @@ -52,7 +52,7 @@ namespace Microsoft.AspNet.Mvc.Rendering public static string CollectionTemplate(IHtmlHelper htmlHelper) { var viewData = htmlHelper.ViewData; - var model = viewData.ModelMetadata.Model; + var model = viewData.Model; if (model == null) { return string.Empty; @@ -96,14 +96,14 @@ namespace Microsoft.AspNet.Mvc.Rendering itemType = item.GetType(); } - var metadata = metadataProvider.GetMetadataForType(() => item, itemType); + var modelExplorer = metadataProvider.GetModelExplorerForType(itemType, item); var fieldName = string.Format(CultureInfo.InvariantCulture, "{0}[{1}]", fieldNameBase, index++); var templateBuilder = new TemplateBuilder( viewEngine, htmlHelper.ViewContext, htmlHelper.ViewData, - metadata, + modelExplorer, htmlFieldName: fieldName, templateName: null, readOnly: false, @@ -123,10 +123,10 @@ namespace Microsoft.AspNet.Mvc.Rendering public static string DecimalTemplate(IHtmlHelper htmlHelper) { - if (htmlHelper.ViewData.TemplateInfo.FormattedModelValue == htmlHelper.ViewData.ModelMetadata.Model) + if (htmlHelper.ViewData.TemplateInfo.FormattedModelValue == htmlHelper.ViewData.Model) { htmlHelper.ViewData.TemplateInfo.FormattedModelValue = - string.Format(CultureInfo.CurrentCulture, "{0:0.00}", htmlHelper.ViewData.ModelMetadata.Model); + string.Format(CultureInfo.CurrentCulture, "{0:0.00}", htmlHelper.ViewData.Model); } return StringTemplate(htmlHelper); @@ -227,18 +227,18 @@ namespace Microsoft.AspNet.Mvc.Rendering { var viewData = htmlHelper.ViewData; var templateInfo = viewData.TemplateInfo; - var modelMetadata = viewData.ModelMetadata; + var modelExplorer = viewData.ModelExplorer; var builder = new StringBuilder(); if (templateInfo.TemplateDepth > 1) { - if (modelMetadata.Model == null) + if (modelExplorer.Model == null) { - return modelMetadata.NullDisplayText; + return modelExplorer.Metadata.NullDisplayText; } - var text = modelMetadata.SimpleDisplayText; - if (modelMetadata.HtmlEncode) + var text = modelExplorer.GetSimpleDisplayText(); + if (modelExplorer.Metadata.HtmlEncode) { text = htmlHelper.Encode(text); } @@ -248,9 +248,15 @@ namespace Microsoft.AspNet.Mvc.Rendering var serviceProvider = htmlHelper.ViewContext.HttpContext.RequestServices; var viewEngine = serviceProvider.GetRequiredService(); - var properties = modelMetadata.Properties.Where(metadata => ShouldShow(metadata, templateInfo)); - foreach (var propertyMetadata in properties) + + foreach (var propertyExplorer in modelExplorer.Properties) { + var propertyMetadata = propertyExplorer.Metadata; + if (!ShouldShow(propertyExplorer, templateInfo)) + { + continue; + } + var divTag = new TagBuilder("div"); if (!propertyMetadata.HideSurroundingHtml) @@ -278,7 +284,7 @@ namespace Microsoft.AspNet.Mvc.Rendering viewEngine, htmlHelper.ViewContext, htmlHelper.ViewData, - propertyMetadata, + propertyExplorer, htmlFieldName: propertyMetadata.PropertyName, templateName: null, readOnly: false, @@ -311,12 +317,12 @@ namespace Microsoft.AspNet.Mvc.Rendering .ToString(); } - private static bool ShouldShow(ModelMetadata metadata, TemplateInfo templateInfo) + private static bool ShouldShow(ModelExplorer modelExplorer, TemplateInfo templateInfo) { return - metadata.ShowForEdit && - !metadata.IsComplexType && - !templateInfo.Visited(metadata); + modelExplorer.Metadata.ShowForEdit && + !modelExplorer.Metadata.IsComplexType && + !templateInfo.Visited(modelExplorer); } public static string StringTemplate(IHtmlHelper htmlHelper) @@ -376,7 +382,7 @@ namespace Microsoft.AspNet.Mvc.Rendering } var metadata = htmlHelper.ViewData.ModelMetadata; - var value = metadata.Model; + var value = htmlHelper.ViewData.Model; if (htmlHelper.ViewData.TemplateInfo.FormattedModelValue != value && metadata.HasNonDefaultEditFormat) { return; @@ -407,4 +413,4 @@ namespace Microsoft.AspNet.Mvc.Rendering .ToString(); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultHtmlGenerator.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultHtmlGenerator.cs index 46731ff56f..7ff872d02a 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultHtmlGenerator.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultHtmlGenerator.cs @@ -90,20 +90,20 @@ namespace Microsoft.AspNet.Mvc.Rendering /// public virtual TagBuilder GenerateCheckBox( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, bool? isChecked, object htmlAttributes) { - if (metadata != null) + if (modelExplorer != null) { // CheckBoxFor() case. That API does not support passing isChecked directly. Debug.Assert(!isChecked.HasValue); - if (metadata.Model != null) + if (modelExplorer.Model != null) { bool modelChecked; - if (Boolean.TryParse(metadata.Model.ToString(), out modelChecked)) + if (Boolean.TryParse(modelExplorer.Model.ToString(), out modelChecked)) { isChecked = modelChecked; } @@ -121,10 +121,10 @@ namespace Microsoft.AspNet.Mvc.Rendering return GenerateInput( viewContext, InputType.CheckBox, - metadata, + modelExplorer, expression, value: "true", - useViewData: (metadata == null && !isChecked.HasValue), + useViewData: (modelExplorer == null && !isChecked.HasValue), isChecked: isChecked ?? false, setId: true, isExplicitValue: false, @@ -135,7 +135,7 @@ namespace Microsoft.AspNet.Mvc.Rendering /// public virtual TagBuilder GenerateHiddenForCheckbox( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression) { var tagBuilder = new TagBuilder("input"); @@ -202,7 +202,7 @@ namespace Microsoft.AspNet.Mvc.Rendering /// public virtual TagBuilder GenerateHidden( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, object value, bool useViewData, @@ -219,7 +219,7 @@ namespace Microsoft.AspNet.Mvc.Rendering return GenerateInput( viewContext, InputType.Hidden, - metadata, + modelExplorer, expression, value, useViewData, @@ -233,12 +233,14 @@ namespace Microsoft.AspNet.Mvc.Rendering /// public virtual TagBuilder GenerateLabel( [NotNull] ViewContext viewContext, - [NotNull] ModelMetadata metadata, + [NotNull] ModelExplorer modelExplorer, string expression, string labelText, object htmlAttributes) { - var resolvedLabelText = labelText ?? metadata.DisplayName ?? metadata.PropertyName; + var resolvedLabelText = labelText ?? + modelExplorer.Metadata.DisplayName ?? + modelExplorer.Metadata.PropertyName; if (resolvedLabelText == null) { resolvedLabelText = @@ -263,7 +265,7 @@ namespace Microsoft.AspNet.Mvc.Rendering /// public virtual TagBuilder GeneratePassword( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, object value, object htmlAttributes) @@ -272,7 +274,7 @@ namespace Microsoft.AspNet.Mvc.Rendering return GenerateInput( viewContext, InputType.Password, - metadata, + modelExplorer, expression, value, useViewData: false, @@ -286,14 +288,14 @@ namespace Microsoft.AspNet.Mvc.Rendering /// public virtual TagBuilder GenerateRadioButton( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, object value, bool? isChecked, object htmlAttributes) { var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes); - if (metadata == null) + if (modelExplorer == null) { // RadioButton() case. Do not override checked attribute if isChecked is implicit. if (!isChecked.HasValue && @@ -322,7 +324,7 @@ namespace Microsoft.AspNet.Mvc.Rendering // Need a value to determine isChecked. Debug.Assert(value != null); - var model = metadata.Model; + var model = modelExplorer.Model; var valueString = Convert.ToString(value, CultureInfo.CurrentCulture); isChecked = model != null && string.Equals(model.ToString(), valueString, StringComparison.OrdinalIgnoreCase); @@ -337,7 +339,7 @@ namespace Microsoft.AspNet.Mvc.Rendering return GenerateInput( viewContext, InputType.Radio, - metadata, + modelExplorer, expression, value, useViewData: false, @@ -365,7 +367,7 @@ namespace Microsoft.AspNet.Mvc.Rendering /// public TagBuilder GenerateSelect( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string optionLabel, string expression, IEnumerable selectList, @@ -375,7 +377,7 @@ namespace Microsoft.AspNet.Mvc.Rendering ICollection ignored; return GenerateSelect( viewContext, - metadata, + modelExplorer, optionLabel, expression, selectList, @@ -387,7 +389,7 @@ namespace Microsoft.AspNet.Mvc.Rendering /// public virtual TagBuilder GenerateSelect( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string optionLabel, string expression, IEnumerable selectList, @@ -428,9 +430,9 @@ namespace Microsoft.AspNet.Mvc.Rendering { defaultValue = viewContext.ViewData.Eval(expression); } - else if (metadata != null) + else if (modelExplorer != null) { - defaultValue = metadata.Model; + defaultValue = modelExplorer.Model; } } @@ -469,7 +471,7 @@ namespace Microsoft.AspNet.Mvc.Rendering } } - tagBuilder.MergeAttributes(GetValidationAttributes(viewContext, metadata, expression)); + tagBuilder.MergeAttributes(GetValidationAttributes(viewContext, modelExplorer, expression)); return tagBuilder; } @@ -477,7 +479,7 @@ namespace Microsoft.AspNet.Mvc.Rendering /// public virtual TagBuilder GenerateTextArea( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, int rows, int columns, @@ -509,9 +511,9 @@ namespace Microsoft.AspNet.Mvc.Rendering { value = modelState.Value.AttemptedValue; } - else if (metadata.Model != null) + else if (modelExplorer.Model != null) { - value = metadata.Model.ToString(); + value = modelExplorer.Model.ToString(); } var tagBuilder = new TagBuilder("textarea"); @@ -528,7 +530,7 @@ namespace Microsoft.AspNet.Mvc.Rendering } tagBuilder.MergeAttribute("name", fullName, true); - tagBuilder.MergeAttributes(GetValidationAttributes(viewContext, metadata, expression)); + tagBuilder.MergeAttributes(GetValidationAttributes(viewContext, modelExplorer, expression)); // If there are any errors for a named field, we add this CSS attribute. if (modelState != null && modelState.Errors.Count > 0) @@ -546,7 +548,7 @@ namespace Microsoft.AspNet.Mvc.Rendering /// public virtual TagBuilder GenerateTextBox( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, object value, string format, @@ -556,10 +558,10 @@ namespace Microsoft.AspNet.Mvc.Rendering return GenerateInput( viewContext, InputType.Text, - metadata, + modelExplorer, expression, value, - useViewData: (metadata == null && value == null), + useViewData: (modelExplorer == null && value == null), isChecked: false, setId: true, isExplicitValue: true, @@ -726,17 +728,19 @@ namespace Microsoft.AspNet.Mvc.Rendering /// public IEnumerable GetClientValidationRules( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression) { var validatorProvider = _bindingContextAccessor.Value.ValidatorProvider; - metadata = metadata ?? + modelExplorer = modelExplorer ?? ExpressionMetadataProvider.FromStringExpression(expression, viewContext.ViewData, _metadataProvider); - var validationContext = - new ClientModelValidationContext(metadata, _metadataProvider, viewContext.HttpContext.RequestServices); + var validationContext = new ClientModelValidationContext( + modelExplorer.Metadata, + _metadataProvider, + viewContext.HttpContext.RequestServices); return validatorProvider - .GetValidators(metadata) + .GetValidators(modelExplorer.Metadata) .OfType() .SelectMany(v => v.GetClientValidationRules(validationContext)); } @@ -832,7 +836,7 @@ namespace Microsoft.AspNet.Mvc.Rendering protected virtual TagBuilder GenerateInput( [NotNull] ViewContext viewContext, InputType inputType, - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, object value, bool useViewData, @@ -926,7 +930,7 @@ namespace Microsoft.AspNet.Mvc.Rendering tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName); } - tagBuilder.MergeAttributes(GetValidationAttributes(viewContext, metadata, expression)); + tagBuilder.MergeAttributes(GetValidationAttributes(viewContext, modelExplorer, expression)); return tagBuilder; } @@ -951,7 +955,7 @@ namespace Microsoft.AspNet.Mvc.Rendering // never rendered validation for a field with this name in this form. protected virtual IDictionary GetValidationAttributes( ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression) { var formContext = viewContext.ClientValidationEnabled ? viewContext.FormContext : null; @@ -967,7 +971,8 @@ namespace Microsoft.AspNet.Mvc.Rendering } formContext.RenderedField(fullName, true); - var clientRules = GetClientValidationRules(viewContext, metadata, expression); + + var clientRules = GetClientValidationRules(viewContext, modelExplorer, expression); return UnobtrusiveValidationAttributesGenerator.GetValidationAttributes(clientRules); } @@ -1153,4 +1158,4 @@ namespace Microsoft.AspNet.Mvc.Rendering return tagBuilder.ToString(TagRenderMode.Normal); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs index 30e65bfd6b..f4921be5ab 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs @@ -235,7 +235,7 @@ namespace Microsoft.AspNet.Mvc.Rendering public HtmlString CheckBox(string expression, bool? isChecked, object htmlAttributes) { return GenerateCheckBox( - metadata: null, + modelExplorer: null, expression: expression, isChecked: isChecked, htmlAttributes: htmlAttributes); @@ -282,15 +282,15 @@ namespace Microsoft.AspNet.Mvc.Rendering /// public string DisplayName(string expression) { - var metadata = ExpressionMetadataProvider.FromStringExpression(expression, ViewData, MetadataProvider); - return GenerateDisplayName(metadata, expression); + var modelExplorer = ExpressionMetadataProvider.FromStringExpression(expression, ViewData, MetadataProvider); + return GenerateDisplayName(modelExplorer, expression); } /// public string DisplayText(string expression) { - var metadata = ExpressionMetadataProvider.FromStringExpression(expression, ViewData, MetadataProvider); - return GenerateDisplayText(metadata); + var modelExplorer = ExpressionMetadataProvider.FromStringExpression(expression, ViewData, MetadataProvider); + return GenerateDisplayText(modelExplorer); } /// @@ -301,7 +301,7 @@ namespace Microsoft.AspNet.Mvc.Rendering object htmlAttributes) { return GenerateDropDown( - metadata: null, + modelExplorer: null, expression: expression, selectList: selectList, optionLabel: optionLabel, @@ -315,10 +315,10 @@ namespace Microsoft.AspNet.Mvc.Rendering string htmlFieldName, object additionalViewData) { - var metadata = ExpressionMetadataProvider.FromStringExpression(expression, ViewData, MetadataProvider); + var modelExplorer = ExpressionMetadataProvider.FromStringExpression(expression, ViewData, MetadataProvider); return GenerateEditor( - metadata, + modelExplorer, htmlFieldName ?? ExpressionHelper.GetExpressionText(expression), templateName, additionalViewData); @@ -328,7 +328,7 @@ namespace Microsoft.AspNet.Mvc.Rendering public HtmlString Hidden(string expression, object value, object htmlAttributes) { return GenerateHidden( - metadata: null, + modelExplorer: null, expression: expression, value: value, useViewData: (value == null), @@ -344,9 +344,9 @@ namespace Microsoft.AspNet.Mvc.Rendering /// public HtmlString Label(string expression, string labelText, object htmlAttributes) { - var metadata = ExpressionMetadataProvider.FromStringExpression(expression, ViewData, MetadataProvider); + var modelExplorer = ExpressionMetadataProvider.FromStringExpression(expression, ViewData, MetadataProvider); return GenerateLabel( - metadata, + modelExplorer, expression, labelText, htmlAttributes); @@ -356,7 +356,7 @@ namespace Microsoft.AspNet.Mvc.Rendering public HtmlString ListBox(string expression, IEnumerable selectList, object htmlAttributes) { return GenerateListBox( - metadata: null, + modelExplorer: null, expression: expression, selectList: selectList, htmlAttributes: htmlAttributes); @@ -388,7 +388,7 @@ namespace Microsoft.AspNet.Mvc.Rendering return RenderPartialCoreAsync(partialViewName, model, viewData, ViewContext.Writer); } - protected virtual HtmlString GenerateDisplay(ModelMetadata metadata, + protected virtual HtmlString GenerateDisplay(ModelExplorer modelExplorer, string htmlFieldName, string templateName, object additionalViewData) @@ -396,7 +396,7 @@ namespace Microsoft.AspNet.Mvc.Rendering var templateBuilder = new TemplateBuilder(_viewEngine, ViewContext, ViewData, - metadata, + modelExplorer, htmlFieldName, templateName, readOnly: true, @@ -443,7 +443,7 @@ namespace Microsoft.AspNet.Mvc.Rendering public HtmlString Password(string expression, object value, object htmlAttributes) { return GeneratePassword( - metadata: null, + modelExplorer: null, expression: expression, value: value, htmlAttributes: htmlAttributes); @@ -453,7 +453,7 @@ namespace Microsoft.AspNet.Mvc.Rendering public HtmlString RadioButton(string expression, object value, bool? isChecked, object htmlAttributes) { return GenerateRadioButton( - metadata: null, + modelExplorer: null, expression: expression, value: value, isChecked: isChecked, @@ -535,19 +535,29 @@ namespace Microsoft.AspNet.Mvc.Rendering /// public HtmlString TextArea(string expression, string value, int rows, int columns, object htmlAttributes) { - var metadata = ExpressionMetadataProvider.FromStringExpression(expression, ViewData, MetadataProvider); + var modelExplorer = ExpressionMetadataProvider.FromStringExpression(expression, ViewData, MetadataProvider); if (value != null) { - metadata.Model = value; + // As a special case we allow treating a string value as a model of arbitrary type. + // So pass through the string representation, even though the ModelMetadata might + // be for some other type. + // + // We do this because thought we're displaying something as a string, we want to have + // the right set of validation attributes. + modelExplorer = new ModelExplorer( + MetadataProvider, + modelExplorer.Container, + modelExplorer.Metadata, + value); } - return GenerateTextArea(metadata, expression, rows, columns, htmlAttributes); + return GenerateTextArea(modelExplorer, expression, rows, columns, htmlAttributes); } /// public HtmlString TextBox(string expression, object value, string format, object htmlAttributes) { - return GenerateTextBox(metadata: null, expression: expression, value: value, format: format, + return GenerateTextBox(modelExplorer: null, expression: expression, value: value, format: format, htmlAttributes: htmlAttributes); } @@ -568,18 +578,19 @@ namespace Microsoft.AspNet.Mvc.Rendering } protected virtual HtmlString GenerateCheckBox( - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, bool? isChecked, object htmlAttributes) { var checkbox = _htmlGenerator.GenerateCheckBox( ViewContext, - metadata, + modelExplorer, expression, isChecked, htmlAttributes); - var hidden = _htmlGenerator.GenerateHiddenForCheckbox(ViewContext, metadata, expression); + + var hidden = _htmlGenerator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, expression); if (checkbox == null || hidden == null) { return HtmlString.Empty; @@ -590,12 +601,12 @@ namespace Microsoft.AspNet.Mvc.Rendering return new HtmlString(elements); } - protected virtual string GenerateDisplayName([NotNull] ModelMetadata metadata, string expression) + protected virtual string GenerateDisplayName([NotNull] ModelExplorer modelExplorer, string expression) { // We don't call ModelMetadata.GetDisplayName here because // we want to fall back to the field name rather than the ModelType. // This is similar to how the GenerateLabel get the text of a label. - var resolvedDisplayName = metadata.DisplayName ?? metadata.PropertyName; + var resolvedDisplayName = modelExplorer.Metadata.DisplayName ?? modelExplorer.Metadata.PropertyName; if (resolvedDisplayName == null) { resolvedDisplayName = @@ -605,13 +616,13 @@ namespace Microsoft.AspNet.Mvc.Rendering return resolvedDisplayName; } - protected virtual string GenerateDisplayText(ModelMetadata metadata) + protected virtual string GenerateDisplayText(ModelExplorer modelExplorer) { - return metadata.SimpleDisplayText ?? string.Empty; + return modelExplorer.GetSimpleDisplayText() ?? string.Empty; } protected HtmlString GenerateDropDown( - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, IEnumerable selectList, string optionLabel, @@ -619,7 +630,7 @@ namespace Microsoft.AspNet.Mvc.Rendering { var tagBuilder = _htmlGenerator.GenerateSelect( ViewContext, - metadata, + modelExplorer, optionLabel, expression: expression, selectList: selectList, @@ -634,7 +645,7 @@ namespace Microsoft.AspNet.Mvc.Rendering } protected virtual HtmlString GenerateEditor( - ModelMetadata metadata, + ModelExplorer modelExplorer, string htmlFieldName, string templateName, object additionalViewData) @@ -643,7 +654,7 @@ namespace Microsoft.AspNet.Mvc.Rendering _viewEngine, ViewContext, ViewData, - metadata, + modelExplorer, htmlFieldName, templateName, readOnly: false, @@ -742,7 +753,7 @@ namespace Microsoft.AspNet.Mvc.Rendering } protected virtual HtmlString GenerateHidden( - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, object value, bool useViewData, @@ -751,7 +762,7 @@ namespace Microsoft.AspNet.Mvc.Rendering var tagBuilder = _htmlGenerator.GenerateHidden( ViewContext, - metadata, + modelExplorer, expression, value, useViewData, @@ -773,14 +784,14 @@ namespace Microsoft.AspNet.Mvc.Rendering } protected virtual HtmlString GenerateLabel( - [NotNull] ModelMetadata metadata, + [NotNull] ModelExplorer modelExplorer, string expression, string labelText, object htmlAttributes) { var tagBuilder = _htmlGenerator.GenerateLabel( ViewContext, - metadata, + modelExplorer, expression: expression, labelText: labelText, htmlAttributes: htmlAttributes); @@ -793,14 +804,14 @@ namespace Microsoft.AspNet.Mvc.Rendering } protected HtmlString GenerateListBox( - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, IEnumerable selectList, object htmlAttributes) { var tagBuilder = _htmlGenerator.GenerateSelect( ViewContext, - metadata, + modelExplorer, optionLabel: null, expression: expression, selectList: selectList, @@ -821,14 +832,14 @@ namespace Microsoft.AspNet.Mvc.Rendering } protected virtual HtmlString GeneratePassword( - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, object value, object htmlAttributes) { var tagBuilder = _htmlGenerator.GeneratePassword( ViewContext, - metadata, + modelExplorer, expression, value, htmlAttributes); @@ -841,7 +852,7 @@ namespace Microsoft.AspNet.Mvc.Rendering } protected virtual HtmlString GenerateRadioButton( - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, object value, bool? isChecked, @@ -849,7 +860,7 @@ namespace Microsoft.AspNet.Mvc.Rendering { var tagBuilder = _htmlGenerator.GenerateRadioButton( ViewContext, - metadata, + modelExplorer, expression, value, isChecked, @@ -863,7 +874,7 @@ namespace Microsoft.AspNet.Mvc.Rendering } protected virtual HtmlString GenerateTextArea( - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, int rows, int columns, @@ -871,7 +882,7 @@ namespace Microsoft.AspNet.Mvc.Rendering { var tagBuilder = _htmlGenerator.GenerateTextArea( ViewContext, - metadata, + modelExplorer, expression, rows, columns, @@ -885,7 +896,7 @@ namespace Microsoft.AspNet.Mvc.Rendering } protected virtual HtmlString GenerateTextBox( - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, object value, string format, @@ -893,7 +904,7 @@ namespace Microsoft.AspNet.Mvc.Rendering { var tagBuilder = _htmlGenerator.GenerateTextBox( ViewContext, - metadata, + modelExplorer, expression, value, format, @@ -962,9 +973,8 @@ namespace Microsoft.AspNet.Mvc.Rendering { if (string.IsNullOrEmpty(expression)) { - // case 2(a): format the value from ModelMetadata for the current model - var metadata = ViewData.ModelMetadata; - resolvedValue = FormatValue(metadata.Model, format); + // case 2(a): format the value from ViewData for the current model + resolvedValue = FormatValue(ViewData.Model, format); } else { @@ -983,10 +993,10 @@ namespace Microsoft.AspNet.Mvc.Rendering /// public IEnumerable GetClientValidationRules( - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression) { - return _htmlGenerator.GetClientValidationRules(ViewContext, metadata, expression); + return _htmlGenerator.GetClientValidationRules(ViewContext, modelExplorer, expression); } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelperOfT.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelperOfT.cs index 2b73018f2e..3c0251d666 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelperOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelperOfT.cs @@ -56,8 +56,8 @@ namespace Microsoft.AspNet.Mvc.Rendering [NotNull] Expression> expression, object htmlAttributes) { - var metadata = GetModelMetadata(expression); - return GenerateCheckBox(metadata, GetExpressionName(expression), isChecked: null, + var modelExplorer = GetModelExplorer(expression); + return GenerateCheckBox(modelExplorer, GetExpressionName(expression), isChecked: null, htmlAttributes: htmlAttributes); } @@ -68,9 +68,9 @@ namespace Microsoft.AspNet.Mvc.Rendering string optionLabel, object htmlAttributes) { - var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider); + var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider); - return GenerateDropDown(metadata, ExpressionHelper.GetExpressionText(expression), selectList, + return GenerateDropDown(modelExplorer, ExpressionHelper.GetExpressionText(expression), selectList, optionLabel, htmlAttributes); } @@ -81,11 +81,11 @@ namespace Microsoft.AspNet.Mvc.Rendering string htmlFieldName, object additionalViewData) { - var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, + var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider); - return GenerateDisplay(metadata, + return GenerateDisplay(modelExplorer, htmlFieldName ?? ExpressionHelper.GetExpressionText(expression), templateName, additionalViewData); @@ -94,32 +94,32 @@ namespace Microsoft.AspNet.Mvc.Rendering /// public string DisplayNameFor([NotNull] Expression> expression) { - var metadata = GetModelMetadata(expression); - return GenerateDisplayName(metadata, ExpressionHelper.GetExpressionText(expression)); + var modelExplorer = GetModelExplorer(expression); + return GenerateDisplayName(modelExplorer, ExpressionHelper.GetExpressionText(expression)); } /// public string DisplayNameForInnerType( [NotNull] Expression> expression) { - var metadata = ExpressionMetadataProvider.FromLambdaExpression( + var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression( expression, new ViewDataDictionary(ViewData, model: null), MetadataProvider); var expressionText = ExpressionHelper.GetExpressionText(expression); - if (metadata == null) + if (modelExplorer == null) { throw new InvalidOperationException(Resources.FormatHtmlHelper_NullModelMetadata(expressionText)); } - return GenerateDisplayName(metadata, expressionText); + return GenerateDisplayName(modelExplorer, expressionText); } /// public string DisplayTextFor([NotNull] Expression> expression) { - return GenerateDisplayText(GetModelMetadata(expression)); + return GenerateDisplayText(GetModelExplorer(expression)); } /// @@ -129,10 +129,10 @@ namespace Microsoft.AspNet.Mvc.Rendering string htmlFieldName, object additionalViewData) { - var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider); + var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider); return GenerateEditor( - metadata, + modelExplorer, htmlFieldName ?? ExpressionHelper.GetExpressionText(expression), templateName, additionalViewData); @@ -143,7 +143,7 @@ namespace Microsoft.AspNet.Mvc.Rendering [NotNull] Expression> expression, object htmlAttributes) { - var metadata = GetModelMetadata(expression); + var metadata = GetModelExplorer(expression); return GenerateHidden(metadata, GetExpressionName(expression), metadata.Model, useViewData: false, htmlAttributes: htmlAttributes); } @@ -160,7 +160,7 @@ namespace Microsoft.AspNet.Mvc.Rendering string labelText, object htmlAttributes) { - var metadata = GetModelMetadata(expression); + var metadata = GetModelExplorer(expression); return GenerateLabel(metadata, ExpressionHelper.GetExpressionText(expression), labelText, htmlAttributes); } @@ -170,7 +170,7 @@ namespace Microsoft.AspNet.Mvc.Rendering IEnumerable selectList, object htmlAttributes) { - var metadata = GetModelMetadata(expression); + var metadata = GetModelExplorer(expression); var name = ExpressionHelper.GetExpressionText(expression); return GenerateListBox(metadata, name, selectList, htmlAttributes); @@ -188,7 +188,7 @@ namespace Microsoft.AspNet.Mvc.Rendering [NotNull] Expression> expression, object htmlAttributes) { - var metadata = GetModelMetadata(expression); + var metadata = GetModelExplorer(expression); return GeneratePassword(metadata, GetExpressionName(expression), value: null, htmlAttributes: htmlAttributes); } @@ -199,7 +199,7 @@ namespace Microsoft.AspNet.Mvc.Rendering [NotNull] object value, object htmlAttributes) { - var metadata = GetModelMetadata(expression); + var metadata = GetModelExplorer(expression); return GenerateRadioButton(metadata, GetExpressionName(expression), value, isChecked: null, htmlAttributes: htmlAttributes); } @@ -211,7 +211,7 @@ namespace Microsoft.AspNet.Mvc.Rendering int columns, object htmlAttributes) { - var metadata = GetModelMetadata(expression); + var metadata = GetModelExplorer(expression); return GenerateTextArea(metadata, GetExpressionName(expression), rows, columns, htmlAttributes); } @@ -221,7 +221,7 @@ namespace Microsoft.AspNet.Mvc.Rendering string format, object htmlAttributes) { - var metadata = GetModelMetadata(expression); + var metadata = GetModelExplorer(expression); return GenerateTextBox(metadata, GetExpressionName(expression), metadata.Model, format, htmlAttributes); } @@ -230,16 +230,16 @@ namespace Microsoft.AspNet.Mvc.Rendering return ExpressionHelper.GetExpressionText(expression); } - protected ModelMetadata GetModelMetadata([NotNull] Expression> expression) + protected ModelExplorer GetModelExplorer([NotNull] Expression> expression) { - var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider); - if (metadata == null) + var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider); + if (modelExplorer == null) { var expressionName = GetExpressionName(expression); throw new InvalidOperationException(Resources.FormatHtmlHelper_NullModelMetadata(expressionName)); } - return metadata; + return modelExplorer; } /// @@ -258,7 +258,7 @@ namespace Microsoft.AspNet.Mvc.Rendering /// public string ValueFor([NotNull] Expression> expression, string format) { - var metadata = GetModelMetadata(expression); + var metadata = GetModelExplorer(expression); return GenerateValue(ExpressionHelper.GetExpressionText(expression), metadata.Model, format, useViewData: false); } diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/IHtmlGenerator.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/IHtmlGenerator.cs index f8b8b50f1b..6938d24ca0 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/IHtmlGenerator.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/IHtmlGenerator.cs @@ -32,9 +32,23 @@ namespace Microsoft.AspNet.Mvc.Rendering TagBuilder GenerateAntiForgery([NotNull] ViewContext viewContext); + /// + /// Generate a <input type="checkbox".../> element. + /// + /// The instance for the current scope. + /// The for the model. + /// The model expression. + /// The initial state of the checkbox element. + /// + /// An that contains the HTML attributes for the element. Alternatively, an + /// instance containing the HTML attributes. + /// + /// + /// A instance for the <input type="checkbox".../> element. + /// TagBuilder GenerateCheckBox( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, bool? isChecked, object htmlAttributes); @@ -46,7 +60,7 @@ namespace Microsoft.AspNet.Mvc.Rendering /// TagBuilder GenerateHiddenForCheckbox( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression); /// @@ -107,7 +121,7 @@ namespace Microsoft.AspNet.Mvc.Rendering TagBuilder GenerateHidden( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, object value, bool useViewData, @@ -115,21 +129,21 @@ namespace Microsoft.AspNet.Mvc.Rendering TagBuilder GenerateLabel( [NotNull] ViewContext viewContext, - [NotNull] ModelMetadata metadata, + [NotNull] ModelExplorer modelExplorer, string expression, string labelText, object htmlAttributes); TagBuilder GeneratePassword( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, object value, object htmlAttributes); TagBuilder GenerateRadioButton( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, object value, bool? isChecked, @@ -146,7 +160,7 @@ namespace Microsoft.AspNet.Mvc.Rendering TagBuilder GenerateSelect( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string optionLabel, string expression, IEnumerable selectList, @@ -155,7 +169,7 @@ namespace Microsoft.AspNet.Mvc.Rendering TagBuilder GenerateSelect( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string optionLabel, string expression, IEnumerable selectList, @@ -165,7 +179,7 @@ namespace Microsoft.AspNet.Mvc.Rendering TagBuilder GenerateTextArea( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, int rows, int columns, @@ -173,7 +187,7 @@ namespace Microsoft.AspNet.Mvc.Rendering TagBuilder GenerateTextBox( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression, object value, string format, @@ -199,7 +213,7 @@ namespace Microsoft.AspNet.Mvc.Rendering /// IEnumerable GetClientValidationRules( [NotNull] ViewContext viewContext, - ModelMetadata metadata, + ModelExplorer modelExplorer, string expression); } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TemplateBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TemplateBuilder.cs index d2cc3bc1a1..4d1c3a55be 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TemplateBuilder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TemplateBuilder.cs @@ -12,6 +12,8 @@ namespace Microsoft.AspNet.Mvc.Rendering private IViewEngine _viewEngine; private ViewContext _viewContext; private ViewDataDictionary _viewData; + private ModelExplorer _modelExplorer; + private object _model; private ModelMetadata _metadata; private string _htmlFieldName; private string _templateName; @@ -21,7 +23,7 @@ namespace Microsoft.AspNet.Mvc.Rendering public TemplateBuilder([NotNull] IViewEngine viewEngine, [NotNull] ViewContext viewContext, [NotNull] ViewDataDictionary viewData, - [NotNull] ModelMetadata metadata, + [NotNull] ModelExplorer modelExplorer, string htmlFieldName, string templateName, bool readOnly, @@ -30,45 +32,51 @@ namespace Microsoft.AspNet.Mvc.Rendering _viewEngine = viewEngine; _viewContext = viewContext; _viewData = viewData; - _metadata = metadata; + _modelExplorer = modelExplorer; _htmlFieldName = htmlFieldName; _templateName = templateName; _readOnly = readOnly; _additionalViewData = additionalViewData; + + _model = modelExplorer.Model; + _metadata = modelExplorer.Metadata; } public string Build() { - if (_metadata.ConvertEmptyStringToNull && string.Empty.Equals(_metadata.Model)) + if (_metadata.ConvertEmptyStringToNull && string.Empty.Equals(_model)) { - _metadata.Model = null; + _model = null; } - var formattedModelValue = _metadata.Model; - if (_metadata.Model == null && _readOnly) + var formattedModelValue = _model; + if (_model == null && _readOnly) { formattedModelValue = _metadata.NullDisplayText; } var formatString = _readOnly ? _metadata.DisplayFormatString : _metadata.EditFormatString; - if (_metadata.Model != null && !string.IsNullOrEmpty(formatString)) + if (_model != null && !string.IsNullOrEmpty(formatString)) { - formattedModelValue = string.Format(CultureInfo.CurrentCulture, formatString, _metadata.Model); + formattedModelValue = string.Format(CultureInfo.CurrentCulture, formatString, _model); } // Normally this shouldn't happen, unless someone writes their own custom Object templates which // don't check to make sure that the object hasn't already been displayed - if (_viewData.TemplateInfo.Visited(_metadata)) + if (_viewData.TemplateInfo.Visited(_modelExplorer)) { return string.Empty; } - var viewData = new ViewDataDictionary(_viewData, model: null) - { - Model = _metadata.Model, - ModelMetadata = _metadata - }; + // 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. + var viewData = new ViewDataDictionary(_viewData, model: null); + + // We're setting ModelExplorer in order to preserve the model metadata of the original + // _viewData even though _model may be null. + viewData.ModelExplorer = _modelExplorer.GetExplorerForModel(_model); viewData.TemplateInfo.FormattedModelValue = formattedModelValue; viewData.TemplateInfo.HtmlFieldPrefix = _viewData.TemplateInfo.GetFullHtmlFieldName(_htmlFieldName); @@ -81,7 +89,7 @@ namespace Microsoft.AspNet.Mvc.Rendering } } - var visitedObjectsKey = _metadata.Model ?? _metadata.RealModelType; + var visitedObjectsKey = _model ?? _modelExplorer.ModelType; viewData.TemplateInfo.AddVisited(visitedObjectsKey); var templateRenderer = new TemplateRenderer(_viewEngine, _viewContext, viewData, _templateName, _readOnly); diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TemplateRenderer.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TemplateRenderer.cs index a452529c64..b43b2d199d 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TemplateRenderer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TemplateRenderer.cs @@ -114,7 +114,7 @@ namespace Microsoft.AspNet.Mvc.Rendering } throw new InvalidOperationException( - Resources.FormatTemplateHelpers_NoTemplate(_viewData.ModelMetadata.RealModelType.FullName)); + Resources.FormatTemplateHelpers_NoTemplate(_viewData.ModelExplorer.ModelType.FullName)); } private Dictionary> GetDefaultActions() @@ -139,7 +139,8 @@ namespace Microsoft.AspNet.Mvc.Rendering // We don't want to search for Nullable, we want to search for T (which should handle both T and // Nullable). - var fieldType = Nullable.GetUnderlyingType(metadata.RealModelType) ?? metadata.RealModelType; + var modelType = _viewData.ModelExplorer.ModelType; + var fieldType = Nullable.GetUnderlyingType(modelType) ?? modelType; yield return fieldType.Name; diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelper.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelper.cs index 7ba295fcc2..7e8209fd75 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelper.cs @@ -352,7 +352,7 @@ namespace Microsoft.AspNet.Mvc.Rendering /// is null; ignored otherwise. /// /// An containing the relevant rules. - IEnumerable GetClientValidationRules(ModelMetadata metadata, string expression); + IEnumerable GetClientValidationRules(ModelExplorer modelExplorer, string expression); /// /// Returns an <input> element of type "hidden" for the specified . diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/ModelExpression.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/ModelExpression.cs index f5ab104e29..3d45b59492 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/ModelExpression.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/ModelExpression.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.Framework.Internal; @@ -19,13 +17,13 @@ namespace Microsoft.AspNet.Mvc.Rendering /// /// String representation of the of interest. /// - /// - /// Metadata about the of interest. + /// + /// Includes the model and metadata about the of interest. /// - public ModelExpression([NotNull] string name, [NotNull] ModelMetadata metadata) + public ModelExpression([NotNull] string name, [NotNull] ModelExplorer modelExplorer) { Name = name; - Metadata = metadata; + ModelExplorer = modelExplorer; } /// @@ -36,10 +34,36 @@ namespace Microsoft.AspNet.Mvc.Rendering /// /// Metadata about the of interest. /// + public ModelMetadata Metadata + { + get + { + return ModelExplorer.Metadata; + } + } + + /// + /// Gets the model object for the of interest. + /// /// - /// Getting will evaluate a compiled version of the original + /// Getting will evaluate a compiled version of the original /// . /// - public ModelMetadata Metadata { get; private set; } + public object Model + { + get + { + return ModelExplorer.Model; + } + } + + /// + /// Gets the model explorer for the of interest. + /// + /// + /// Getting will evaluate a compiled version of the original + /// . + /// + public ModelExplorer ModelExplorer { get; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/TemplateInfo.cs b/src/Microsoft.AspNet.Mvc.Core/TemplateInfo.cs index 9a1f3335d2..59bfe84568 100644 --- a/src/Microsoft.AspNet.Mvc.Core/TemplateInfo.cs +++ b/src/Microsoft.AspNet.Mvc.Core/TemplateInfo.cs @@ -88,9 +88,9 @@ namespace Microsoft.AspNet.Mvc } } - public bool Visited(ModelMetadata metadata) + public bool Visited(ModelExplorer modelExplorer) { - return _visitedObjects.Contains(metadata.Model ?? metadata.ModelType); + return _visitedObjects.Contains(modelExplorer.Model ?? modelExplorer.Metadata.ModelType); } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewDataDictionary.cs b/src/Microsoft.AspNet.Mvc.Core/ViewDataDictionary.cs index 90dfd9d6e3..fb63b11b2d 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ViewDataDictionary.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ViewDataDictionary.cs @@ -18,9 +18,6 @@ namespace Microsoft.AspNet.Mvc private readonly Type _declaredModelType; private readonly IModelMetadataProvider _metadataProvider; - private object _model; - private ModelMetadata _modelMetadata; - /// /// Initializes a new instance of the class. /// @@ -128,7 +125,7 @@ namespace Microsoft.AspNet.Mvc templateInfo: new TemplateInfo()) { // This is the core constructor called when Model is unknown. Base ModelMetadata on the declared type. - ModelMetadata = _metadataProvider.GetMetadataForType(() => null, _declaredModelType); + ModelExplorer = _metadataProvider.GetModelExplorerForType(declaredModelType, model: null); } /// @@ -188,16 +185,16 @@ namespace Microsoft.AspNet.Mvc { // This is the core constructor called when Model is known. var modelType = GetModelType(model); - if (modelType == source.ModelMetadata.ModelType && model == source.ModelMetadata.Model) + if (modelType == source.ModelMetadata.ModelType && model == source.ModelExplorer.Model) { - // Preserve any customizations made to source.ModelMetadata if the Type that will be calculated in - // SetModel() and source.Model match new instance's values. - ModelMetadata = source.ModelMetadata; + // 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. + ModelExplorer = source.ModelExplorer; } else if (model == null) { - // Ensure ModelMetadata is never null though SetModel() isn't called. - ModelMetadata = _metadataProvider.GetMetadataForType(() => null, _declaredModelType); + // Ensure ModelMetadata is never null though SetModel() isn't called below. + ModelExplorer = _metadataProvider.GetModelExplorerForType(_declaredModelType, model: null); } // If we're constructing a ViewDataDictionary where TModel is a non-Nullable value type, @@ -226,12 +223,11 @@ namespace Microsoft.AspNet.Mvc { get { - return _model; + return ModelExplorer.Model; } set { - // Reset ModelMetadata to ensure Model and ModelMetadata.Model remain equal. - _modelMetadata = null; + // Reset ModelExplorer to ensure Model and ModelMetadata.Model remain equal. SetModel(value); } } @@ -250,21 +246,15 @@ namespace Microsoft.AspNet.Mvc { get { - return _modelMetadata; - } - set - { - if (value == null) - { - throw new ArgumentNullException(Resources.FormatPropertyOfTypeCannotBeNull( - nameof(ViewDataDictionary.ModelMetadata), - nameof(ViewDataDictionary))); - } - - _modelMetadata = value; + return ModelExplorer.Metadata; } } + /// + /// Gets or sets the for the . + /// + public ModelExplorer ModelExplorer { get; set; } + public TemplateInfo TemplateInfo { get; } #region IDictionary properties @@ -355,15 +345,24 @@ namespace Microsoft.AspNet.Mvc protected virtual void SetModel(object value) { EnsureCompatible(value); - _model = value; // Reset or override ModelMetadata based on runtime value type. Fall back to declared type if value is // null. When called from the Model setter, ModelMetadata will (temporarily) be null. When called from // a constructor, current ModelMetadata may already be set to preserve customizations made in parent scope. var modelType = GetModelType(value); - if (ModelMetadata == null || ModelMetadata.ModelType != modelType) + if (ModelExplorer?.Metadata.ModelType != modelType) { - ModelMetadata = _metadataProvider.GetMetadataForType(() => value, modelType); + 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. + } + else + { + // The metadata matches, but it's a new value. + ModelExplorer = new ModelExplorer(_metadataProvider, ModelExplorer.Container, ModelMetadata, value); } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CollectionModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CollectionModelBinder.cs index 26d579d5dd..d0b4f1e32e 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CollectionModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CollectionModelBinder.cs @@ -44,14 +44,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var boundCollection = new List(); + var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; + var elementMetadata = metadataProvider.GetMetadataForType(typeof(TElement)); + var rawValueArray = RawValueToObjectArray(rawValue); foreach (var rawValueElement in rawValueArray) { - var innerModelMetadata = - bindingContext.OperationBindingContext.MetadataProvider.GetMetadataForType(null, typeof(TElement)); var innerBindingContext = new ModelBindingContext(bindingContext, bindingContext.ModelName, - innerModelMetadata) + elementMetadata) { ValueProvider = new CompositeValueProvider { @@ -97,13 +98,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding .Select(i => i.ToString(CultureInfo.InvariantCulture)); } + var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; + var elementMetadata = metadataProvider.GetMetadataForType(typeof(TElement)); + var boundCollection = new List(); foreach (var indexName in indexNames) { var fullChildName = ModelBindingHelper.CreateIndexModelName(bindingContext.ModelName, indexName); - var childModelMetadata = - bindingContext.OperationBindingContext.MetadataProvider.GetMetadataForType(null, typeof(TElement)); - var childBindingContext = new ModelBindingContext(bindingContext, fullChildName, childModelMetadata); + var childBindingContext = new ModelBindingContext(bindingContext, fullChildName, elementMetadata); var didBind = false; object boundValue = null; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs index 42e50d6247..5bb57432e2 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding typeof(ComplexModelDto), allowNullModel: false); - var dto = (ComplexModelDto)bindingContext.ModelMetadata.Model; + var dto = (ComplexModelDto)bindingContext.Model; foreach (var propertyMetadata in dto.PropertyMetadata) { var propertyModelName = ModelBindingHelper.CreatePropertyModelName( diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs index eabbc33aa9..93b0640632 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs @@ -57,8 +57,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding if (modelBindingResult.IsModelSet) { - bindingContext.ModelMetadata.Model = modelBindingResult.Model; - // Update the model state key if we are bound using an empty prefix and it is a complex type. // This is needed as validation uses the model state key to log errors. The client validation expects // the errors with property names rather than the full name. @@ -112,6 +110,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { var newBindingContext = new ModelBindingContext { + Model = oldBindingContext.Model, ModelMetadata = oldBindingContext.ModelMetadata, ModelName = modelName, ModelState = oldBindingContext.ModelState, diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/KeyValuePairModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/KeyValuePairModelBinder.cs index 4a75fbd432..889994503d 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/KeyValuePairModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/KeyValuePairModelBinder.cs @@ -18,36 +18,40 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var keyResult = await TryBindStrongModel(bindingContext, "Key"); var valueResult = await TryBindStrongModel(bindingContext, "Value"); - var model = bindingContext.ModelMetadata.Model; - var isModelSet = false; + if (keyResult.IsModelSet && valueResult.IsModelSet) { - model = new KeyValuePair( + var model = new KeyValuePair( ModelBindingHelper.CastOrDefault(keyResult.Model), ModelBindingHelper.CastOrDefault(valueResult.Model)); - isModelSet = true; + + return new ModelBindingResult(model, bindingContext.ModelName, isModelSet: true); } else if (!keyResult.IsModelSet && valueResult.IsModelSet) { - bindingContext.ModelState.TryAddModelError(keyResult.Key, + bindingContext.ModelState.TryAddModelError( + keyResult.Key, Resources.KeyValuePair_BothKeyAndValueMustBePresent); + return new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false); } else if (keyResult.IsModelSet && !valueResult.IsModelSet) { - bindingContext.ModelState.TryAddModelError(valueResult.Key, + bindingContext.ModelState.TryAddModelError( + valueResult.Key, Resources.KeyValuePair_BothKeyAndValueMustBePresent); + return new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false); + } + else + { + return null; } - - var result = new ModelBindingResult(model, bindingContext.ModelName, isModelSet); - return (keyResult.IsModelSet || valueResult.IsModelSet) ? result : null; } internal async Task TryBindStrongModel(ModelBindingContext parentBindingContext, string propertyName) { var propertyModelMetadata = - parentBindingContext.OperationBindingContext.MetadataProvider.GetMetadataForType(modelAccessor: null, - modelType: typeof(TModel)); + parentBindingContext.OperationBindingContext.MetadataProvider.GetMetadataForType(typeof(TModel)); var propertyModelName = ModelBindingHelper.CreatePropertyModelName(parentBindingContext.ModelName, propertyName); var propertyBindingContext = diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs index 9ae1c4942f..16a571de30 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs @@ -38,8 +38,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding // post-processing, e.g. property setters and hooking up validation ProcessDto(bindingContext, (ComplexModelDto)result.Model); - return - new ModelBindingResult(bindingContext.ModelMetadata.Model, bindingContext.ModelName, isModelSet: true); + return new ModelBindingResult( + bindingContext.Model, + bindingContext.ModelName, + isModelSet: true); } protected virtual bool CanUpdateProperty(ModelMetadata propertyMetadata) @@ -236,18 +238,25 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return true; } - private async Task CreateAndPopulateDto(ModelBindingContext bindingContext, - IEnumerable propertyMetadatas) + private async Task CreateAndPopulateDto( + ModelBindingContext bindingContext, + IEnumerable propertyMetadatas) { // create a DTO and call into the DTO binder - var originalDto = new ComplexModelDto(bindingContext.ModelMetadata, propertyMetadatas); - var complexModelDtoMetadata = - bindingContext.OperationBindingContext.MetadataProvider.GetMetadataForType(() => originalDto, - typeof(ComplexModelDto)); - var dtoBindingContext = - new ModelBindingContext(bindingContext, bindingContext.ModelName, complexModelDtoMetadata); + var dto = new ComplexModelDto(bindingContext.ModelMetadata, propertyMetadatas); - return await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(dtoBindingContext); + var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; + var dtoMetadata = metadataProvider.GetMetadataForType(typeof(ComplexModelDto)); + + var childContext = new ModelBindingContext( + bindingContext, + bindingContext.ModelName, + dtoMetadata) + { + Model = dto, + }; + + return await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(childContext); } protected virtual object CreateModel(ModelBindingContext bindingContext) @@ -259,9 +268,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding protected virtual void EnsureModel(ModelBindingContext bindingContext) { - if (bindingContext.ModelMetadata.Model == null) + if (bindingContext.Model == null) { - bindingContext.ModelMetadata.Model = CreateModel(bindingContext); + bindingContext.Model = CreateModel(bindingContext); } } @@ -349,6 +358,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding internal void ProcessDto(ModelBindingContext bindingContext, ComplexModelDto dto) { + var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; + var modelExplorer = metadataProvider.GetModelExplorerForType(bindingContext.ModelType, bindingContext.Model); + var validationInfo = GetPropertyValidationInfo(bindingContext); // Eliminate provided properties from requiredProperties; leaving just *missing* required properties. @@ -359,12 +371,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { var addedError = false; - // Update Model as SetProperty() would: Place null value where validator will check for non-null. This - // ensures a failure result from a required validator (if any) even for a non-nullable property. - // (Otherwise, propertyMetadata.Model is likely already null.) - var propertyMetadata = bindingContext.ModelMetadata.Properties[missingRequiredProperty]; - propertyMetadata.Model = null; - var propertyName = propertyMetadata.BinderModelName ?? missingRequiredProperty; + // We want to provide the 'null' value, not the value of model.Property, + // so avoiding modelExplorer.GetProperty here which would call the actual getter on the + // model. This avoids issues with value types, or properties with pre-initialized values. + var propertyExplorer = modelExplorer.GetExplorerForProperty(missingRequiredProperty, model: null); + + var propertyName = propertyExplorer.Metadata.BinderModelName ?? missingRequiredProperty; var modelStateKey = ModelBindingHelper.CreatePropertyModelName( bindingContext.ModelName, propertyName); @@ -373,7 +385,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding IModelValidator validator; if (validationInfo.RequiredValidators.TryGetValue(missingRequiredProperty, out validator)) { - addedError = RunValidator(validator, bindingContext, propertyMetadata, modelStateKey); + addedError = RunValidator(validator, bindingContext, propertyExplorer, modelStateKey); } // Fall back to default message if BindingBehaviorAttribute required this property or validator @@ -389,26 +401,31 @@ namespace Microsoft.AspNet.Mvc.ModelBinding // for each property that was bound, call the setter, recording exceptions as necessary foreach (var entry in dto.Results) { - var propertyMetadata = entry.Key; var dtoResult = entry.Value; if (dtoResult != null) { + var propertyMetadata = entry.Key; IModelValidator requiredValidator; - validationInfo.RequiredValidators.TryGetValue(propertyMetadata.PropertyName, - out requiredValidator); - SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator); + validationInfo.RequiredValidators.TryGetValue( + propertyMetadata.PropertyName, + out requiredValidator); + + SetProperty(bindingContext, modelExplorer, propertyMetadata, dtoResult, requiredValidator); } } } - protected virtual void SetProperty(ModelBindingContext bindingContext, - ModelMetadata propertyMetadata, - ModelBindingResult dtoResult, - IModelValidator requiredValidator) + protected virtual void SetProperty( + ModelBindingContext bindingContext, + ModelExplorer modelExplorer, + ModelMetadata propertyMetadata, + ModelBindingResult dtoResult, + IModelValidator requiredValidator) { var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase; - var property = bindingContext.ModelType - .GetProperty(propertyMetadata.PropertyName, bindingFlags); + var property = bindingContext.ModelType.GetProperty( + propertyMetadata.PropertyName, + bindingFlags); if (property == null || !property.CanWrite) { @@ -427,8 +444,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding hasDefaultValue = TryGetPropertyDefaultValue(property, out value); } - propertyMetadata.Model = value; - // 'Required' validators need to run first so that we can provide useful error messages if // the property setters throw, e.g. if we're setting entity keys to null. if (value == null) @@ -439,7 +454,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { if (requiredValidator != null) { - var validationContext = new ModelValidationContext(bindingContext, propertyMetadata); + var propertyExplorer = modelExplorer.GetExplorerForExpression(propertyMetadata, model: null); + var validationContext = new ModelValidationContext(bindingContext, propertyExplorer); foreach (var validationResult in requiredValidator.Validate(validationContext)) { bindingContext.ModelState.TryAddModelError(modelStateKey, validationResult.Message); @@ -459,7 +475,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { try { - property.SetValue(bindingContext.ModelMetadata.Model, value); + property.SetValue(bindingContext.Model, value); } catch (Exception ex) { @@ -492,12 +508,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } // Returns true if validator execution adds a model error. - private static bool RunValidator(IModelValidator validator, - ModelBindingContext bindingContext, - ModelMetadata propertyMetadata, - string modelStateKey) + private static bool RunValidator( + IModelValidator validator, + ModelBindingContext bindingContext, + ModelExplorer propertyExplorer, + string modelStateKey) { - var validationContext = new ModelValidationContext(bindingContext, propertyMetadata); + var validationContext = new ModelValidationContext(bindingContext, propertyExplorer); var addedError = false; foreach (var validationResult in validator.Validate(validationContext)) diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/IObjectModelValidator.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/IObjectModelValidator.cs index f1f593b2aa..e9f98e12c4 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/IObjectModelValidator.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/IObjectModelValidator.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding public interface IObjectModelValidator { /// - /// Validates the given model in . + /// Validates the given model in . /// /// The associated with the current call. /// diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ModelBindingHelper.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ModelBindingHelper.cs index 170982b341..6419c71727 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ModelBindingHelper.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ModelBindingHelper.cs @@ -81,17 +81,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Internal throw new ArgumentException(message, "bindingContext"); } - if (!allowNullModel && bindingContext.ModelMetadata.Model == null) + if (!allowNullModel && bindingContext.Model == null) { var message = Resources.FormatModelBinderUtil_ModelCannotBeNull(requiredType); throw new ArgumentException(message, "bindingContext"); } - if (bindingContext.ModelMetadata.Model != null && + if (bindingContext.Model != null && !bindingContext.ModelType.GetTypeInfo().IsAssignableFrom(requiredType.GetTypeInfo())) { var message = Resources.FormatModelBinderUtil_ModelInstanceIsWrong( - bindingContext.ModelMetadata.Model.GetType(), + bindingContext.Model.GetType(), requiredType); throw new ArgumentException(message, "bindingContext"); } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs index c2f3c694f2..f434d07232 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs @@ -19,13 +19,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private readonly ConcurrentDictionary> _typePropertyInfoCache = new ConcurrentDictionary>(); - public IEnumerable GetMetadataForProperties(object container, [NotNull] Type containerType) + public IEnumerable GetMetadataForProperties([NotNull] Type containerType) { - return GetMetadataForPropertiesCore(container, containerType); + return GetMetadataForPropertiesCore(containerType); } - public ModelMetadata GetMetadataForProperty(Func modelAccessor, - [NotNull] Type containerType, + public ModelMetadata GetMetadataForProperty([NotNull] Type containerType, [NotNull] string propertyName) { if (string.IsNullOrEmpty(propertyName)) @@ -42,17 +41,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding throw new ArgumentException(message, nameof(propertyName)); } - return CreatePropertyMetadata(modelAccessor, propertyInfo); + return CreatePropertyMetadata(propertyInfo); } - public ModelMetadata GetMetadataForType(Func modelAccessor, [NotNull] Type modelType) + public ModelMetadata GetMetadataForType([NotNull] Type modelType) { var prototype = GetTypeInformation(modelType); - return CreateMetadataFromPrototype(prototype, modelAccessor); + return CreateMetadataFromPrototype(prototype); } public ModelMetadata GetMetadataForParameter( - Func modelAccessor, [NotNull] MethodInfo methodInfo, [NotNull] string parameterName) { @@ -69,7 +67,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding throw new ArgumentException(message, nameof(parameterName)); } - return GetMetadataForParameterCore(modelAccessor, parameterName, parameter); + return GetMetadataForParameterCore(parameterName, parameter); } // Override for creating the prototype metadata (without the model accessor). @@ -104,47 +102,36 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// /// A new instance based on . /// - protected abstract TModelMetadata CreateMetadataFromPrototype(TModelMetadata prototype, - Func modelAccessor); + protected abstract TModelMetadata CreateMetadataFromPrototype(TModelMetadata prototype); - private ModelMetadata GetMetadataForParameterCore(Func modelAccessor, - string parameterName, - ParameterInfo parameter) + private ModelMetadata GetMetadataForParameterCore( + string parameterName, + ParameterInfo parameter) { var parameterInfo = CreateParameterInfo(parameter.ParameterType, ModelAttributes.GetAttributesForParameter(parameter), parameterName); - var metadata = CreateMetadataFromPrototype(parameterInfo.Prototype, modelAccessor); + var metadata = CreateMetadataFromPrototype(parameterInfo.Prototype); return metadata; } - private IEnumerable GetMetadataForPropertiesCore(object container, Type containerType) + private IEnumerable GetMetadataForPropertiesCore(Type containerType) { var typePropertyInfo = GetTypePropertyInformation(containerType); foreach (var kvp in typePropertyInfo) { var propertyInfo = kvp.Value; - Func modelAccessor = null; - if (container != null) - { - modelAccessor = () => propertyInfo.PropertyHelper.GetValue(container); - } - var propertyMetadata = CreatePropertyMetadata(modelAccessor, propertyInfo); - if (propertyMetadata != null) - { - propertyMetadata.Container = container; - } - + var propertyMetadata = CreatePropertyMetadata(propertyInfo); yield return propertyMetadata; } } - private TModelMetadata CreatePropertyMetadata(Func modelAccessor, PropertyInformation propertyInfo) + private TModelMetadata CreatePropertyMetadata(PropertyInformation propertyInfo) { - var metadata = CreateMetadataFromPrototype(propertyInfo.Prototype, modelAccessor); + var metadata = CreateMetadataFromPrototype(propertyInfo.Prototype); if (propertyInfo.IsReadOnly) { metadata.IsReadOnly = true; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs index 3220d35f60..10bda645de 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs @@ -16,9 +16,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private static readonly string HtmlName = DataType.Html.ToString(); private bool _isEditFormatStringFromCache; - public CachedDataAnnotationsModelMetadata(CachedDataAnnotationsModelMetadata prototype, - Func modelAccessor) - : base(prototype, modelAccessor) + public CachedDataAnnotationsModelMetadata(CachedDataAnnotationsModelMetadata prototype) + : base(prototype) { } @@ -299,24 +298,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return result ?? base.ComputeOrder(); } - protected override string ComputeSimpleDisplayText() + /// + protected override string ComputeSimpleDisplayProperty() { - if (Model != null && - PrototypeCache.DisplayColumn != null && - !string.IsNullOrEmpty(PrototypeCache.DisplayColumn.DisplayColumn)) + if (!string.IsNullOrEmpty(PrototypeCache.DisplayColumn?.DisplayColumn)) { var displayColumnProperty = ModelType.GetTypeInfo().GetDeclaredProperty( PrototypeCache.DisplayColumn.DisplayColumn); ValidateDisplayColumnAttribute(PrototypeCache.DisplayColumn, displayColumnProperty, ModelType); - var simpleDisplayTextValue = displayColumnProperty.GetValue(Model, null); - if (simpleDisplayTextValue != null) - { - return simpleDisplayTextValue.ToString(); - } + return displayColumnProperty.Name; } - return base.ComputeSimpleDisplayText(); + return base.ComputeSimpleDisplayProperty(); } protected override bool ComputeShowForDisplay() diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs index 3e5e6e094a..ece05c4887 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs @@ -33,6 +33,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private int _order; private bool _showForDisplay; private bool _showForEdit; + private string _simpleDisplayProperty; private string _templateHint; private IBinderMetadata _binderMetadata; private string _binderModelName; @@ -56,6 +57,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private bool _orderComputed; private bool _showForDisplayComputed; private bool _showForEditComputed; + private bool _simpleDisplayPropertyComputed; private bool _templateHintComputed; private bool _isBinderMetadataComputed; private bool _isBinderModelNameComputed; @@ -63,10 +65,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private bool _propertyBindingPredicateProviderComputed; // Constructor for creating real instances of the metadata class based on a prototype - protected CachedModelMetadata(CachedModelMetadata prototype, Func modelAccessor) + protected CachedModelMetadata(CachedModelMetadata prototype) : base(prototype.Provider, prototype.ContainerType, - modelAccessor, prototype.ModelType, prototype.PropertyName) { @@ -82,7 +83,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding Type modelType, string propertyName, TPrototypeCache prototypeCache) - : base(provider, containerType, modelAccessor: null, modelType: modelType, propertyName: propertyName) + : base(provider, containerType, modelType: modelType, propertyName: propertyName) { PrototypeCache = prototypeCache; } @@ -500,17 +501,21 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } /// - public sealed override string SimpleDisplayText + public sealed override string SimpleDisplayProperty { get { - // Value already cached in ModelMetadata. That class also already exposes ComputeSimpleDisplayText() - // for overrides. Sealed here for consistency with other properties. - return base.SimpleDisplayText; + if (!_simpleDisplayPropertyComputed) + { + _simpleDisplayProperty = ComputeSimpleDisplayProperty(); + _simpleDisplayPropertyComputed = true; + } + return _simpleDisplayProperty; } set { - base.SimpleDisplayText = value; + _simpleDisplayProperty = value; + _simpleDisplayPropertyComputed = true; } } @@ -701,6 +706,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return base.ShowForEdit; } + /// + /// Calculate the value. + /// + /// Calculated value. + protected virtual string ComputeSimpleDisplayProperty() + { + return base.SimpleDisplayProperty; + } + /// /// Calculate the value. /// diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/DataAnnotationsModelMetadataProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/DataAnnotationsModelMetadataProvider.cs index a838a7b0dc..af1c4dc781 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/DataAnnotationsModelMetadataProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/DataAnnotationsModelMetadataProvider.cs @@ -28,10 +28,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// Copies only a few values from the . Unlikely the rest have been computed. /// protected override CachedDataAnnotationsModelMetadata CreateMetadataFromPrototype( - CachedDataAnnotationsModelMetadata prototype, - Func modelAccessor) + CachedDataAnnotationsModelMetadata prototype) { - var metadata = new CachedDataAnnotationsModelMetadata(prototype, modelAccessor); + var metadata = new CachedDataAnnotationsModelMetadata(prototype); foreach (var keyValuePair in prototype.AdditionalValues) { metadata.AdditionalValues.Add(keyValuePair); diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/EmptyModelMetadataProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/EmptyModelMetadataProvider.cs index 7f24b9ca9f..fe59cb6684 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/EmptyModelMetadataProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/EmptyModelMetadataProvider.cs @@ -26,7 +26,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return new ModelMetadata( this, containerType, - modelAccessor: null, modelType: modelType, propertyName: propertyName); } @@ -36,13 +35,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// Copies very few values from the . Likely has not /// been modified except to add entries. /// - protected override ModelMetadata CreateMetadataFromPrototype([NotNull] ModelMetadata prototype, - Func modelAccessor) + protected override ModelMetadata CreateMetadataFromPrototype([NotNull] ModelMetadata prototype) { var metadata = new ModelMetadata( this, prototype.ContainerType, - modelAccessor, prototype.ModelType, prototype.PropertyName); foreach (var keyValuePair in prototype.AdditionalValues) diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/IModelMetadataProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/IModelMetadataProvider.cs index 5b873db513..0730ec2a07 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/IModelMetadataProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/IModelMetadataProvider.cs @@ -10,18 +10,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { public interface IModelMetadataProvider { - IEnumerable GetMetadataForProperties(object container, [NotNull] Type containerType); + IEnumerable GetMetadataForProperties([NotNull] Type containerType); - ModelMetadata GetMetadataForProperty( - Func modelAccessor, - [NotNull] Type containerType, - [NotNull] string propertyName); + ModelMetadata GetMetadataForType([NotNull] Type modelType); - ModelMetadata GetMetadataForType(Func modelAccessor, [NotNull] Type modelType); - - ModelMetadata GetMetadataForParameter( - Func modelAccessor, - [NotNull] MethodInfo methodInfo, - [NotNull] string parameterName); + ModelMetadata GetMetadataForParameter([NotNull] MethodInfo methodInfo, [NotNull] string parameterName); } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelExplorer.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelExplorer.cs new file mode 100644 index 0000000000..936e309699 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelExplorer.cs @@ -0,0 +1,392 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Diagnostics; +using System.Linq; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + /// + /// Associates a model object with it's corresponding . + /// + [DebuggerDisplay("DeclaredType={Metadata.ModelType.Name} PropertyName={Metadata.PropertyName}")] + public class ModelExplorer + { + private readonly IModelMetadataProvider _metadataProvider; + + private object _model; + private Func _modelAccessor; + private Type _modelType; + private List _properties; + + /// + /// Creates a new . + /// + /// The . + /// The . + /// The model object. May be null. + public ModelExplorer( + [NotNull] IModelMetadataProvider metadataProvider, + [NotNull] ModelMetadata metadata, + object model) + { + _metadataProvider = metadataProvider; + Metadata = metadata; + _model = model; + } + + /// + /// Creates a new . + /// + /// The . + /// The container . + /// The . + /// A model accessor function. May be null. + public ModelExplorer( + [NotNull] IModelMetadataProvider metadataProvider, + [NotNull] ModelExplorer container, + [NotNull] ModelMetadata metadata, + Func modelAccessor) + { + _metadataProvider = metadataProvider; + Container = container; + Metadata = metadata; + _modelAccessor = modelAccessor; + } + + /// + /// Creates a new . + /// + /// The . + /// The container . + /// The . + /// The model object. May be null. + public ModelExplorer( + [NotNull] IModelMetadataProvider metadataProvider, + [NotNull] ModelExplorer container, + [NotNull] ModelMetadata metadata, + object model) + { + _metadataProvider = metadataProvider; + Container = container; + Metadata = metadata; + _model = model; + } + + /// + /// Gets the container . + /// + /// + /// + /// The will most commonly be set as a result of calling + /// . In this case, the returned will + /// have it's set to the instance upon which + /// was called. + /// + /// + /// This however is not a requirement. The is informational, and may not + /// represent a type that defines the property represented by . This can + /// occur when constructing a based on evaluation of a complex + /// expression. + /// + /// + /// If calling code relies on a parent-child relationship between + /// instances, then use to validate this assumption. + /// + /// + public ModelExplorer Container { get; } + + /// + /// Gets the . + /// + public ModelMetadata Metadata { get; } + + /// + /// Gets the model object. + /// + /// + /// Retrieving the object will execute the model accessor function if this + /// was provided with one. + /// + public object Model + { + get + { + if (_model == null && _modelAccessor != null) + { + Debug.Assert(Container != null); + _model = _modelAccessor(Container.Model); + + // Null-out the accessor so we don't invoke it repeatedly if it returns null. + _modelAccessor = null; + } + + return _model; + } + } + + /// + /// Retrieving the will execute the model accessor function if this + /// was provided with one. + /// + public Type ModelType + { + get + { + if (_modelType == null) + { + if (Model == null) + { + // If the model is null, then use the declared model type; + _modelType = Metadata.ModelType; + } + else if (Metadata.IsNullableValueType) + { + // We have a model, but if it's a nullable value type, then Model.GetType() will return + // the non-nullable type (int? -> int). Since it's a value type, there's no subclassing, + // just go with the declared type. + _modelType = Metadata.ModelType; + } + else + { + // We have a model, and it's not a nullable so use the runtime type to handle + // cases where the model is a subclass of the declared type and has extra data. + _modelType = Model.GetType(); + } + } + + return _modelType; + } + } + + /// + /// Gets the properties. + /// + /// + /// Includes a for each property of the + /// for . + /// + public IEnumerable Properties + { + get + { + if (_properties == null) + { + _properties = new List(); + + var metadata = GetMetadataForRuntimeType(); + + var properties = Enumerable.Join( + metadata.Properties, + PropertyHelper.GetProperties(ModelType), + m => m.PropertyName, + ph => ph.Property.Name, + (m, ph) => CreateExplorerForProperty(m, ph)); + + _properties.AddRange(properties); + } + + return _properties; + } + } + + /// + /// Gets a for the given value. + /// + /// The model value. + /// A . + public ModelExplorer GetExplorerForModel(object model) + { + if (Container == null) + { + return new ModelExplorer(_metadataProvider, Metadata, model); + } + else + { + return new ModelExplorer(_metadataProvider, Container, Metadata, model); + } + } + + /// + /// Gets a for the property with given , or null if + /// the property cannot be found. + /// + /// The property name. + /// A , or null. + public ModelExplorer GetExplorerForProperty([NotNull] string name) + { + return Properties.FirstOrDefault(p => string.Equals( + p.Metadata.PropertyName, + name, + StringComparison.Ordinal)); + } + + /// + /// Gets a for the property with given , or null if + /// the property cannot be found. + /// + /// The property name. + /// An accessor for the model value. + /// A , or null. + /// + /// As this creates a model explorer with a specific model accessor function, the result is not cached. + /// + public ModelExplorer GetExplorerForProperty([NotNull] string name, Func modelAccessor) + { + var metadata = GetMetadataForRuntimeType(); + + var propertyMetadata = metadata.Properties[name]; + if (propertyMetadata == null) + { + return null; + } + + return new ModelExplorer(_metadataProvider, this, propertyMetadata, modelAccessor); + } + + /// + /// Gets a for the property with given , or null if + /// the property cannot be found. + /// + /// The property name. + /// The model value. + /// A , or null. + /// + /// As this creates a model explorer with a specific model value, the result is not cached. + /// + public ModelExplorer GetExplorerForProperty([NotNull] string name, object model) + { + var metadata = GetMetadataForRuntimeType(); + + var propertyMetadata = metadata.Properties[name]; + if (propertyMetadata == null) + { + return null; + } + + return new ModelExplorer(_metadataProvider, this, propertyMetadata, model); + } + + /// + /// Gets a for the provided model value and model . + /// + /// The model . + /// The model value. + /// A . + /// + /// + /// A created by + /// represents the result of executing an arbitrary expression against the model contained + /// in the current instance. + /// + /// + /// The returned will have the current instance set as its . + /// + /// + public ModelExplorer GetExplorerForExpression([NotNull] Type modelType, object model) + { + var metadata = _metadataProvider.GetMetadataForType(modelType); + return GetExplorerForExpression(metadata, model); + } + + /// + /// Gets a for the provided model value and model . + /// + /// The model . + /// The model value. + /// A . + /// + /// + /// A created by + /// + /// represents the result of executing an arbitrary expression against the model contained + /// in the current instance. + /// + /// + /// The returned will have the current instance set as its . + /// + /// + public ModelExplorer GetExplorerForExpression([NotNull] ModelMetadata metadata, object model) + { + return new ModelExplorer(_metadataProvider, this, metadata, model); + } + + /// + /// Gets a for the provided model value and model . + /// + /// The model . + /// The model value. + /// A . + /// + /// + /// A created by + /// + /// represents the result of executing an arbitrary expression against the model contained + /// in the current instance. + /// + /// + /// The returned will have the current instance set as its . + /// + /// + public ModelExplorer GetExplorerForExpression([NotNull] Type modelType, Func modelAccessor) + { + var metadata = _metadataProvider.GetMetadataForType(modelType); + return GetExplorerForExpression(metadata, modelAccessor); + } + + /// + /// Gets a for the provided model value and model . + /// + /// The model . + /// The model value. + /// A . + /// + /// + /// A created by + /// + /// represents the result of executing an arbitrary expression against the model contained + /// in the current instance. + /// + /// + /// The returned will have the current instance set as its . + /// + /// + public ModelExplorer GetExplorerForExpression([NotNull] ModelMetadata metadata, Func modelAccessor) + { + return new ModelExplorer(_metadataProvider, this, metadata, modelAccessor); + } + + private ModelMetadata GetMetadataForRuntimeType() + { + // We want to make sure we're looking at the runtime properties of the model, and for + // that we need the model metadata of the runtime type. + var metadata = Metadata; + if (Metadata.ModelType != ModelType) + { + metadata = _metadataProvider.GetMetadataForType(ModelType); + } + + return metadata; + } + + private ModelExplorer CreateExplorerForProperty( + ModelMetadata propertyMetadata, + PropertyHelper propertyHelper) + { + if (propertyHelper == null) + { + return new ModelExplorer(_metadataProvider, this, propertyMetadata, modelAccessor: null); + } + + var modelAccessor = new Func((c) => + { + return c == null ? null : propertyHelper.GetValue(c); + }); + + return new ModelExplorer(_metadataProvider, this, propertyMetadata, modelAccessor); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelExplorerExtensions.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelExplorerExtensions.cs new file mode 100644 index 0000000000..9325adc549 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelExplorerExtensions.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Linq; +using System.Globalization; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + /// + /// Extension methods for . + /// + public static class ModelExplorerExtensions + { + /// + /// Gets a simple display string for the property + /// of . + /// + /// The . + /// A simple display string for the model. + public static string GetSimpleDisplayText([NotNull] this ModelExplorer modelExplorer) + { + if (modelExplorer.Metadata.SimpleDisplayProperty != null) + { + var propertyExplorer = modelExplorer.GetExplorerForProperty( + modelExplorer.Metadata.SimpleDisplayProperty); + if (propertyExplorer?.Model != null) + { + return propertyExplorer.Model.ToString(); + } + } + + if (modelExplorer.Model == null) + { + return modelExplorer.Metadata.NullDisplayText; + } + + var stringResult = Convert.ToString(modelExplorer.Model, CultureInfo.CurrentCulture); + if (stringResult == null) + { + return string.Empty; + } + + if (!stringResult.Equals(modelExplorer.Model.GetType().FullName, StringComparison.Ordinal)) + { + return stringResult; + } + + var firstProperty = modelExplorer.Properties.FirstOrDefault(); + if (firstProperty == null) + { + return string.Empty; + } + + if (firstProperty.Model == null) + { + return firstProperty.Metadata.NullDisplayText; + } + + return Convert.ToString(firstProperty.Model, CultureInfo.CurrentCulture); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs index 927704e458..6fd2b77f71 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using Microsoft.AspNet.Mvc.ModelBinding.Internal; using Microsoft.Framework.Internal; @@ -25,24 +24,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private bool _showForDisplay = true; private bool _showForEdit = true; - private object _model; - private Func _modelAccessor; private int _order = DefaultOrder; private bool _isRequired; private ModelPropertyCollection _properties; - private Type _realModelType; - private string _simpleDisplayText; public ModelMetadata([NotNull] IModelMetadataProvider provider, Type containerType, - Func modelAccessor, [NotNull] Type modelType, string propertyName) { Provider = provider; _containerType = containerType; - _modelAccessor = modelAccessor; _modelType = modelType; _propertyName = propertyName; _isRequired = !modelType.AllowsNullValue(); @@ -70,12 +63,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// public virtual IBinderMetadata BinderMetadata { get; set; } - /// - /// A reference to the model's container . - /// Will be non-null if the model represents a property. - /// - public object Container { get; set; } - public Type ContainerType { get { return _containerType; } @@ -88,7 +75,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } /// - /// Gets or sets the name of the 's datatype. Overrides in some + /// Gets or sets the name of the Model's datatype. Overrides in some /// display scenarios. /// /// null unless set manually or through additional metadata e.g. attributes. @@ -98,7 +85,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// /// Gets or sets the composite format (see - /// http://msdn.microsoft.com/en-us/library/txafckwd.aspx) used to display the . + /// http://msdn.microsoft.com/en-us/library/txafckwd.aspx) used to display the Model. /// public virtual string DisplayFormatString { get; set; } @@ -106,7 +93,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// /// Gets or sets the composite format (see - /// http://msdn.microsoft.com/en-us/library/txafckwd.aspx) used to edit the . + /// http://msdn.microsoft.com/en-us/library/txafckwd.aspx) used to edit the Model. /// /// /// instances that set this property to a non-null, non-empty, @@ -195,26 +182,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding set { _order = value; } } - public object Model - { - get - { - if (_modelAccessor != null) - { - _model = _modelAccessor(); - _modelAccessor = null; - } - return _model; - } - set - { - _model = value; - _modelAccessor = null; - _properties = null; - _realModelType = null; - } - } - public Type ModelType { get { return _modelType; } @@ -231,7 +198,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { if (_properties == null) { - var properties = Provider.GetMetadataForProperties(Model, RealModelType); + var properties = Provider.GetMetadataForProperties(ModelType); _properties = new ModelPropertyCollection(properties.OrderBy(m => m.Order)); } @@ -250,46 +217,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding get { return _propertyName; } } - protected IModelMetadataProvider Provider { get; set; } + protected IModelMetadataProvider Provider { get; } - /// - /// Gets runtime of if is non-null and - /// is not ; otherwise. - /// - public Type RealModelType - { - get - { - if (_realModelType == null) - { - _realModelType = ModelType; - - // Don't call GetType() if the model is Nullable, because it will - // turn Nullable into T for non-null values - if (Model != null && !ModelType.IsNullableValueType()) - { - _realModelType = Model.GetType(); - } - } - - return _realModelType; - } - } - - public virtual string SimpleDisplayText - { - get - { - if (_simpleDisplayText == null) - { - _simpleDisplayText = ComputeSimpleDisplayText(); - } - - return _simpleDisplayText; - } - - set { _simpleDisplayText = value; } - } + /// + /// Gets or sets a value which is the name of the property used to display the model. + /// + public virtual string SimpleDisplayProperty { get; set; } /// /// Gets or sets a value that indicates whether the property should be displayed in read-only views. @@ -338,38 +271,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return DisplayName ?? PropertyName ?? ModelType.Name; } - protected virtual string ComputeSimpleDisplayText() - { - if (Model == null) - { - return NullDisplayText; - } - - var stringResult = Convert.ToString(Model, CultureInfo.CurrentCulture); - if (stringResult == null) - { - return string.Empty; - } - - if (!stringResult.Equals(Model.GetType().FullName, StringComparison.Ordinal)) - { - return stringResult; - } - - var firstProperty = Properties.FirstOrDefault(); - if (firstProperty == null) - { - return string.Empty; - } - - if (firstProperty.Model == null) - { - return firstProperty.NullDisplayText; - } - - return Convert.ToString(firstProperty.Model, CultureInfo.CurrentCulture); - } - + private static EfficientTypePropertyKey CreateCacheKey(Type containerType, Type modelType, string propertyName) diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadataProviderExtensions.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadataProviderExtensions.cs new file mode 100644 index 0000000000..c47e1d6ae0 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadataProviderExtensions.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + /// + /// Extensions methods for . + /// + public static class ModelMetadataProviderExtensions + { + /// + /// Gets a for the provided and + /// . + /// + /// The . + /// The declared of the model object. + /// The model object. + /// + public static ModelExplorer GetModelExplorerForType( + [NotNull] this IModelMetadataProvider provider, + [NotNull] Type modelType, + object model) + { + var modelMetadata = provider.GetMetadataForType(modelType); + return new ModelExplorer(provider, modelMetadata, model); + } + + /// + /// Gets a for property identified by the provided + /// and . + /// + /// The . + /// The for which the property is defined. + /// The property name. + /// A for the property. + public static ModelMetadata GetMetadataForProperty( + [NotNull] this IModelMetadataProvider provider, + [NotNull] Type containerType, + [NotNull] string propertyName) + { + var containerMetadata = provider.GetMetadataForType(containerType); + + var propertyMetadata = containerMetadata.Properties[propertyName]; + if (propertyMetadata == null) + { + var message = Resources.FormatCommon_PropertyNotFound(containerType, propertyName); + throw new ArgumentException(message, nameof(propertyName)); + } + + return propertyMetadata; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ModelBindingContext.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ModelBindingContext.cs index c57b77457a..fa9b0267a9 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ModelBindingContext.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ModelBindingContext.cs @@ -55,6 +55,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// public OperationBindingContext OperationBindingContext { get; set; } + /// + /// Gets or sets the model value for the current operation. + /// + /// + /// The will typically be set for a binding operation that works + /// against a pre-existing model object to update certain properties. + /// + public object Model { get; set; } + /// /// Gets or sets the metadata for the model associated with this context. /// diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/CompareAttributeAdapter.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/CompareAttributeAdapter.cs index cc8aecb201..5c8f0093aa 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/CompareAttributeAdapter.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/CompareAttributeAdapter.cs @@ -66,10 +66,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var otherPropertyDisplayName = OtherPropertyDisplayName; if (otherPropertyDisplayName == null && metadata.ContainerType != null) { - var otherProperty = context.MetadataProvider - .GetMetadataForProperty(() => metadata.Model, - metadata.ContainerType, - OtherProperty); + var otherProperty = context.MetadataProvider.GetMetadataForProperty( + metadata.ContainerType, + OtherProperty); if (otherProperty != null) { return otherProperty.GetDisplayName(); diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs index eefa31ec73..24d70780c4 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs @@ -25,17 +25,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding public IEnumerable Validate(ModelValidationContext validationContext) { - var metadata = validationContext.ModelMetadata; + var modelExplorer = validationContext.ModelExplorer; + var metadata = modelExplorer.Metadata; + var memberName = metadata.PropertyName ?? metadata.ModelType.Name; - var containerMetadata = validationContext.ContainerMetadata; - var container = containerMetadata != null ? containerMetadata.Model : null; - var context = new ValidationContext(container ?? metadata.Model) + var containerExplorer = modelExplorer.Container; + + var container = containerExplorer?.Model; + var context = new ValidationContext(container ?? modelExplorer.Model) { DisplayName = metadata.GetDisplayName(), MemberName = memberName }; - var result = Attribute.GetValidationResult(metadata.Model, context); + var result = Attribute.GetValidationResult(modelExplorer.Model, context); if (result != ValidationResult.Success) { // ModelValidationResult.MemberName is used by invoking validators (such as ModelValidator) to diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DefaultObjectValidator.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DefaultObjectValidator.cs index 7da1cae7b4..99eb7a253d 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DefaultObjectValidator.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DefaultObjectValidator.cs @@ -31,7 +31,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// public void Validate([NotNull] ModelValidationContext modelValidationContext) { - var metadata = modelValidationContext.ModelMetadata; + var modelExplorer = modelValidationContext.ModelExplorer; var validationContext = new ValidationContext() { ModelValidationContext = modelValidationContext, @@ -39,17 +39,23 @@ namespace Microsoft.AspNet.Mvc.ModelBinding }; ValidateNonVisitedNodeAndChildren( - modelValidationContext.RootPrefix, metadata, validationContext, validators: null); + modelValidationContext.RootPrefix, + modelExplorer, + validationContext, + validators: null); } - private bool ValidateNonVisitedNodeAndChildren(string modelKey, - ModelMetadata metadata, ValidationContext validationContext, IEnumerable validators) + private bool ValidateNonVisitedNodeAndChildren( + string modelKey, + ModelExplorer modelExplorer, + ValidationContext validationContext, + IEnumerable validators) { // Recursion guard to avoid stack overflows RuntimeHelpers.EnsureSufficientExecutionStack(); var modelState = validationContext.ModelValidationContext.ModelState; - var bindingSourceMetadata = metadata.BinderMetadata as IBindingSourceMetadata; + var bindingSourceMetadata = modelExplorer.Metadata.BinderMetadata as IBindingSourceMetadata; var bindingSource = bindingSourceMetadata?.BindingSource; if (bindingSource != null && !bindingSource.IsFromRequest) @@ -78,37 +84,38 @@ namespace Microsoft.AspNet.Mvc.ModelBinding // The validators are not null in the case of validating an array. Since the validators are // the same for all the elements of the array, we do not do GetValidators for each element, // instead we just pass them over. See ValidateElements function. - validators = validationContext.ModelValidationContext.ValidatorProvider.GetValidators(metadata); + var validatorProvider = validationContext.ModelValidationContext.ValidatorProvider; + validators = validatorProvider.GetValidators(modelExplorer.Metadata); } // We don't need to recursively traverse the graph for null values - if (metadata.Model == null) + if (modelExplorer.Model == null) { - return ShallowValidate(modelKey, metadata, validationContext, validators); + return ShallowValidate(modelKey, modelExplorer, validationContext, validators); } // We don't need to recursively traverse the graph for types that shouldn't be validated - var modelType = metadata.Model.GetType(); + var modelType = modelExplorer.Model.GetType(); if (IsTypeExcludedFromValidation(_excludeFilterProvider.ExcludeFilters, modelType)) { - var result = ShallowValidate(modelKey, metadata, validationContext, validators); - MarkPropertiesAsSkipped(modelKey, metadata, validationContext); + var result = ShallowValidate(modelKey, modelExplorer, validationContext, validators); + MarkPropertiesAsSkipped(modelKey, modelExplorer.Metadata, validationContext); return result; } // Check to avoid infinite recursion. This can happen with cycles in an object graph. - if (validationContext.Visited.Contains(metadata.Model)) + if (validationContext.Visited.Contains(modelExplorer.Model)) { return true; } - validationContext.Visited.Add(metadata.Model); + validationContext.Visited.Add(modelExplorer.Model); // Validate the children first - depth-first traversal - var enumerableModel = metadata.Model as IEnumerable; + var enumerableModel = modelExplorer.Model as IEnumerable; if (enumerableModel == null) { - isValid = ValidateProperties(modelKey, metadata, validationContext); + isValid = ValidateProperties(modelKey, modelExplorer, validationContext); } else { @@ -118,11 +125,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding if (isValid) { // Don't bother to validate this node if children failed. - isValid = ShallowValidate(modelKey, metadata, validationContext, validators); + isValid = ShallowValidate(modelKey, modelExplorer, validationContext, validators); } // Pop the object so that it can be validated again in a different path - validationContext.Visited.Remove(metadata.Model); + validationContext.Visited.Remove(modelExplorer.Model); return isValid; } @@ -151,15 +158,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } } - private bool ValidateProperties(string currentModelKey, ModelMetadata metadata, ValidationContext validationContext) + private bool ValidateProperties(string currentModelKey, ModelExplorer modelExplorer, ValidationContext validationContext) { var isValid = true; - foreach (var childMetadata in metadata.Properties) + foreach (var property in modelExplorer.Metadata.Properties) { - var propertyName = childMetadata.BinderModelName ?? childMetadata.PropertyName; - var childKey = ModelBindingHelper.CreatePropertyModelName(currentModelKey, propertyName); - if (!ValidateNonVisitedNodeAndChildren(childKey, childMetadata, validationContext, validators: null)) + var propertyExplorer = modelExplorer.GetExplorerForProperty(property.PropertyName); + var propertyMetadata = propertyExplorer.Metadata; + + var propertyBindingName = propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName; + var childKey = ModelBindingHelper.CreatePropertyModelName(currentModelKey, propertyBindingName); + if (!ValidateNonVisitedNodeAndChildren( + childKey, + propertyExplorer, + validationContext, + validators: null)) { isValid = false; } @@ -171,9 +185,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private bool ValidateElements(string currentKey, IEnumerable model, ValidationContext validationContext) { var elementType = GetElementType(model.GetType()); - var elementMetadata = _modelMetadataProvider.GetMetadataForType( - modelAccessor: null, - modelType: elementType); + var elementMetadata = _modelMetadataProvider.GetMetadataForType(elementType); var validators = validationContext.ModelValidationContext.ValidatorProvider.GetValidators(elementMetadata); @@ -189,9 +201,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding // If it's null, then a shallow validation will be performed. if (element != null || anyValidatorsDefined) { - elementMetadata.Model = element; + var elementExplorer = new ModelExplorer(_modelMetadataProvider, elementMetadata, element); var elementKey = ModelBindingHelper.CreateIndexModelName(currentKey, index); - if (!ValidateNonVisitedNodeAndChildren(elementKey, elementMetadata, validationContext, validators)) + if (!ValidateNonVisitedNodeAndChildren(elementKey, elementExplorer, validationContext, validators)) { isValid = false; } @@ -207,7 +219,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding // Returns true if validation passes successfully private static bool ShallowValidate( string modelKey, - ModelMetadata metadata, + ModelExplorer modelExplorer, ValidationContext validationContext, IEnumerable validators) { @@ -219,7 +231,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding if (validatorsAsCollection == null || validatorsAsCollection.Count > 0) { var modelValidationContext = - new ModelValidationContext(validationContext.ModelValidationContext, metadata); + new ModelValidationContext(validationContext.ModelValidationContext, modelExplorer); var modelState = validationContext.ModelValidationContext.ModelState; var modelValidationState = modelState.GetValidationState(modelKey); var fieldValidationState = modelState.GetFieldValidationState(modelKey); @@ -298,4 +310,4 @@ namespace Microsoft.AspNet.Mvc.ModelBinding public HashSet Visited { get; set; } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationContext.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationContext.cs index 8ab0cc9ee6..be35ba033c 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationContext.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationContext.cs @@ -8,42 +8,40 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { public class ModelValidationContext { - public ModelValidationContext([NotNull] ModelBindingContext bindingContext, - [NotNull] ModelMetadata metadata) + public ModelValidationContext( + [NotNull] ModelBindingContext bindingContext, + [NotNull] ModelExplorer modelExplorer) : this(bindingContext.ModelName, bindingContext.OperationBindingContext.ValidatorProvider, bindingContext.ModelState, - metadata, - bindingContext.ModelMetadata) + modelExplorer) { } - public ModelValidationContext(string rootPrefix, - [NotNull] IModelValidatorProvider validatorProvider, - [NotNull] ModelStateDictionary modelState, - [NotNull] ModelMetadata metadata, - ModelMetadata containerMetadata) + public ModelValidationContext( + string rootPrefix, + [NotNull] IModelValidatorProvider validatorProvider, + [NotNull] ModelStateDictionary modelState, + [NotNull] ModelExplorer modelExplorer) { - ModelMetadata = metadata; ModelState = modelState; RootPrefix = rootPrefix; ValidatorProvider = validatorProvider; - ContainerMetadata = containerMetadata; + ModelExplorer = modelExplorer; } - public ModelValidationContext([NotNull] ModelValidationContext parentContext, - [NotNull] ModelMetadata metadata) + public ModelValidationContext( + [NotNull] ModelValidationContext parentContext, + [NotNull] ModelExplorer modelExplorer) { - ModelMetadata = metadata; - ContainerMetadata = parentContext.ModelMetadata; + ModelExplorer = modelExplorer; ModelState = parentContext.ModelState; RootPrefix = parentContext.RootPrefix; ValidatorProvider = parentContext.ValidatorProvider; } - public ModelMetadata ModelMetadata { get; } - public ModelMetadata ContainerMetadata { get; } + public ModelExplorer ModelExplorer { get; } public ModelStateDictionary ModelState { get; } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ValidatableObjectAdapter.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ValidatableObjectAdapter.cs index 31ca66abd9..0f99b90ce1 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ValidatableObjectAdapter.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ValidatableObjectAdapter.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding public IEnumerable Validate(ModelValidationContext context) { - var model = context.ModelMetadata.Model; + var model = context.ModelExplorer.Model; if (model == null) { return Enumerable.Empty(); diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorPageOfT.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPageOfT.cs index 94341c2ed3..d7e5546618 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorPageOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPageOfT.cs @@ -48,14 +48,14 @@ namespace Microsoft.AspNet.Mvc.Razor } var name = ExpressionHelper.GetExpressionText(expression); - var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, _provider); - if (metadata == null) + var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, _provider); + if (modelExplorer == null) { throw new InvalidOperationException( Resources.FormatRazorPage_NullModelMetadata(nameof(IModelMetadataProvider), name)); } - return new ModelExpression(name, metadata); + return new ModelExpression(name, modelExplorer); } } } diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/InputTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/InputTagHelper.cs index 69762d5a5f..9410f36fff 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/InputTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/InputTagHelper.cs @@ -137,6 +137,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // Note null or empty For.Name is allowed because TemplateInfo.HtmlFieldPrefix may be sufficient. // IHtmlGenerator will enforce name requirements. var metadata = For.Metadata; + var modelExplorer = For.ModelExplorer; if (metadata == null) { throw new InvalidOperationException(Resources.FormatTagHelpers_NoProvidedMetadata( @@ -151,7 +152,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers if (string.IsNullOrEmpty(InputTypeName)) { // Note GetInputType never returns null. - inputType = GetInputType(metadata, out inputTypeHint); + inputType = GetInputType(modelExplorer, out inputTypeHint); } else { @@ -169,15 +170,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers switch (inputType) { case "checkbox": - GenerateCheckBox(metadata, output); + GenerateCheckBox(modelExplorer, output); return; case "hidden": tagBuilder = Generator.GenerateHidden( ViewContext, - metadata, + modelExplorer, For.Name, - value: metadata.Model, + value: For.Model, useViewData: false, htmlAttributes: null); break; @@ -185,18 +186,18 @@ namespace Microsoft.AspNet.Mvc.TagHelpers case "password": tagBuilder = Generator.GeneratePassword( ViewContext, - metadata, + modelExplorer, For.Name, value: null, htmlAttributes: null); break; case "radio": - tagBuilder = GenerateRadio(metadata); + tagBuilder = GenerateRadio(modelExplorer); break; default: - tagBuilder = GenerateTextBox(metadata, inputTypeHint, inputType); + tagBuilder = GenerateTextBox(modelExplorer, inputTypeHint, inputType); break; } @@ -210,14 +211,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers } } - private void GenerateCheckBox(ModelMetadata metadata, TagHelperOutput output) + private void GenerateCheckBox(ModelExplorer modelExplorer, TagHelperOutput output) { - if (typeof(bool) != metadata.RealModelType) + if (typeof(bool) != modelExplorer.ModelType) { throw new InvalidOperationException(Resources.FormatInputTagHelper_InvalidExpressionResult( "", ForAttributeName, - metadata.RealModelType.FullName, + modelExplorer.ModelType.FullName, typeof(bool).FullName, "type", "checkbox")); @@ -230,7 +231,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var tagBuilder = Generator.GenerateCheckBox( ViewContext, - metadata, + modelExplorer, For.Name, isChecked: null, htmlAttributes: htmlAttributes); @@ -243,7 +244,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers output.Content += tagBuilder.ToString(TagRenderMode.SelfClosing); - tagBuilder = Generator.GenerateHiddenForCheckbox(ViewContext, metadata, For.Name); + tagBuilder = Generator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, For.Name); if (tagBuilder != null) { output.Content += tagBuilder.ToString(TagRenderMode.SelfClosing); @@ -251,7 +252,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers } } - private TagBuilder GenerateRadio(ModelMetadata metadata) + private TagBuilder GenerateRadio(ModelExplorer modelExplorer) { // Note empty string is allowed. if (Value == null) @@ -265,38 +266,38 @@ namespace Microsoft.AspNet.Mvc.TagHelpers return Generator.GenerateRadioButton( ViewContext, - metadata, + modelExplorer, For.Name, Value, isChecked: null, htmlAttributes: null); } - private TagBuilder GenerateTextBox(ModelMetadata metadata, string inputTypeHint, string inputType) + private TagBuilder GenerateTextBox(ModelExplorer modelExplorer, string inputTypeHint, string inputType) { var format = Format; if (string.IsNullOrEmpty(format)) { - format = GetFormat(metadata, inputTypeHint, inputType); + format = GetFormat(modelExplorer, inputTypeHint, inputType); } return Generator.GenerateTextBox( ViewContext, - metadata, + modelExplorer, For.Name, - value: metadata.Model, + value: modelExplorer.Model, format: Format, htmlAttributes: null); } // Get a fall-back format based on the metadata. - private string GetFormat(ModelMetadata metadata, string inputTypeHint, string inputType) + private string GetFormat(ModelExplorer modelExplorer, string inputTypeHint, string inputType) { string format; string rfc3339Format; if (string.Equals("decimal", inputTypeHint, StringComparison.OrdinalIgnoreCase) && string.Equals("text", inputType, StringComparison.Ordinal) && - string.IsNullOrEmpty(metadata.EditFormatString)) + string.IsNullOrEmpty(modelExplorer.Metadata.EditFormatString)) { // Decimal data is edited using an element, with a reasonable format. // EditFormatString has precedence over this fall-back format. @@ -304,8 +305,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers } else if (_rfc3339Formats.TryGetValue(inputType, out rfc3339Format) && ViewContext.Html5DateRenderingMode == Html5DateRenderingMode.Rfc3339 && - !metadata.HasNonDefaultEditFormat && - (typeof(DateTime) == metadata.RealModelType || typeof(DateTimeOffset) == metadata.RealModelType)) + !modelExplorer.Metadata.HasNonDefaultEditFormat && + (typeof(DateTime) == modelExplorer.ModelType || typeof(DateTimeOffset) == modelExplorer.ModelType)) { // Rfc3339 mode _may_ override EditFormatString in a limited number of cases e.g. EditFormatString // must be a default format (i.e. came from a built-in [DataType] attribute). @@ -314,15 +315,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers else { // Otherwise use EditFormatString, if any. - format = metadata.EditFormatString; + format = modelExplorer.Metadata.EditFormatString; } return format; } - private string GetInputType(ModelMetadata metadata, out string inputTypeHint) + private string GetInputType(ModelExplorer modelExplorer, out string inputTypeHint) { - foreach (var hint in GetInputTypeHints(metadata)) + foreach (var hint in GetInputTypeHints(modelExplorer)) { string inputType; if (_defaultInputTypes.TryGetValue(hint, out inputType)) @@ -337,12 +338,12 @@ namespace Microsoft.AspNet.Mvc.TagHelpers } // A variant of TemplateRenderer.GetViewNames(). Main change relates to bool? handling. - private static IEnumerable GetInputTypeHints(ModelMetadata metadata) + private static IEnumerable GetInputTypeHints(ModelExplorer modelExplorer) { var inputTypeHints = new string[] { - metadata.TemplateHint, - metadata.DataTypeName, + modelExplorer.Metadata.TemplateHint, + modelExplorer.Metadata.DataTypeName, }; foreach (string inputTypeHint in inputTypeHints.Where(s => !string.IsNullOrEmpty(s))) @@ -352,7 +353,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // In most cases, we don't want to search for Nullable. We want to search for T, which should handle // both T and Nullable. However we special-case bool? to avoid turning an into a