diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/AssemblyInfo.cs index c7cb07590a..5d995b03fd 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/AssemblyInfo.cs @@ -4,4 +4,6 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.AspNet.Mvc.Core.Test")] +[assembly: InternalsVisibleTo("Microsoft.AspNet.Mvc.Razor.Test")] +[assembly: InternalsVisibleTo("Microsoft.AspNet.Mvc.TagHelpers.Test")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewDataDictionary.cs b/src/Microsoft.AspNet.Mvc.Core/ViewDataDictionary.cs index eca162da51..897f0b370f 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ViewDataDictionary.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ViewDataDictionary.cs @@ -14,78 +14,238 @@ namespace Microsoft.AspNet.Mvc public class ViewDataDictionary : IDictionary { private readonly IDictionary _data; + private readonly Type _declaredModelType; + private readonly IModelMetadataProvider _metadataProvider; + private object _model; private ModelMetadata _modelMetadata; - private IModelMetadataProvider _metadataProvider; - public ViewDataDictionary([NotNull] IModelMetadataProvider metadataProvider) + /// + /// Initializes a new instance of the class. + /// + /// + /// instance used to calculate + /// values. + /// + /// instance for this scope. + /// For use when creating a for a new top-level scope. + public ViewDataDictionary([NotNull] IModelMetadataProvider metadataProvider, + [NotNull] ModelStateDictionary modelState) + : this(metadataProvider, modelState, declaredModelType: typeof(object)) + { + } + + /// + /// Initializes a new instance of the class based entirely on an existing + /// instance. + /// + /// instance to copy initial values from. + /// + /// For use when copying a instance and the declared + /// will not change e.g. when copying from a + /// instance to a base instance. + /// + public ViewDataDictionary([NotNull] ViewDataDictionary source) + : this(source, source.Model, source._declaredModelType) + { + } + + /// + /// Initializes a new instance of the class based in part on an existing + /// instance. This constructor is careful to avoid exceptions may throw when + /// is null. + /// + /// instance to copy initial values from. + /// Value for the property. + /// + /// For use when the new instance's declared is unknown but its + /// is known. In this case, is the best possible guess about the + /// declared type when is null. + /// + public ViewDataDictionary([NotNull] ViewDataDictionary source, object model) + : this(source, model, declaredModelType: typeof(object)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// instance used to calculate + /// values. + /// + /// Internal for testing. + internal ViewDataDictionary([NotNull] IModelMetadataProvider metadataProvider) : this(metadataProvider, new ModelStateDictionary()) { } - public ViewDataDictionary([NotNull] IModelMetadataProvider metadataProvider, - [NotNull] ModelStateDictionary modelState) + /// + /// Initializes a new instance of the class. + /// + /// + /// instance used to calculate + /// values. + /// + /// + /// of values expected. Used to set + /// when is null. + /// + /// + /// For use when creating a derived for a new top-level scope. + /// + protected ViewDataDictionary( + [NotNull] IModelMetadataProvider metadataProvider, + [NotNull] Type declaredModelType) + : this(metadataProvider, new ModelStateDictionary(), declaredModelType) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// instance used to calculate + /// values. + /// + /// instance for this scope. + /// + /// of values expected. Used to set + /// when is null. + /// + /// + /// For use when creating a derived for a new top-level scope. + /// + protected ViewDataDictionary( + [NotNull] IModelMetadataProvider metadataProvider, + [NotNull] ModelStateDictionary modelState, + [NotNull] Type declaredModelType) : this(metadataProvider, - modelState: modelState, + modelState, + declaredModelType, data: new Dictionary(StringComparer.OrdinalIgnoreCase), templateInfo: new TemplateInfo()) { + // This is the core constructor called when Model is unknown. Base ModelMetadata on the declared type. + ModelMetadata = _metadataProvider.GetMetadataForType(() => null, _declaredModelType); } /// - /// copy constructor for use when model type does not change or caller will - /// immediately set the property. + /// Initializes a new instance of the class based in part on an existing + /// instance. /// - public ViewDataDictionary([NotNull] ViewDataDictionary source) - : this(source, source.Model) + /// instance to copy initial values from. + /// + /// of values expected. Used to set + /// when is null. + /// + /// + /// + /// For use when copying a instance and new instance's declared + /// is known but should be copied from the existing + /// instance e.g. when copying from a base instance to a + /// instance. + /// + /// + /// This constructor may throw if source.Model is non-null and incompatible with + /// . Pass model: null to + /// to ignore source.Model. + /// + /// + protected ViewDataDictionary([NotNull] ViewDataDictionary source, Type declaredModelType) + : this(source, model: source.Model, declaredModelType: declaredModelType) { } /// - /// copy constructor for use when model type may change. This avoids - /// exceptions a derived class may throw when is called. + /// Initializes a new instance of the class based in part on an existing + /// instance. This constructor is careful to avoid exceptions may throw when + /// is null. /// - public ViewDataDictionary([NotNull] ViewDataDictionary source, object model) - : this(source.MetadataProvider, + /// instance to copy initial values from. + /// Value for the property. + /// + /// of values expected. Used to set + /// when is null. + /// + /// + /// + /// For use when copying a instance and new instance's declared + /// and are known. + /// + /// + /// This constructor may throw if is non-null and incompatible with + /// . + /// + /// + protected ViewDataDictionary([NotNull] ViewDataDictionary source, object model, Type declaredModelType) + : this(source._metadataProvider, new ModelStateDictionary(source.ModelState), - new CopyOnWriteDictionary(source, StringComparer.OrdinalIgnoreCase), - new TemplateInfo(source.TemplateInfo)) + declaredModelType, + data: new CopyOnWriteDictionary(source, StringComparer.OrdinalIgnoreCase), + templateInfo: new TemplateInfo(source.TemplateInfo)) { - // Avoid copying information about the object type. To do so when model==null would confuse the - // ViewDataDictionary.ModelMetadata getter. - if (source.ModelMetadata?.ModelType != typeof(object)) + // This is the core constructor called when Model is known. + var modelType = GetModelType(model); + if (modelType == source.ModelMetadata.ModelType && model == source.ModelMetadata.Model) { - _modelMetadata = source.ModelMetadata; + // 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; + } + else if (model == null) + { + // Ensure ModelMetadata is never null though SetModel() isn't called. + ModelMetadata = _metadataProvider.GetMetadataForType(() => null, _declaredModelType); } - // If we're constructing a derived ViewDataDictionary where TModel is a non-Nullable value type, - // SetModel will throw if we try to call it with null. We should not throw in that case. + // If we're constructing a ViewDataDictionary where TModel is a non-Nullable value type, + // SetModel() will throw if we try to call it with null. We should not throw in that case. if (model != null) { SetModel(model); } } - private ViewDataDictionary(IModelMetadataProvider metadataProvider, - ModelStateDictionary modelState, - IDictionary data, - TemplateInfo templateInfo) + private ViewDataDictionary( + IModelMetadataProvider metadataProvider, + ModelStateDictionary modelState, + Type declaredModelType, + IDictionary data, + TemplateInfo templateInfo) { _metadataProvider = metadataProvider; ModelState = modelState; + _declaredModelType = declaredModelType; _data = data; TemplateInfo = templateInfo; } public object Model { - get { return _model; } - set { SetModel(value); } + get + { + return _model; + } + set + { + // Reset ModelMetadata to ensure Model and ModelMetadata.Model remain equal. + _modelMetadata = null; + SetModel(value); + } } - public ModelStateDictionary ModelState { get; private set; } + public ModelStateDictionary ModelState { get; } - public virtual ModelMetadata ModelMetadata + /// + /// for the current value or the declared if + /// is null. + /// + /// + /// Value is never null but may describe the class in some cases. This may for + /// example occur in controllers if is null. + /// + public ModelMetadata ModelMetadata { get { @@ -93,19 +253,18 @@ namespace Microsoft.AspNet.Mvc } set { + if (value == null) + { + throw new ArgumentNullException(Resources.FormatPropertyOfTypeCannotBeNull( + nameof(ViewDataDictionary.ModelMetadata), + nameof(ViewDataDictionary))); + } + _modelMetadata = value; } } - public TemplateInfo TemplateInfo { get; private set; } - - /// - /// Provider for subclasses that need it to override . - /// - protected IModelMetadataProvider MetadataProvider - { - get { return _metadataProvider; } - } + public TemplateInfo TemplateInfo { get; } #region IDictionary properties // Do not just pass through to _data: Indexer should not throw a KeyNotFoundException. @@ -194,19 +353,46 @@ namespace Microsoft.AspNet.Mvc // enough so as not to depend on the "this" pointer referencing a fully constructed object. protected virtual void SetModel(object value) { + EnsureCompatible(value); _model = value; - if (value == null) + + // 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) { - // Unable to determine model metadata. - _modelMetadata = null; + ModelMetadata = _metadataProvider.GetMetadataForType(() => value, modelType); } - else if (_modelMetadata == null || value.GetType() != ModelMetadata.ModelType) + } + + // Throw if given value is incompatible with the declared Model Type. + private void EnsureCompatible(object value) + { + // IsCompatibleObject verifies if the value is either an instance of _declaredModelType or (if value is + // null) that _declaredModelType is a nullable type. + var castWillSucceed = _declaredModelType.IsCompatibleWith(value); + if (!castWillSucceed) { - // Reset or override model metadata based on new value type. - _modelMetadata = _metadataProvider.GetMetadataForType(() => value, value.GetType()); + string message; + if (value == null) + { + message = Resources.FormatViewData_ModelCannotBeNull(_declaredModelType); + } + else + { + message = Resources.FormatViewData_WrongTModelType(value.GetType(), _declaredModelType); + } + + throw new InvalidOperationException(message); } } + private Type GetModelType(object value) + { + return (value == null) ? _declaredModelType : value.GetType(); + } + #region IDictionary methods public void Add([NotNull] string key, object value) { diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewDataDictionaryOfT.cs b/src/Microsoft.AspNet.Mvc.Core/ViewDataDictionaryOfT.cs index ad5b2b81e1..8df9997a18 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ViewDataDictionaryOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ViewDataDictionaryOfT.cs @@ -1,88 +1,89 @@ // 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; namespace Microsoft.AspNet.Mvc { public class ViewDataDictionary : ViewDataDictionary { - // Fallback ModelMetadata based on TModel. Used when Model is null and base ViewDataDictionary class is unable - // to determine the correct metadata. - private readonly ModelMetadata _defaultModelMetadata; - + /// public ViewDataDictionary([NotNull] IModelMetadataProvider metadataProvider) - : base(metadataProvider) + : base(metadataProvider, declaredModelType: typeof(TModel)) { - _defaultModelMetadata = MetadataProvider.GetMetadataForType(null, typeof(TModel)); } - public ViewDataDictionary([NotNull] IModelMetadataProvider metadataProvider, + // References may not show up due to ITypeActivator use in RazorPageActivator. + /// + /// Initializes a new instance of the class. + /// + /// + /// For use when creating a for a new top-level scope. + /// + /// + public ViewDataDictionary( + [NotNull] IModelMetadataProvider metadataProvider, [NotNull] ModelStateDictionary modelState) - : base(metadataProvider, modelState) + : base(metadataProvider, modelState, declaredModelType: typeof(TModel)) { } + // References may not show up due to ITypeActivator use in RazorPageActivator. + /// + /// Initializes a new instance of the class based in part on an + /// existing instance. + /// + /// + /// + /// For use when copying a instance and is known + /// but should be copied from the existing instance e.g. when copying from a base + /// instance to a instance. + /// + /// + /// This constructor may throw if source.Model is non-null and incompatible with + /// . Pass model: null to + /// to ignore source.Model. + /// + /// /// public ViewDataDictionary([NotNull] ViewDataDictionary source) - : this(source, source.Model) + : base(source, declaredModelType: typeof(TModel)) { } + // Model parameter type is object to allow "model: null" calls even when TModel is a value type. A TModel + // parameter would likely require IEquatable type restrictions to pass expected null value to the base + // constructor. + /// + /// Initializes a new instance of the class based in part on an + /// existing instance. This constructor is careful to avoid exceptions + /// may throw when is null. + /// + /// + /// + /// For use when copying a instance and and + /// are known. + /// + /// + /// This constructor may throw if is non-null and incompatible with + /// . + /// + /// /// public ViewDataDictionary([NotNull] ViewDataDictionary source, object model) - : base(source, model) + : base(source, model, declaredModelType: typeof(TModel)) { - var original = source as ViewDataDictionary; - if (original != null) - { - _defaultModelMetadata = original._defaultModelMetadata; - } - else - { - _defaultModelMetadata = MetadataProvider.GetMetadataForType(null, typeof(TModel)); - } } public new TModel Model - { - get { return (TModel)base.Model; } - set { SetModel(value); } - } - - public override ModelMetadata ModelMetadata { get { - return base.ModelMetadata ?? _defaultModelMetadata; + return (base.Model == null) ? default(TModel) : (TModel)base.Model; } - } - - protected override void SetModel(object value) - { - // IsCompatibleObject verifies if the value is either an instance of TModel or (if value is null) that - // TModel is a nullable type. - var castWillSucceed = typeof(TModel).IsCompatibleWith(value); - - if (castWillSucceed) + set { - base.SetModel(value); - } - else - { - string message; - if (value == null) - { - message = Resources.FormatViewData_ModelCannotBeNull(typeof(TModel)); - } - else - { - message = Resources.FormatViewData_WrongTModelType(value.GetType(), typeof(TModel)); - } - - throw new InvalidOperationException(message); + base.Model = value; } } }