aspnetcore/src/Microsoft.AspNetCore.Mvc.Vi.../ViewFeatures/ViewDataEvaluator.cs

221 lines
8.6 KiB
C#

// 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.Generic;
using System.Reflection;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
public static class ViewDataEvaluator
{
/// <summary>
/// Gets <see cref="ViewDataInfo"/> for named <paramref name="expression"/> in given
/// <paramref name="viewData"/>.
/// </summary>
/// <param name="viewData">
/// The <see cref="ViewDataDictionary"/> that may contain the <paramref name="expression"/> value.
/// </param>
/// <param name="expression">Expression name, relative to <c>viewData.Model</c>.</param>
/// <returns>
/// <see cref="ViewDataInfo"/> for named <paramref name="expression"/> in given <paramref name="viewData"/>.
/// </returns>
public static ViewDataInfo Eval(ViewDataDictionary viewData, string expression)
{
if (viewData == null)
{
throw new ArgumentNullException(nameof(viewData));
}
// While it is not valid to generate a field for the top-level model itself because the result is an
// unnamed input element, do not throw here if full name is null or empty. Support is needed for cases
// such as Html.Label() and Html.Value(), where the user's code is not creating a name attribute. Checks
// are in place at higher levels for the invalid cases.
var fullName = viewData.TemplateInfo.GetFullHtmlFieldName(expression);
// Given an expression "one.two.three.four" we look up the following (pseudo-code):
// 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"]
// Try to find a matching ViewData entry using the full expression name. If that fails, fall back to
// ViewData.Model using the expression's relative name.
var result = EvalComplexExpression(viewData, fullName);
if (result == null)
{
if (string.IsNullOrEmpty(expression))
{
// Null or empty expression name means current model even if that model is null.
result = new ViewDataInfo(container: viewData, value: viewData.Model);
}
else
{
result = EvalComplexExpression(viewData.Model, expression);
}
}
return result;
}
/// <summary>
/// Gets <see cref="ViewDataInfo"/> for named <paramref name="expression"/> in given
/// <paramref name="indexableObject"/>.
/// </summary>
/// <param name="indexableObject">
/// The <see cref="object"/> that may contain the <paramref name="expression"/> value.
/// </param>
/// <param name="expression">Expression name, relative to <paramref name="indexableObject"/>.</param>
/// <returns>
/// <see cref="ViewDataInfo"/> for named <paramref name="expression"/> in given
/// <paramref name="indexableObject"/>.
/// </returns>
public static ViewDataInfo Eval(object indexableObject, string expression)
{
// Run through many of the same cases as other Eval() overload.
return EvalComplexExpression(indexableObject, expression);
}
private static ViewDataInfo EvalComplexExpression(object indexableObject, string expression)
{
if (indexableObject == null)
{
return null;
}
if (expression == null)
{
// In case a Dictionary indexableObject contains a "" entry, don't short-circuit the logic below.
expression = string.Empty;
}
return InnerEvalComplexExpression(indexableObject, expression);
}
private static ViewDataInfo InnerEvalComplexExpression(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 = InnerEvalComplexExpression(subTargetInfo.Value, postExpression);
if (potential != null)
{
return potential;
}
}
}
}
return null;
}
// Produces an enumeration of combinations of property names given a complex expression in the following order:
// this["one.two.three.four"]
// this["one.two.three][four"]
// this["one.two][three.four"]
// this["one][two.three.four"]
// Recursion of InnerEvalComplexExpression() further sub-divides these cases to cover the full set of
// combinations shown in Eval(ViewDataDictionary, string) comments.
private static IEnumerable<ExpressionPair> GetRightToLeftExpressions(string expression)
{
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;
}
// This method handles one "segment" of a complex property expression
private static ViewDataInfo GetPropertyValue(object container, string propertyName)
{
// First, try to evaluate the property based on its indexer.
var value = GetIndexedPropertyValue(container, propertyName);
if (value != null)
{
return value;
}
// Do not attempt to find a property with an empty name and or of a ViewDataDictionary.
if (string.IsNullOrEmpty(propertyName) || container is ViewDataDictionary)
{
return null;
}
// If the indexer didn't return anything useful, 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;
}
}
}
}