// 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 { /// /// Gets for named in given /// . /// /// /// The that may contain the value. /// /// Expression name, relative to viewData.Model. /// /// for named in given . /// 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; } /// /// Gets for named in given /// . /// /// /// The that may contain the value. /// /// Expression name, relative to . /// /// for named in given /// . /// 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 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; 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; } } } }