// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Mvc.ViewFeatures { public class ViewDataDictionary : IDictionary { private readonly IDictionary _data; private readonly Type _declaredModelType; private readonly IModelMetadataProvider _metadataProvider; /// /// Initializes a new instance of the class. /// /// /// instance used to create /// instances. /// /// instance for this scope. /// For use when creating a for a new top-level scope. public ViewDataDictionary( IModelMetadataProvider metadataProvider, 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. /// /// /// This constructor should not be used in any context where may be set to a value /// incompatible with the declared type of . /// /// public ViewDataDictionary(ViewDataDictionary source) : this(source, source.Model, source._declaredModelType) { } /// /// Initializes a new instance of the class. /// /// /// instance used to create /// instances. /// /// Internal for testing. internal ViewDataDictionary(IModelMetadataProvider metadataProvider) : this(metadataProvider, new ModelStateDictionary()) { } /// /// Initializes a new instance of the class. /// /// /// instance used to create /// instances. /// /// /// of values expected. Used to set . /// /// /// For use when creating a derived for a new top-level scope. /// protected ViewDataDictionary( IModelMetadataProvider metadataProvider, Type declaredModelType) : this(metadataProvider, new ModelStateDictionary(), declaredModelType) { } /// /// Initializes a new instance of the class. /// /// /// instance used to create /// instances. /// /// instance for this scope. /// /// of values expected. Used to set . /// /// /// For use when creating a derived for a new top-level scope. /// // This is the core constructor called when Model is unknown. protected ViewDataDictionary( IModelMetadataProvider metadataProvider, ModelStateDictionary modelState, Type declaredModelType) : this(metadataProvider, modelState, declaredModelType, data: new Dictionary(StringComparer.OrdinalIgnoreCase), templateInfo: new TemplateInfo()) { if (metadataProvider == null) { throw new ArgumentNullException(nameof(metadataProvider)); } if (modelState == null) { throw new ArgumentNullException(nameof(modelState)); } if (declaredModelType == null) { throw new ArgumentNullException(nameof(declaredModelType)); } // Base ModelMetadata on the declared type. ModelExplorer = _metadataProvider.GetModelExplorerForType(declaredModelType, model: null); } /// /// Initializes a new instance of the class based in part on an existing /// instance. /// /// instance to copy initial values from. /// /// of values expected. Used to set . /// /// /// /// 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(ViewDataDictionary source, Type declaredModelType) : this(source, model: source.Model, declaredModelType: 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. /// /// of values expected. Used to set . /// /// /// /// 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 /// . /// /// // This is the core constructor called when Model is known. protected ViewDataDictionary(ViewDataDictionary source, object model, Type declaredModelType) : this(source._metadataProvider, source.ModelState, declaredModelType, data: new CopyOnWriteDictionary(source, StringComparer.OrdinalIgnoreCase), templateInfo: new TemplateInfo(source.TemplateInfo)) { if (source == null) { throw new ArgumentNullException(nameof(source)); } // A non-null Model must always be assignable to both _declaredModelType and ModelMetadata.ModelType. // // ModelMetadata.ModelType should also be assignable to _declaredModelType. Though corner cases exist such // as a ViewDataDictionary> holding information about an IEnumerable property (because an // @model directive matched the runtime type though the view's name did not), we'll throw away the property // metadata in those cases -- preserving invariant that ModelType can be assigned to _declaredModelType. // // More generally, since defensive copies to base VDD and VDD abound, it's important to preserve // metadata despite _declaredModelType changes. var modelType = model?.GetType(); var modelOrDeclaredType = modelType ?? declaredModelType; if (source.ModelMetadata.MetadataKind == ModelMetadataKind.Type && source.ModelMetadata.ModelType == typeof(object) && modelOrDeclaredType != typeof(object)) { // Base ModelMetadata on new type when there's no property information to preserve and type changes to // something besides typeof(object). ModelExplorer = _metadataProvider.GetModelExplorerForType(modelOrDeclaredType, model); } else if (!declaredModelType.IsAssignableFrom(source.ModelMetadata.ModelType)) { // Base ModelMetadata on new type when existing metadata is incompatible with the new declared type. ModelExplorer = _metadataProvider.GetModelExplorerForType(modelOrDeclaredType, model); } else if (modelType != null && !source.ModelMetadata.ModelType.IsAssignableFrom(modelType)) { // Base ModelMetadata on new type when new model is incompatible with the existing metadata. ModelExplorer = _metadataProvider.GetModelExplorerForType(modelType, model); } else if (object.ReferenceEquals(model, source.ModelExplorer.Model)) { // Source's ModelExplorer is already exactly correct. ModelExplorer = source.ModelExplorer; } else { // The existing metadata is compatible with the value and declared type but it's a new value. ModelExplorer = new ModelExplorer( _metadataProvider, source.ModelExplorer.Container, source.ModelMetadata, model); } // Ensure the given Model is compatible with _declaredModelType. Do not do this one of the following // special cases: // - Constructing a ViewDataDictionary where TModel is a non-Nullable value type. This may for // example occur when activating a RazorPage and the container is null. // - Constructing a ViewDataDictionary immediately before overwriting ModelExplorer with correct // information. See TemplateBuilder.Build(). if (model != null) { EnsureCompatible(model); } } private ViewDataDictionary( IModelMetadataProvider metadataProvider, ModelStateDictionary modelState, Type declaredModelType, IDictionary data, TemplateInfo templateInfo) { _metadataProvider = metadataProvider; ModelState = modelState; _declaredModelType = declaredModelType; _data = data; TemplateInfo = templateInfo; } /// /// Gets or sets the current model. /// public object Model { get { return ModelExplorer.Model; } set { // Reset ModelExplorer to ensure Model and ModelExplorer.Model remain equal. SetModel(value); } } /// /// Gets the . /// public ModelStateDictionary ModelState { get; } /// /// Gets the for an expression, the (if /// non-null), or the declared . /// /// /// Value is never null but may describe the class in some cases. This may for /// example occur in controllers. /// public ModelMetadata ModelMetadata { get { return ModelExplorer.Metadata; } } /// /// Gets or sets the for the . /// public ModelExplorer ModelExplorer { get; set; } /// /// Gets the . /// public TemplateInfo TemplateInfo { get; } #region IDictionary properties /// // Do not just pass through to _data: Indexer should not throw a KeyNotFoundException. public object this[string index] { get { object result; _data.TryGetValue(index, out result); return result; } set { _data[index] = value; } } /// public int Count { get { return _data.Count; } } /// public bool IsReadOnly { get { return _data.IsReadOnly; } } /// public ICollection Keys { get { return _data.Keys; } } /// public ICollection Values { get { return _data.Values; } } #endregion // for unit testing internal IDictionary Data { get { return _data; } } /// /// Gets value of named in this . /// /// Expression name, relative to the current model. /// Value of named in this . /// /// Looks up in the dictionary first. Falls back to evaluating it against /// . /// public object Eval(string expression) { var info = GetViewDataInfo(expression); return info?.Value; } /// /// Gets value of named in this , formatted /// using given . /// /// Expression name, relative to the current model. /// /// The format string (see https://msdn.microsoft.com/en-us/library/txafckwd.aspx). /// /// /// Value of named in this , formatted using /// given . /// /// /// Looks up in the dictionary first. Falls back to evaluating it against /// . /// public string Eval(string expression, string format) { var value = Eval(expression); return FormatValue(value, format); } /// /// Formats the given using given . /// /// The value to format. /// /// The format string (see https://msdn.microsoft.com/en-us/library/txafckwd.aspx). /// /// The formatted . public static string FormatValue(object value, string format) { if (value == null) { return string.Empty; } if (string.IsNullOrEmpty(format)) { return Convert.ToString(value, CultureInfo.CurrentCulture); } else { return string.Format(CultureInfo.CurrentCulture, format, value); } } /// /// Gets for named in this /// . /// /// Expression name, relative to the current model. /// /// for named in this /// . /// /// /// Looks up in the dictionary first. Falls back to evaluating it against /// . /// public ViewDataInfo GetViewDataInfo(string expression) { return ViewDataEvaluator.Eval(this, expression); } /// /// Set to ensure and /// reflect the new . /// /// New value. protected virtual void SetModel(object value) { // Update ModelExplorer to reflect the new value. When possible, preserve ModelMetadata to avoid losing // property information. var modelType = value?.GetType(); if (ModelMetadata.MetadataKind == ModelMetadataKind.Type && ModelMetadata.ModelType == typeof(object) && modelType != null && modelType != typeof(object)) { // Base ModelMetadata on new type when there's no property information to preserve and type changes to // something besides typeof(object). ModelExplorer = _metadataProvider.GetModelExplorerForType(modelType, value); } else if (modelType != null && !ModelMetadata.ModelType.IsAssignableFrom(modelType)) { // Base ModelMetadata on new type when new model is incompatible with the existing metadata. The most // common case is _declaredModelType==typeof(object), metadata was copied from another VDD, and user // code sets the Model to a new type e.g. within a view component or a view that lacks an @model // directive. ModelExplorer = _metadataProvider.GetModelExplorerForType(modelType, value); } else if (object.ReferenceEquals(value, Model)) { // The metadata matches and the model is literally the same; usually nothing to do here. if (value == null && !ModelMetadata.IsReferenceOrNullableType && _declaredModelType != ModelMetadata.ModelType) { // Base ModelMetadata on declared type when setting Model to null, source VDD's Model was never // set, and source VDD had a non-Nullable value type. Though _declaredModelType might also be a // non-Nullable value type, would need to duplicate logic behind // ModelMetadata.IsReferenceOrNullableType to avoid this allocation in the error case. ModelExplorer = _metadataProvider.GetModelExplorerForType(_declaredModelType, value); } } else { // The existing metadata is compatible with the value but it's a new value. ModelExplorer = new ModelExplorer(_metadataProvider, ModelExplorer.Container, ModelMetadata, value); } EnsureCompatible(value); } // Throw if given value is incompatible with the declared Model Type. 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 = IsCompatibleWithDeclaredType(value); if (!castWillSucceed) { string message; if (value == null) { message = Resources.FormatViewData_ModelCannotBeNull(_declaredModelType); } else { message = Resources.FormatViewData_WrongTModelType(value.GetType(), _declaredModelType); } throw new InvalidOperationException(message); } } // Call after updating the ModelExplorer because this uses both _declaredModelType and ModelMetadata. May // otherwise get incorrect compatibility errors. private bool IsCompatibleWithDeclaredType(object value) { if (value == null) { // In this case ModelMetadata.ModelType matches _declaredModelType. return ModelMetadata.IsReferenceOrNullableType; } else { return _declaredModelType.IsAssignableFrom(value.GetType()); } } #region IDictionary methods /// public void Add(string key, object value) { if (key == null) { throw new ArgumentNullException(nameof(key)); } _data.Add(key, value); } /// public bool ContainsKey(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } return _data.ContainsKey(key); } /// public bool Remove(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } return _data.Remove(key); } /// public bool TryGetValue(string key, out object value) { if (key == null) { throw new ArgumentNullException(nameof(key)); } return _data.TryGetValue(key, out value); } /// public void Add(KeyValuePair item) { _data.Add(item); } /// public void Clear() { _data.Clear(); } /// public bool Contains(KeyValuePair item) { return _data.Contains(item); } /// public void CopyTo(KeyValuePair[] array, int arrayIndex) { if (array == null) { throw new ArgumentNullException(nameof(array)); } _data.CopyTo(array, arrayIndex); } /// public bool Remove(KeyValuePair item) { return _data.Remove(item); } /// IEnumerator> IEnumerable>.GetEnumerator() { return _data.GetEnumerator(); } /// IEnumerator IEnumerable.GetEnumerator() { return _data.GetEnumerator(); } #endregion } }