From a1a180d4d08846266ee6d5b94d2a6f846d343c2d Mon Sep 17 00:00:00 2001 From: dougbu Date: Mon, 31 Mar 2014 10:25:29 -0700 Subject: [PATCH] Fill out Rendering.Expressions II Copy from: - some `static` `ModelMetadata` methods -> `ExpressionMetadataProvider` - `TryGetValueDelegate` -> `TryGetValueDelegate` - `TypeHelpers.CreateTryGetValueDelegate()`, related bits -> `TryGetValueProvider` - `ViewDataDictionary.ViewDataEvaluator` inner class -> `ViewDataEvaluator` - `ViewDataInfo` -> `ViewDataInfo` - `ViewDataDictionary.Eval()`, related bits -> add to `ViewDataDictionary` Change to fit in new world: - usual stuff: `var`, `[NotNull]`, String -> string, namespaces, etc. - PropertyDescriptor -> PropertyInfo - update Reflection use - no `IModelMetadata.Container` property - improve a couple of variable and parameter names - make `ViewDataInfo` immutable - make `ViewDataDictionary.FormatValueInternal` `public` and -> `FormatValue` - remove `[SuppressMessage]` attributes --- .../Expressions/ExpressionMetadataProvider.cs | 142 ++++++++++++++++ .../Expressions/TryGetValueDelegate.cs | 4 + .../Expressions/TryGetValueProvider.cs | 91 +++++++++++ .../Expressions/ViewDataEvaluator.cs | 152 ++++++++++++++++++ .../Expressions/ViewDataInfo.cs | 55 +++++++ .../Properties/Resources.Designer.cs | 16 ++ .../Properties/Resources.resx | 3 + .../View/ViewDataDictionary.cs | 41 +++++ .../project.json | 1 + 9 files changed, 505 insertions(+) create mode 100644 src/Microsoft.AspNet.Mvc.Rendering/Expressions/ExpressionMetadataProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc.Rendering/Expressions/TryGetValueDelegate.cs create mode 100644 src/Microsoft.AspNet.Mvc.Rendering/Expressions/TryGetValueProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc.Rendering/Expressions/ViewDataEvaluator.cs create mode 100644 src/Microsoft.AspNet.Mvc.Rendering/Expressions/ViewDataInfo.cs diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Expressions/ExpressionMetadataProvider.cs b/src/Microsoft.AspNet.Mvc.Rendering/Expressions/ExpressionMetadataProvider.cs new file mode 100644 index 0000000000..261260e7dd --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/Expressions/ExpressionMetadataProvider.cs @@ -0,0 +1,142 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.AspNet.Mvc.ModelBinding; + +namespace Microsoft.AspNet.Mvc.Rendering.Expressions +{ + public static class ExpressionMetadataProvider + { + public static ModelMetadata FromLambdaExpression( + [NotNull] Expression> expression, + [NotNull] ViewDataDictionary viewData, + IModelMetadataProvider metadataProvider) + { + string propertyName = null; + Type containerType = null; + var legalExpression = false; + + // Need to verify the expression is valid; it needs to at least end in something + // that we can convert to a meaningful string for model binding purposes + + switch (expression.Body.NodeType) + { + case ExpressionType.ArrayIndex: + // ArrayIndex always means a single-dimensional indexer; + // multi-dimensional indexer is a method call to Get(). + legalExpression = true; + break; + + case ExpressionType.Call: + // Only legal method call is a single argument indexer/DefaultMember call + legalExpression = ExpressionHelper.IsSingleArgumentIndexer(expression.Body); + break; + + case ExpressionType.MemberAccess: + // Property/field access is always legal + var memberExpression = (MemberExpression)expression.Body; + propertyName = memberExpression.Member is PropertyInfo ? memberExpression.Member.Name : null; + containerType = memberExpression.Expression.Type; + legalExpression = true; + break; + + case ExpressionType.Parameter: + // Parameter expression means "model => model", so we delegate to FromModel + return FromModel(viewData, metadataProvider); + } + + if (!legalExpression) + { + throw new InvalidOperationException(Resources.TemplateHelpers_TemplateLimitations); + } + + var container = viewData.Model; + Func modelAccessor = () => + { + try + { + return CachedExpressionCompiler.Process(expression)(container); + } + catch (NullReferenceException) + { + return null; + } + }; + + return GetMetadataFromProvider(modelAccessor, typeof(TValue), propertyName, containerType, metadataProvider); + } + + public static ModelMetadata FromStringExpression([NotNull] string expression, + [NotNull] ViewDataDictionary viewData, + IModelMetadataProvider metadataProvider) + { + if (expression.Length == 0) + { + // Empty string really means "model metadata for the current model" + return FromModel(viewData, metadataProvider); + } + + var viewDataInfo = ViewDataEvaluator.Eval(viewData, expression); + Type containerType = null; + Type modelType = null; + Func modelAccessor = null; + string propertyName = null; + + if (viewDataInfo != null) + { + if (viewDataInfo.Container != null) + { + containerType = viewDataInfo.Container.GetType(); + } + + modelAccessor = () => viewDataInfo.Value; + + if (viewDataInfo.PropertyInfo != null) + { + propertyName = viewDataInfo.PropertyInfo.Name; + modelType = viewDataInfo.PropertyInfo.PropertyType; + } + else if (viewDataInfo.Value != null) + { + // We only need to delay accessing properties (for LINQ to SQL) + modelType = viewDataInfo.Value.GetType(); + } + } + else if (viewData.ModelMetadata != null) + { + // Try getting a property from ModelMetadata if we couldn't find an answer in ViewData + var propertyMetadata = + viewData.ModelMetadata.Properties.Where(p => p.PropertyName == expression).FirstOrDefault(); + if (propertyMetadata != null) + { + return propertyMetadata; + } + } + + return GetMetadataFromProvider(modelAccessor, modelType ?? typeof(string), propertyName, containerType, + metadataProvider); + } + + private static ModelMetadata FromModel([NotNull] ViewDataDictionary viewData, + IModelMetadataProvider metadataProvider) + { + return viewData.ModelMetadata ?? GetMetadataFromProvider(null, typeof(string), propertyName: null, + containerType: null, metadataProvider: metadataProvider); + } + + // 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, Type containerType, [NotNull] IModelMetadataProvider metadataProvider) + { + if (containerType != null && !string.IsNullOrEmpty(propertyName)) + { + return metadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName); + } + + return metadataProvider.GetMetadataForType(modelAccessor, modelType); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Expressions/TryGetValueDelegate.cs b/src/Microsoft.AspNet.Mvc.Rendering/Expressions/TryGetValueDelegate.cs new file mode 100644 index 0000000000..b07890505c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/Expressions/TryGetValueDelegate.cs @@ -0,0 +1,4 @@ +namespace Microsoft.AspNet.Mvc.Rendering.Expressions +{ + public delegate bool TryGetValueDelegate(object dictionary, string key, out object value); +} diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Expressions/TryGetValueProvider.cs b/src/Microsoft.AspNet.Mvc.Rendering/Expressions/TryGetValueProvider.cs new file mode 100644 index 0000000000..cc1ec70f4a --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/Expressions/TryGetValueProvider.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; + +namespace Microsoft.AspNet.Mvc.Rendering.Expressions +{ + public static class TryGetValueProvider + { + private static readonly Dictionary _tryGetValueDelegateCache = + new Dictionary(); + private static readonly ReaderWriterLockSlim _tryGetValueDelegateCacheLock = new ReaderWriterLockSlim(); + + // Information about private static method declared below. + private static readonly MethodInfo _strongTryGetValueImplInfo = + typeof(TryGetValueProvider).GetTypeInfo().GetDeclaredMethod("StrongTryGetValueImpl"); + + public static TryGetValueDelegate CreateInstance([NotNull] Type targetType) + { + TryGetValueDelegate result; + + // Cache delegates since properties of model types are re-evaluated numerous times. + _tryGetValueDelegateCacheLock.EnterReadLock(); + try + { + if (_tryGetValueDelegateCache.TryGetValue(targetType, out result)) + { + return result; + } + } + finally + { + _tryGetValueDelegateCacheLock.ExitReadLock(); + } + + var dictionaryType = targetType.ExtractGenericInterface(typeof(IDictionary<,>)); + + // Just wrap a call to the underlying IDictionary.TryGetValue() where string can be cast to TKey. + if (dictionaryType != null) + { + var typeArguments = dictionaryType.GetGenericArguments(); + var keyType = typeArguments[0]; + var returnType = typeArguments[1]; + + if (keyType.IsAssignableFrom(typeof(string))) + { + var implementationMethod = _strongTryGetValueImplInfo.MakeGenericMethod(keyType, returnType); + result = (TryGetValueDelegate)implementationMethod.CreateDelegate(typeof(TryGetValueDelegate)); + } + } + + // Wrap a call to the underlying IDictionary.Item(). + if (result == null && typeof(IDictionary).IsAssignableFrom(targetType)) + { + result = TryGetValueFromNonGenericDictionary; + } + + _tryGetValueDelegateCacheLock.EnterWriteLock(); + try + { + _tryGetValueDelegateCache[targetType] = result; + } + finally + { + _tryGetValueDelegateCacheLock.ExitWriteLock(); + } + + return result; + } + + private static bool StrongTryGetValueImpl(object dictionary, string key, out object value) + { + var strongDict = (IDictionary)dictionary; + + TValue strongValue; + var success = strongDict.TryGetValue((TKey)(object)key, out strongValue); + value = strongValue; + return success; + } + + private static bool TryGetValueFromNonGenericDictionary(object dictionary, string key, out object value) + { + var weakDict = (IDictionary)dictionary; + + var success = weakDict.Contains(key); + value = success ? weakDict[key] : null; + return success; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Expressions/ViewDataEvaluator.cs b/src/Microsoft.AspNet.Mvc.Rendering/Expressions/ViewDataEvaluator.cs new file mode 100644 index 0000000000..ff772a6335 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/Expressions/ViewDataEvaluator.cs @@ -0,0 +1,152 @@ +using System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.AspNet.Mvc.Rendering.Expressions +{ + public static class ViewDataEvaluator + { + public static ViewDataInfo Eval([NotNull] ViewDataDictionary viewData, [NotNull] string expression) + { + // Given an expression "one.two.three.four" we look up the following (pseudocode): + // this["one.two.three.four"] + // this["one.two.three"]["four"] + // this["one.two"]["three.four] + // this["one.two"]["three"]["four"] + // this["one"]["two.three.four"] + // this["one"]["two.three"]["four"] + // this["one"]["two"]["three.four"] + // this["one"]["two"]["three"]["four"] + + return EvalComplexExpression(viewData, expression); + } + + private static ViewDataInfo EvalComplexExpression(object indexableObject, string expression) + { + foreach (var expressionPair in GetRightToLeftExpressions(expression)) + { + var subExpression = expressionPair.Left; + var postExpression = expressionPair.Right; + + var subTargetInfo = GetPropertyValue(indexableObject, subExpression); + if (subTargetInfo != null) + { + if (string.IsNullOrEmpty(postExpression)) + { + return subTargetInfo; + } + + if (subTargetInfo.Value != null) + { + var potential = EvalComplexExpression(subTargetInfo.Value, postExpression); + if (potential != null) + { + return potential; + } + } + } + } + + return null; + } + + private static IEnumerable GetRightToLeftExpressions(string expression) + { + // Produces an enumeration of all the combinations of complex property names + // given a complex expression. See the list above for an example of the result + // of the enumeration. + + yield return new ExpressionPair(expression, string.Empty); + + var lastDot = expression.LastIndexOf('.'); + + var subExpression = expression; + var postExpression = string.Empty; + + while (lastDot > -1) + { + subExpression = expression.Substring(0, lastDot); + postExpression = expression.Substring(lastDot + 1); + yield return new ExpressionPair(subExpression, postExpression); + + lastDot = subExpression.LastIndexOf('.'); + } + } + + private static ViewDataInfo GetIndexedPropertyValue(object indexableObject, string key) + { + var dict = indexableObject as IDictionary; + object value = null; + var success = false; + + if (dict != null) + { + success = dict.TryGetValue(key, out value); + } + else + { + // Fall back to TryGetValue() calls for other Dictionary types. + var tryDelegate = TryGetValueProvider.CreateInstance(indexableObject.GetType()); + if (tryDelegate != null) + { + success = tryDelegate(indexableObject, key, out value); + } + } + + if (success) + { + return new ViewDataInfo(indexableObject, value); + } + + return null; + } + + private static ViewDataInfo GetPropertyValue(object container, string propertyName) + { + // This method handles one "segment" of a complex property expression + + // First, we try to evaluate the property based on its indexer + var value = GetIndexedPropertyValue(container, propertyName); + if (value != null) + { + return value; + } + + // If the indexer didn't return anything useful, continue... + + // If the container is a ViewDataDictionary then treat its Model property + // as the container instead of the ViewDataDictionary itself. + var viewData = container as ViewDataDictionary; + if (viewData != null) + { + container = viewData.Model; + } + + // If the container is null, we're out of options + if (container == null) + { + return null; + } + + // Finally try to use PropertyInfo and treat the expression as a property name + var propertyInfo = container.GetType().GetRuntimeProperty(propertyName); + if (propertyInfo == null) + { + return null; + } + + return new ViewDataInfo(container, propertyInfo, () => propertyInfo.GetValue(container)); + } + + private struct ExpressionPair + { + public readonly string Left; + public readonly string Right; + + public ExpressionPair(string left, string right) + { + Left = left; + Right = right; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Expressions/ViewDataInfo.cs b/src/Microsoft.AspNet.Mvc.Rendering/Expressions/ViewDataInfo.cs new file mode 100644 index 0000000000..3f0fdb0b8e --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/Expressions/ViewDataInfo.cs @@ -0,0 +1,55 @@ +using System; +using System.Reflection; + +namespace Microsoft.AspNet.Mvc.Rendering.Expressions +{ + public class ViewDataInfo + { + private object _value; + private Func _valueAccessor; + + /// + /// Initializes a new instance of the class with info about a + /// lookup which has already been evaluated. + /// + public ViewDataInfo(object container, object value) + { + Container = container; + _value = value; + } + + /// + /// Initializes a new instance of the class with info about a + /// lookup which is evaluated when is read. + /// + public ViewDataInfo(object container, PropertyInfo propertyInfo, Func valueAccessor) + { + Container = container; + PropertyInfo = propertyInfo; + _valueAccessor = valueAccessor; + } + + public object Container { get; private set; } + + public PropertyInfo PropertyInfo { get; private set; } + + public object Value + { + get + { + if (_valueAccessor != null) + { + _value = _valueAccessor(); + _valueAccessor = null; + } + + return _value; + } + set + { + _value = value; + _valueAccessor = null; + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.Designer.cs index 53c98e6693..ca111c1832 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.Designer.cs @@ -138,6 +138,22 @@ namespace Microsoft.AspNet.Mvc.Rendering return GetString("HtmlHelper_NotContextualized"); } + /// + /// Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions. + /// + internal static string TemplateHelpers_TemplateLimitations + { + get { return GetString("TemplateHelpers_TemplateLimitations"); } + } + + /// + /// Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions. + /// + internal static string FormatTemplateHelpers_TemplateLimitations() + { + return GetString("TemplateHelpers_TemplateLimitations"); + } + /// /// The model item passed is null, but this ViewDataDictionary instance requires a non-null model item of type '{0}'. /// diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.resx b/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.resx index 9753e01b0b..7adfea7bc5 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.resx @@ -141,6 +141,9 @@ Must call 'Contextualize' method before using this HtmlHelper instance. + + Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions. + The model item passed is null, but this ViewDataDictionary instance requires a non-null model item of type '{0}'. diff --git a/src/Microsoft.AspNet.Mvc.Rendering/View/ViewDataDictionary.cs b/src/Microsoft.AspNet.Mvc.Rendering/View/ViewDataDictionary.cs index 04571deaf7..6494a39cf7 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/View/ViewDataDictionary.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/View/ViewDataDictionary.cs @@ -1,7 +1,9 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.Rendering.Expressions; namespace Microsoft.AspNet.Mvc.Rendering { @@ -124,6 +126,45 @@ namespace Microsoft.AspNet.Mvc.Rendering } #endregion + public object Eval(string expression) + { + var info = GetViewDataInfo(expression); + return (info != null) ? info.Value : null; + } + + 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); + } + } + + public ViewDataInfo GetViewDataInfo(string expression) + { + if (string.IsNullOrEmpty(expression)) + { + throw new ArgumentException(Resources.ArgumentNullOrEmpty, "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. diff --git a/src/Microsoft.AspNet.Mvc.Rendering/project.json b/src/Microsoft.AspNet.Mvc.Rendering/project.json index d70f9b4f31..bc423c7937 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/project.json +++ b/src/Microsoft.AspNet.Mvc.Rendering/project.json @@ -28,6 +28,7 @@ "System.Runtime": "4.0.20.0", "System.Runtime.Extensions": "4.0.10.0", "System.Runtime.InteropServices": "4.0.20.0", + "System.Threading": "4.0.0.0", "System.Threading.Tasks": "4.0.10.0" } }