// 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; #if DNXCORE50 using System.Reflection; #endif using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.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 calculate /// values. /// /// 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)) { if (metadataProvider == null) { throw new ArgumentNullException(nameof(metadataProvider)); } if (modelState == null) { throw new ArgumentNullException(nameof(modelState)); } } /// /// 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(ViewDataDictionary source) : this(source, source.Model, source._declaredModelType) { if (source == null) { throw new ArgumentNullException(nameof(source)); } } /// /// Initializes a new instance of the class based in part on an existing /// instance. This constructor is careful to avoid exceptions may throw when /// is null. /// /// instance to copy initial values from. /// Value for the property. /// /// For use when the new instance's declared is unknown but its /// is known. In this case, is the best possible guess about the /// declared type when is null. /// public ViewDataDictionary(ViewDataDictionary source, object model) : this(source, model, declaredModelType: typeof(object)) { if (source == null) { throw new ArgumentNullException(nameof(source)); } } /// /// Initializes a new instance of the class. /// /// /// instance used to calculate /// values. /// /// Internal for testing. internal ViewDataDictionary(IModelMetadataProvider metadataProvider) : this(metadataProvider, new ModelStateDictionary()) { if (metadataProvider == null) { throw new ArgumentNullException(nameof(metadataProvider)); } } /// /// 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( IModelMetadataProvider metadataProvider, Type declaredModelType) : this(metadataProvider, new ModelStateDictionary(), declaredModelType) { if (metadataProvider == null) { throw new ArgumentNullException(nameof(metadataProvider)); } if (declaredModelType == null) { throw new ArgumentNullException(nameof(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( 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)); } // This is the core constructor called when Model is unknown. 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 /// 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(ViewDataDictionary source, Type declaredModelType) : this(source, model: source.Model, declaredModelType: declaredModelType) { if (source == null) { throw new ArgumentNullException(nameof(source)); } } /// /// 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 /// 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(ViewDataDictionary source, object model, Type declaredModelType) : this(source._metadataProvider, new ModelStateDictionary(source.ModelState), declaredModelType, data: new CopyOnWriteDictionary(source, StringComparer.OrdinalIgnoreCase), templateInfo: new TemplateInfo(source.TemplateInfo)) { if (source == null) { throw new ArgumentNullException(nameof(source)); } // This is the core constructor called when Model is known. var modelType = GetModelType(model); var metadataModelType = source.ModelMetadata.UnderlyingOrModelType; if (modelType == metadataModelType && model == source.ModelExplorer.Model) { // 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 below. ModelExplorer = _metadataProvider.GetModelExplorerForType(_declaredModelType, model: null); } // 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, Type declaredModelType, IDictionary data, TemplateInfo templateInfo) { _metadataProvider = metadataProvider; ModelState = modelState; _declaredModelType = declaredModelType; _data = data; TemplateInfo = templateInfo; } public object Model { get { return ModelExplorer.Model; } set { // Reset ModelExplorer to ensure Model and ModelMetadata.Model remain equal. SetModel(value); } } public ModelStateDictionary ModelState { get; } /// /// 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 { return ModelExplorer.Metadata; } } /// /// Gets or sets the for the . /// public ModelExplorer ModelExplorer { get; set; } 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 != null) ? info.Value : null; } /// /// Gets value of named in this , formatted /// using given . /// /// Expression name, relative to the current model. /// /// The composite format (see http://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); } 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); } // This method will execute before the derived type's instance constructor executes. Derived types must // be aware of this and should plan accordingly. For example, the logic in SetModel() should be simple // enough so as not to depend on the "this" pointer referencing a fully constructed object. protected virtual void SetModel(object value) { EnsureCompatible(value); // Reset or override ModelMetadata based on runtime value type. Fall back to declared type if value is // null. When called from a constructor, current ModelExplorer may already be set to preserve // customizations made in parent scope. But ModelExplorer is never null after instance is initialized. var modelType = GetModelType(value); Type metadataModelType = null; if (ModelExplorer != null) { metadataModelType = ModelMetadata.UnderlyingOrModelType; } if (metadataModelType != 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); } } // 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); } } private Type GetModelType(object value) { return (value == null) ? _declaredModelType : value.GetType(); } 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 } }