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
This commit is contained in:
parent
87c2041a52
commit
a1a180d4d0
|
|
@ -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<TParameter, TValue>(
|
||||
[NotNull] Expression<Func<TParameter, TValue>> expression,
|
||||
[NotNull] ViewDataDictionary<TParameter> 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<object> 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<object> 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<object> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
namespace Microsoft.AspNet.Mvc.Rendering.Expressions
|
||||
{
|
||||
public delegate bool TryGetValueDelegate(object dictionary, string key, out object value);
|
||||
}
|
||||
|
|
@ -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<Type, TryGetValueDelegate> _tryGetValueDelegateCache =
|
||||
new Dictionary<Type, TryGetValueDelegate>();
|
||||
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<TKey, TValue>.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<TKey, TValue>(object dictionary, string key, out object value)
|
||||
{
|
||||
var strongDict = (IDictionary<TKey, TValue>)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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ExpressionPair> 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<string, object>;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Rendering.Expressions
|
||||
{
|
||||
public class ViewDataInfo
|
||||
{
|
||||
private object _value;
|
||||
private Func<object> _valueAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ViewDataInfo"/> class with info about a
|
||||
/// <see cref="ViewDataDictionary"/> lookup which has already been evaluated.
|
||||
/// </summary>
|
||||
public ViewDataInfo(object container, object value)
|
||||
{
|
||||
Container = container;
|
||||
_value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ViewDataInfo"/> class with info about a
|
||||
/// <see cref="ViewDataDictionary"/> lookup which is evaluated when <see cref="Value"/> is read.
|
||||
/// </summary>
|
||||
public ViewDataInfo(object container, PropertyInfo propertyInfo, Func<object> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -138,6 +138,22 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
|||
return GetString("HtmlHelper_NotContextualized");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.
|
||||
/// </summary>
|
||||
internal static string TemplateHelpers_TemplateLimitations
|
||||
{
|
||||
get { return GetString("TemplateHelpers_TemplateLimitations"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.
|
||||
/// </summary>
|
||||
internal static string FormatTemplateHelpers_TemplateLimitations()
|
||||
{
|
||||
return GetString("TemplateHelpers_TemplateLimitations");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The model item passed is null, but this ViewDataDictionary instance requires a non-null model item of type '{0}'.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -141,6 +141,9 @@
|
|||
<data name="HtmlHelper_NotContextualized" xml:space="preserve">
|
||||
<value>Must call 'Contextualize' method before using this HtmlHelper instance.</value>
|
||||
</data>
|
||||
<data name="TemplateHelpers_TemplateLimitations" xml:space="preserve">
|
||||
<value>Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.</value>
|
||||
</data>
|
||||
<data name="ViewData_ModelCannotBeNull" xml:space="preserve">
|
||||
<value>The model item passed is null, but this ViewDataDictionary instance requires a non-null model item of type '{0}'.</value>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue