Make `string`-based expression evaluations behave consistently
- #1485, #1487 - handle `TemplateInfo.HtmlFieldPrefix` in `ViewDataEvaluator.Eval()` - attempt lookup in the `ViewDataDictionary` using full name then evaluate relative `expression` against `viewData.Model` - handle `null` or empty `expression` special case in this method (remove `throw`s) - always pass relative `expression` name into `Eval()` - remove `null` or empty `expression` handling from higher-level code - in a couple of cases, special-case returned `ViewDataInfo` - #2662 - remove incorrect guard from `DefaultHtmlGenerator.GenerateRadioButtion()` - add doc comments for the core methods that have changed - enable unit tests skipped due to one of above bugs - fix one (yeah, just one) other test with incorrect expectations - remove functional test comments about the above bugs and update expectations nits: - move some comments describing `ViewDataEvaluator` methods above the methods
This commit is contained in:
parent
8b5931d758
commit
27283ec098
|
|
@ -71,7 +71,7 @@ namespace Microsoft.AspNet.Mvc.Rendering.Expressions
|
||||||
ModelMetadata metadata;
|
ModelMetadata metadata;
|
||||||
if (propertyName == null)
|
if (propertyName == null)
|
||||||
{
|
{
|
||||||
// Ex:
|
// Ex:
|
||||||
// m => 5 (arbitrary expression)
|
// m => 5 (arbitrary expression)
|
||||||
// m => foo (arbitrary expression)
|
// m => foo (arbitrary expression)
|
||||||
// m => m.Widgets[0] (expression ending with non-property-access)
|
// m => m.Widgets[0] (expression ending with non-property-access)
|
||||||
|
|
@ -79,7 +79,7 @@ namespace Microsoft.AspNet.Mvc.Rendering.Expressions
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Ex:
|
// Ex:
|
||||||
// m => m.Color (simple property access)
|
// m => m.Color (simple property access)
|
||||||
// m => m.Color.Red (nested property access)
|
// m => m.Color.Red (nested property access)
|
||||||
// m => m.Widgets[0].Size (expression ending with property-access)
|
// m => m.Widgets[0].Size (expression ending with property-access)
|
||||||
|
|
@ -89,22 +89,27 @@ namespace Microsoft.AspNet.Mvc.Rendering.Expressions
|
||||||
return viewData.ModelExplorer.GetExplorerForExpression(metadata, modelAccessor);
|
return viewData.ModelExplorer.GetExplorerForExpression(metadata, modelAccessor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets <see cref="ModelExplorer"/> for named <paramref name="expression"/> in given
|
||||||
|
/// <paramref name="viewData"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="expression">Expression name, relative to <c>viewData.Model</c>.</param>
|
||||||
|
/// <param name="viewData">
|
||||||
|
/// The <see cref="ViewDataDictionary"/> that may contain the <paramref name="expression"/> value.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// <see cref="ModelExplorer"/> for named <paramref name="expression"/> in given <paramref name="viewData"/>.
|
||||||
|
/// </returns>
|
||||||
public static ModelExplorer FromStringExpression(
|
public static ModelExplorer FromStringExpression(
|
||||||
string expression,
|
string expression,
|
||||||
[NotNull] ViewDataDictionary viewData,
|
[NotNull] ViewDataDictionary viewData,
|
||||||
IModelMetadataProvider metadataProvider)
|
IModelMetadataProvider metadataProvider)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(expression))
|
|
||||||
{
|
|
||||||
// Empty string really means "ModelMetadata for the current model".
|
|
||||||
return FromModel(viewData, metadataProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
var viewDataInfo = ViewDataEvaluator.Eval(viewData, expression);
|
var viewDataInfo = ViewDataEvaluator.Eval(viewData, expression);
|
||||||
|
|
||||||
if (viewDataInfo == null)
|
if (viewDataInfo == null)
|
||||||
{
|
{
|
||||||
// Try getting a property from ModelMetadata if we couldn't find an answer in ViewData
|
// Try getting a property from ModelMetadata if we couldn't find an answer in ViewData
|
||||||
var propertyExplorer = viewData.ModelExplorer.GetExplorerForProperty(expression);
|
var propertyExplorer = viewData.ModelExplorer.GetExplorerForProperty(expression);
|
||||||
if (propertyExplorer != null)
|
if (propertyExplorer != null)
|
||||||
{
|
{
|
||||||
|
|
@ -114,11 +119,20 @@ namespace Microsoft.AspNet.Mvc.Rendering.Expressions
|
||||||
|
|
||||||
if (viewDataInfo != null)
|
if (viewDataInfo != null)
|
||||||
{
|
{
|
||||||
|
if (viewDataInfo.Container == viewData &&
|
||||||
|
viewDataInfo.Value == viewData.Model &&
|
||||||
|
string.IsNullOrEmpty(expression))
|
||||||
|
{
|
||||||
|
// Nothing for empty expression in ViewData and ViewDataEvaluator just returned the model. Handle
|
||||||
|
// using FromModel() for its object special case.
|
||||||
|
return FromModel(viewData, metadataProvider);
|
||||||
|
}
|
||||||
|
|
||||||
ModelExplorer containerExplorer = viewData.ModelExplorer;
|
ModelExplorer containerExplorer = viewData.ModelExplorer;
|
||||||
if (viewDataInfo.Container != null)
|
if (viewDataInfo.Container != null)
|
||||||
{
|
{
|
||||||
containerExplorer = metadataProvider.GetModelExplorerForType(
|
containerExplorer = metadataProvider.GetModelExplorerForType(
|
||||||
viewDataInfo.Container.GetType(),
|
viewDataInfo.Container.GetType(),
|
||||||
viewDataInfo.Container);
|
viewDataInfo.Container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,34 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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.Collections.Generic;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Microsoft.AspNet.Mvc.Extensions;
|
|
||||||
using Microsoft.Framework.Internal;
|
using Microsoft.Framework.Internal;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Rendering.Expressions
|
namespace Microsoft.AspNet.Mvc.Rendering.Expressions
|
||||||
{
|
{
|
||||||
public static class ViewDataEvaluator
|
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([NotNull] ViewDataDictionary viewData, string expression)
|
public static ViewDataInfo Eval([NotNull] ViewDataDictionary viewData, string expression)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(expression))
|
// 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
|
||||||
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(expression));
|
// 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 (pseudocode):
|
// 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]
|
||||||
|
|
@ -28,21 +38,60 @@ namespace Microsoft.AspNet.Mvc.Rendering.Expressions
|
||||||
// this["one"]["two"]["three.four"]
|
// this["one"]["two"]["three.four"]
|
||||||
// this["one"]["two"]["three"]["four"]
|
// this["one"]["two"]["three"]["four"]
|
||||||
|
|
||||||
return EvalComplexExpression(viewData, expression);
|
// 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);
|
||||||
public static ViewDataInfo Eval(object indexableObject, string expression)
|
if (result == null)
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(expression))
|
|
||||||
{
|
{
|
||||||
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(expression));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run through same cases as other Eval() overload but allow a null container.
|
return result;
|
||||||
return (indexableObject == null) ? null : EvalComplexExpression(indexableObject, expression);
|
}
|
||||||
|
|
||||||
|
/// <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)
|
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))
|
foreach (var expressionPair in GetRightToLeftExpressions(expression))
|
||||||
{
|
{
|
||||||
|
|
@ -59,7 +108,7 @@ namespace Microsoft.AspNet.Mvc.Rendering.Expressions
|
||||||
|
|
||||||
if (subTargetInfo.Value != null)
|
if (subTargetInfo.Value != null)
|
||||||
{
|
{
|
||||||
var potential = EvalComplexExpression(subTargetInfo.Value, postExpression);
|
var potential = InnerEvalComplexExpression(subTargetInfo.Value, postExpression);
|
||||||
if (potential != null)
|
if (potential != null)
|
||||||
{
|
{
|
||||||
return potential;
|
return potential;
|
||||||
|
|
@ -71,12 +120,15 @@ namespace Microsoft.AspNet.Mvc.Rendering.Expressions
|
||||||
return null;
|
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)
|
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);
|
yield return new ExpressionPair(expression, string.Empty);
|
||||||
|
|
||||||
var lastDot = expression.LastIndexOf('.');
|
var lastDot = expression.LastIndexOf('.');
|
||||||
|
|
@ -122,34 +174,24 @@ namespace Microsoft.AspNet.Mvc.Rendering.Expressions
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This method handles one "segment" of a complex property expression
|
||||||
private static ViewDataInfo GetPropertyValue(object container, string propertyName)
|
private static ViewDataInfo GetPropertyValue(object container, string propertyName)
|
||||||
{
|
{
|
||||||
// This method handles one "segment" of a complex property expression
|
// First, try to evaluate the property based on its indexer.
|
||||||
|
|
||||||
// First, we try to evaluate the property based on its indexer
|
|
||||||
var value = GetIndexedPropertyValue(container, propertyName);
|
var value = GetIndexedPropertyValue(container, propertyName);
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the indexer didn't return anything useful, continue...
|
// Do not attempt to find a property with an empty name and or of a ViewDataDictionary.
|
||||||
|
if (string.IsNullOrEmpty(propertyName) || container is ViewDataDictionary)
|
||||||
// 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally try to use PropertyInfo and treat the expression as a property name
|
// 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);
|
var propertyInfo = container.GetType().GetRuntimeProperty(propertyName);
|
||||||
if (propertyInfo == null)
|
if (propertyInfo == null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -322,11 +322,10 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
|
|
||||||
// isChecked not provided nor found in the given attributes; fall back to view data.
|
// isChecked not provided nor found in the given attributes; fall back to view data.
|
||||||
var valueString = Convert.ToString(value, CultureInfo.CurrentCulture);
|
var valueString = Convert.ToString(value, CultureInfo.CurrentCulture);
|
||||||
isChecked = !string.IsNullOrEmpty(expression) &&
|
isChecked = string.Equals(
|
||||||
string.Equals(
|
EvalString(viewContext, expression),
|
||||||
EvalString(viewContext, expression),
|
valueString,
|
||||||
valueString,
|
StringComparison.OrdinalIgnoreCase);
|
||||||
StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -419,14 +418,6 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
// If we got a null selectList, try to use ViewData to get the list of items.
|
// If we got a null selectList, try to use ViewData to get the list of items.
|
||||||
if (selectList == null)
|
if (selectList == null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(expression))
|
|
||||||
{
|
|
||||||
// Do not call ViewData.Eval(); that would return ViewData.Model, which is not correct here.
|
|
||||||
// Note this case has a simple workaround: users must pass a non-null selectList to use
|
|
||||||
// DropDownList() or ListBox() in a template, where a null or empty name has meaning.
|
|
||||||
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(expression));
|
|
||||||
}
|
|
||||||
|
|
||||||
selectList = GetSelectListItems(viewContext, expression);
|
selectList = GetSelectListItems(viewContext, expression);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1012,7 +1003,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
|
|
||||||
if (!usedModelState && useViewData)
|
if (!usedModelState && useViewData)
|
||||||
{
|
{
|
||||||
isChecked = EvalBoolean(viewContext, fullName);
|
isChecked = EvalBoolean(viewContext, expression);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isChecked)
|
if (isChecked)
|
||||||
|
|
@ -1036,7 +1027,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
var attributeValue = (string)GetModelStateValue(viewContext, fullName, typeof(string));
|
var attributeValue = (string)GetModelStateValue(viewContext, fullName, typeof(string));
|
||||||
if (attributeValue == null)
|
if (attributeValue == null)
|
||||||
{
|
{
|
||||||
attributeValue = useViewData ? EvalString(viewContext, fullName, format) : valueParameter;
|
attributeValue = useViewData ? EvalString(viewContext, expression, format) : valueParameter;
|
||||||
}
|
}
|
||||||
|
|
||||||
var addValue = true;
|
var addValue = true;
|
||||||
|
|
@ -1193,14 +1184,20 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
[NotNull] ViewContext viewContext,
|
[NotNull] ViewContext viewContext,
|
||||||
string expression)
|
string expression)
|
||||||
{
|
{
|
||||||
|
// Method is called only if user did not pass a select list in. They must provide select list items in the
|
||||||
|
// ViewData dictionary and definitely not as the Model. (Even if the Model datatype were correct, a
|
||||||
|
// <select> element generated for a collection of SelectListItems would be useless.)
|
||||||
var value = viewContext.ViewData.Eval(expression);
|
var value = viewContext.ViewData.Eval(expression);
|
||||||
if (value == null)
|
|
||||||
|
// First check whether above evaluation was successful and did not match ViewData.Model.
|
||||||
|
if (value == null || value == viewContext.ViewData.Model)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(Resources.FormatHtmlHelper_MissingSelectData(
|
throw new InvalidOperationException(Resources.FormatHtmlHelper_MissingSelectData(
|
||||||
$"IEnumerable<{nameof(SelectListItem)}>",
|
$"IEnumerable<{nameof(SelectListItem)}>",
|
||||||
expression));
|
expression));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Second check the Eval() call returned a collection of SelectListItems.
|
||||||
var selectList = value as IEnumerable<SelectListItem>;
|
var selectList = value as IEnumerable<SelectListItem>;
|
||||||
if (selectList == null)
|
if (selectList == null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Reflection;
|
|
||||||
using Microsoft.AspNet.Mvc.Extensions;
|
using Microsoft.AspNet.Mvc.Extensions;
|
||||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||||
using Microsoft.AspNet.Mvc.Rendering.Expressions;
|
using Microsoft.AspNet.Mvc.Rendering.Expressions;
|
||||||
|
|
@ -301,12 +300,37 @@ namespace Microsoft.AspNet.Mvc
|
||||||
get { return _data; }
|
get { return _data; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets value of named <paramref name="expression"/> in this <see cref="ViewDataDictionary"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="expression">Expression name, relative to the current model.</param>
|
||||||
|
/// <returns>Value of named <paramref name="expression"/> in this <see cref="ViewDataDictionary"/>.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Looks up <paramref name="expression"/> in the dictionary first. Falls back to evaluating it against
|
||||||
|
/// <see cref="Model"/>.
|
||||||
|
/// </remarks>
|
||||||
public object Eval(string expression)
|
public object Eval(string expression)
|
||||||
{
|
{
|
||||||
var info = GetViewDataInfo(expression);
|
var info = GetViewDataInfo(expression);
|
||||||
return (info != null) ? info.Value : null;
|
return (info != null) ? info.Value : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets value of named <paramref name="expression"/> in this <see cref="ViewDataDictionary"/>, formatted
|
||||||
|
/// using given <paramref name="format"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="expression">Expression name, relative to the current model.</param>
|
||||||
|
/// <param name="format">
|
||||||
|
/// The composite format <see cref="string"/> (see http://msdn.microsoft.com/en-us/library/txafckwd.aspx).
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// Value of named <paramref name="expression"/> in this <see cref="ViewDataDictionary"/>, formatted using
|
||||||
|
/// given <paramref name="format"/>.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Looks up <paramref name="expression"/> in the dictionary first. Falls back to evaluating it against
|
||||||
|
/// <see cref="Model"/>.
|
||||||
|
/// </remarks>
|
||||||
public string Eval(string expression, string format)
|
public string Eval(string expression, string format)
|
||||||
{
|
{
|
||||||
var value = Eval(expression);
|
var value = Eval(expression);
|
||||||
|
|
@ -330,14 +354,21 @@ namespace Microsoft.AspNet.Mvc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets <see cref="ViewDataInfo"/> for named <paramref name="expression"/> in this
|
||||||
|
/// <see cref="ViewDataDictionary"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="expression">Expression name, relative to the current model.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// <see cref="ViewDataInfo"/> for named <paramref name="expression"/> in this
|
||||||
|
/// <see cref="ViewDataDictionary"/>.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Looks up <paramref name="expression"/> in the dictionary first. Falls back to evaluating it against
|
||||||
|
/// <see cref="Model"/>.
|
||||||
|
/// </remarks>
|
||||||
public ViewDataInfo GetViewDataInfo(string expression)
|
public ViewDataInfo GetViewDataInfo(string expression)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(expression))
|
|
||||||
{
|
|
||||||
// Null or empty expression name means current model.
|
|
||||||
return new ViewDataInfo(container: null, value: Model);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ViewDataEvaluator.Eval(this, expression);
|
return ViewDataEvaluator.Eval(this, expression);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
Assert.Equal(expected, html.ToString());
|
Assert.Equal(expected, html.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "#1485, unable to get Model value.")]
|
[Fact]
|
||||||
public void CheckBoxInTemplate_GetsModelValue_IfModelStateAndViewDataEmpty()
|
public void CheckBoxInTemplate_GetsModelValue_IfModelStateAndViewDataEmpty()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
|
||||||
|
|
@ -248,7 +248,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DisplayText_ReturnsModelStateEntry()
|
public void DisplayText_IgnoresModelStateEntry_ReturnsViewDataEntry()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var model = new OverriddenToStringModel("Model value")
|
var model = new OverriddenToStringModel("Model value")
|
||||||
|
|
@ -257,7 +257,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
};
|
};
|
||||||
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
|
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
|
||||||
var viewData = helper.ViewData;
|
var viewData = helper.ViewData;
|
||||||
viewData["Name"] = "View data dictionary value";
|
viewData["FieldPrefix.Name"] = "View data dictionary value";
|
||||||
viewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix";
|
viewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix";
|
||||||
|
|
||||||
var modelState = new ModelState();
|
var modelState = new ModelState();
|
||||||
|
|
|
||||||
|
|
@ -239,7 +239,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
Assert.Equal(expected, result.ToString());
|
Assert.Equal(expected, result.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "#1485, unable to get Model value.")]
|
[Fact]
|
||||||
public void HiddenInTemplate_GetsModelValue_IfModelStateAndViewDataEmpty()
|
public void HiddenInTemplate_GetsModelValue_IfModelStateAndViewDataEmpty()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
|
||||||
|
|
@ -488,7 +488,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
Assert.Equal(expectedHtml, html.ToString());
|
Assert.Equal(expectedHtml, html.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "#1487, incorrectly matches Property1 entry (without prefix) in ViewData.")]
|
[Fact]
|
||||||
public void DropDownListInTemplate_GetsViewDataEntry_IfModelStateEmpty()
|
public void DropDownListInTemplate_GetsViewDataEntry_IfModelStateEmpty()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -516,7 +516,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
Assert.Equal(expectedHtml, html.ToString());
|
Assert.Equal(expectedHtml, html.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "#1487, incorrectly matches Property1 entry (without prefix) in ViewData.")]
|
[Fact]
|
||||||
public void DropDownListInTemplate_GetsPropertyOfViewDataEntry_IfModelStateEmptyAndNoViewDataEntryWithPrefix()
|
public void DropDownListInTemplate_GetsPropertyOfViewDataEntry_IfModelStateEmptyAndNoViewDataEntryWithPrefix()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -899,7 +899,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
Assert.Equal(expectedHtml, html.ToString());
|
Assert.Equal(expectedHtml, html.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "#1487, incorrectly matches Property1 entry (without prefix) in ViewData.")]
|
[Fact]
|
||||||
public void ListBoxInTemplate_GetsViewDataEntry_IfModelStateEmpty()
|
public void ListBoxInTemplate_GetsViewDataEntry_IfModelStateEmpty()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -927,7 +927,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
Assert.Equal(expectedHtml, html.ToString());
|
Assert.Equal(expectedHtml, html.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "#1487, incorrectly matches Property1 entry (without prefix) in ViewData.")]
|
[Fact]
|
||||||
public void ListBoxInTemplate_GetsPropertyOfViewDataEntry_IfModelStateEmptyAndNoViewDataEntryWithPrefix()
|
public void ListBoxInTemplate_GetsPropertyOfViewDataEntry_IfModelStateEmptyAndNoViewDataEntryWithPrefix()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ namespace Microsoft.AspNet.Mvc.Core
|
||||||
Assert.Equal("ViewDataValue", html);
|
Assert.Equal("ViewDataValue", html);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "$1487, finds 'StringProperty' entry (without prefix) instead.")]
|
[Fact]
|
||||||
public void ValueInTemplate_GetsValueFromPrefixedViewDataEntry()
|
public void ValueInTemplate_GetsValueFromPrefixedViewDataEntry()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -59,7 +59,7 @@ namespace Microsoft.AspNet.Mvc.Core
|
||||||
Assert.Equal("ContainedViewDataValue", html);
|
Assert.Equal("ContainedViewDataValue", html);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "$1487, finds 'StringProperty' entry (without prefix) instead.")]
|
[Fact]
|
||||||
public void ValueInTemplate_GetsValueFromPropertyOfViewDataEntry()
|
public void ValueInTemplate_GetsValueFromPropertyOfViewDataEntry()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -117,7 +117,7 @@ namespace Microsoft.AspNet.Mvc.Core
|
||||||
Assert.Empty(html);
|
Assert.Empty(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "$1487, finds 'StringProperty' entry (without prefix) instead.")]
|
[Fact]
|
||||||
public void ValueInTemplate_GetsValueFromViewData_EvenIfNull()
|
public void ValueInTemplate_GetsValueFromViewData_EvenIfNull()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,13 @@
|
||||||
<form action="/HtmlGeneration_Home/EmployeeList" method="post">
|
<form action="/HtmlGeneration_Home/EmployeeList" method="post">
|
||||||
<div>
|
<div>
|
||||||
<label for="z0__Number">Number</label>
|
<label for="z0__Number">Number</label>
|
||||||
|
<input data-val="true" data-val-range="The field Number must be between 1 and 100." data-val-range-max="100" data-val-range-min="1" data-val-required="The Number field is required." disabled="disabled" id="z0__Number" name="[0].Number" readonly="readonly" type="text" value="0" />
|
||||||
<input data-val="true" data-val-range="The field Number must be between 1 and 100." data-val-range-max="100" data-val-range-min="1" data-val-required="The Number field is required." disabled="disabled" id="z0__Number" name="[0].Number" readonly="readonly" type="text" value="" />
|
|
||||||
<input class="form-control" type="number" id="z0__Number" name="[0].Number" value="0" />
|
<input class="form-control" type="number" id="z0__Number" name="[0].Number" value="0" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="employee" for="z0__Name">Name</label>
|
<label class="employee" for="z0__Name">Name</label>
|
||||||
|
|
||||||
<textarea id="z0__Name" name="[0].Name">
|
<textarea id="z0__Name" name="[0].Name">
|
||||||
Name value that should not be seen.</textarea>
|
EmployeeName_0</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="employee" for="z0__PhoneNumber">PhoneNumber</label>
|
<label class="employee" for="z0__PhoneNumber">PhoneNumber</label>
|
||||||
|
|
@ -20,9 +18,7 @@ Name value that should not be seen.</textarea>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<label class="employee" for="z0__Gender">Gender</label>
|
<label class="employee" for="z0__Gender">Gender</label>
|
||||||
|
<input data-val="true" data-val-required="The Gender field is required." disabled="disabled" id="z0__Gender" name="[0].Gender" readonly="readonly" type="radio" value="Female" />
|
||||||
<input disabled="disabled" id="z0__Gender" name="[0].Gender" readonly="readonly" type="radio" value="Female" />
|
|
||||||
|
|
||||||
<select id="z0__Gender" name="[0].Gender"><option selected="selected">Male</option>
|
<select id="z0__Gender" name="[0].Gender"><option selected="selected">Male</option>
|
||||||
<option>Female</option>
|
<option>Female</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -40,15 +36,13 @@ Name value that should not be seen.</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="z1__Number">Number</label>
|
<label for="z1__Number">Number</label>
|
||||||
|
<input data-val="true" data-val-range="The field Number must be between 1 and 100." data-val-range-max="100" data-val-range-min="1" data-val-required="The Number field is required." disabled="disabled" id="z1__Number" name="[1].Number" readonly="readonly" type="text" value="1" />
|
||||||
<input data-val="true" data-val-range="The field Number must be between 1 and 100." data-val-range-max="100" data-val-range-min="1" data-val-required="The Number field is required." disabled="disabled" id="z1__Number" name="[1].Number" readonly="readonly" type="text" value="" />
|
|
||||||
<input class="form-control" type="number" id="z1__Number" name="[1].Number" value="1" />
|
<input class="form-control" type="number" id="z1__Number" name="[1].Number" value="1" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="employee" for="z1__Name">Name</label>
|
<label class="employee" for="z1__Name">Name</label>
|
||||||
|
|
||||||
<textarea id="z1__Name" name="[1].Name">
|
<textarea id="z1__Name" name="[1].Name">
|
||||||
Name value that should not be seen.</textarea>
|
EmployeeName_1</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="employee" for="z1__PhoneNumber">PhoneNumber</label>
|
<label class="employee" for="z1__PhoneNumber">PhoneNumber</label>
|
||||||
|
|
@ -57,9 +51,7 @@ Name value that should not be seen.</textarea>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<label class="employee" for="z1__Gender">Gender</label>
|
<label class="employee" for="z1__Gender">Gender</label>
|
||||||
|
<input checked="checked" data-val="true" data-val-required="The Gender field is required." disabled="disabled" id="z1__Gender" name="[1].Gender" readonly="readonly" type="radio" value="Female" />
|
||||||
<input disabled="disabled" id="z1__Gender" name="[1].Gender" readonly="readonly" type="radio" value="Female" />
|
|
||||||
|
|
||||||
<select id="z1__Gender" name="[1].Gender"><option>Male</option>
|
<select id="z1__Gender" name="[1].Gender"><option>Male</option>
|
||||||
<option selected="selected">Female</option>
|
<option selected="selected">Female</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -77,15 +69,13 @@ Name value that should not be seen.</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="z2__Number">Number</label>
|
<label for="z2__Number">Number</label>
|
||||||
|
<input data-val="true" data-val-range="The field Number must be between 1 and 100." data-val-range-max="100" data-val-range-min="1" data-val-required="The Number field is required." disabled="disabled" id="z2__Number" name="[2].Number" readonly="readonly" type="text" value="2" />
|
||||||
<input data-val="true" data-val-range="The field Number must be between 1 and 100." data-val-range-max="100" data-val-range-min="1" data-val-required="The Number field is required." disabled="disabled" id="z2__Number" name="[2].Number" readonly="readonly" type="text" value="" />
|
|
||||||
<input class="form-control" type="number" id="z2__Number" name="[2].Number" value="2" />
|
<input class="form-control" type="number" id="z2__Number" name="[2].Number" value="2" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="employee" for="z2__Name">Name</label>
|
<label class="employee" for="z2__Name">Name</label>
|
||||||
|
|
||||||
<textarea id="z2__Name" name="[2].Name">
|
<textarea id="z2__Name" name="[2].Name">
|
||||||
Name value that should not be seen.</textarea>
|
EmployeeName_2</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="employee" for="z2__PhoneNumber">PhoneNumber</label>
|
<label class="employee" for="z2__PhoneNumber">PhoneNumber</label>
|
||||||
|
|
@ -94,9 +84,7 @@ Name value that should not be seen.</textarea>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<label class="employee" for="z2__Gender">Gender</label>
|
<label class="employee" for="z2__Gender">Gender</label>
|
||||||
|
<input data-val="true" data-val-required="The Gender field is required." disabled="disabled" id="z2__Gender" name="[2].Gender" readonly="readonly" type="radio" value="Female" />
|
||||||
<input disabled="disabled" id="z2__Gender" name="[2].Gender" readonly="readonly" type="radio" value="Female" />
|
|
||||||
|
|
||||||
<select id="z2__Gender" name="[2].Gender"><option selected="selected">Male</option>
|
<select id="z2__Gender" name="[2].Gender"><option selected="selected">Male</option>
|
||||||
<option>Female</option>
|
<option>Female</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,7 @@
|
||||||
<div>
|
<div>
|
||||||
<label class="HtmlEncode[[order]]" for="HtmlEncode[[Customer_Gender]]">HtmlEncode[[Gender]]</label>
|
<label class="HtmlEncode[[order]]" for="HtmlEncode[[Customer_Gender]]">HtmlEncode[[Gender]]</label>
|
||||||
<input data-val="HtmlEncode[[true]]" data-val-required="HtmlEncode[[The Gender field is required.]]" id="HtmlEncode[[Customer_Gender]]" name="HtmlEncode[[Customer.Gender]]" type="HtmlEncode[[radio]]" value="HtmlEncode[[Male]]" /> Male
|
<input data-val="HtmlEncode[[true]]" data-val-required="HtmlEncode[[The Gender field is required.]]" id="HtmlEncode[[Customer_Gender]]" name="HtmlEncode[[Customer.Gender]]" type="HtmlEncode[[radio]]" value="HtmlEncode[[Male]]" /> Male
|
||||||
|
<input checked="HtmlEncode[[checked]]" id="HtmlEncode[[Customer_Gender]]" name="HtmlEncode[[Customer.Gender]]" type="HtmlEncode[[radio]]" value="HtmlEncode[[Female]]" /> Female
|
||||||
<input id="HtmlEncode[[Customer_Gender]]" name="HtmlEncode[[Customer.Gender]]" type="HtmlEncode[[radio]]" value="HtmlEncode[[Female]]" /> Female
|
|
||||||
<span class="HtmlEncode[[field-validation-valid]]" data-valmsg-for="HtmlEncode[[Customer.Gender]]" data-valmsg-replace="HtmlEncode[[true]]"></span>
|
<span class="HtmlEncode[[field-validation-valid]]" data-valmsg-for="HtmlEncode[[Customer.Gender]]" data-valmsg-replace="HtmlEncode[[true]]"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="HtmlEncode[[validation-summary-valid order]]" data-valmsg-summary="HtmlEncode[[true]]"><ul><li style="display:none"></li>
|
<div class="HtmlEncode[[validation-summary-valid order]]" data-valmsg-summary="HtmlEncode[[true]]"><ul><li style="display:none"></li>
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,7 @@
|
||||||
<div>
|
<div>
|
||||||
<label class="order" for="Customer_Gender">Gender</label>
|
<label class="order" for="Customer_Gender">Gender</label>
|
||||||
<input data-val="true" data-val-required="The Gender field is required." id="Customer_Gender" name="Customer.Gender" type="radio" value="Male" /> Male
|
<input data-val="true" data-val-required="The Gender field is required." id="Customer_Gender" name="Customer.Gender" type="radio" value="Male" /> Male
|
||||||
|
<input checked="checked" id="Customer_Gender" name="Customer.Gender" type="radio" value="Female" /> Female
|
||||||
<input id="Customer_Gender" name="Customer.Gender" type="radio" value="Female" /> Female
|
|
||||||
<span class="field-validation-valid" data-valmsg-for="Customer.Gender" data-valmsg-replace="true"></span>
|
<span class="field-validation-valid" data-valmsg-for="Customer.Gender" data-valmsg-replace="true"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="validation-summary-valid order" data-valmsg-summary="true"><ul><li style="display:none"></li>
|
<div class="validation-summary-valid order" data-valmsg-summary="true"><ul><li style="display:none"></li>
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,7 @@ namespace HtmlGenerationWebSite.Controllers
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extra data that should be ignored within a template. But #1487 currently affects RadioButton and
|
// Extra data that should be ignored / not used within a template.
|
||||||
// TextArea as well as ModelMetadata for <select> tag helper.
|
|
||||||
ViewData[nameof(Employee.Gender)] = "Gender value that will not match.";
|
ViewData[nameof(Employee.Gender)] = "Gender value that will not match.";
|
||||||
ViewData[nameof(Employee.Name)] = "Name value that should not be seen.";
|
ViewData[nameof(Employee.Name)] = "Name value that should not be seen.";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,11 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@Html.LabelFor(m => m.Number)
|
@Html.LabelFor(m => m.Number)
|
||||||
@* Due to #1485, text box will be empty though all employees have Number values. *@
|
|
||||||
@Html.TextBox(nameof(Model.Number), value: null, htmlAttributes: new { disabled = "disabled", @readonly = "readonly" })
|
@Html.TextBox(nameof(Model.Number), value: null, htmlAttributes: new { disabled = "disabled", @readonly = "readonly" })
|
||||||
<input asp-for="Number" type="number" class="form-control" />
|
<input asp-for="Number" type="number" class="form-control" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label asp-for="Name" class="employee"></label>
|
<label asp-for="Name" class="employee"></label>
|
||||||
@* Due to #1487, text area will contain incorrect "Value that should not be seen." *@
|
|
||||||
@Html.TextArea(nameof(Model.Name))
|
@Html.TextArea(nameof(Model.Name))
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -22,9 +20,7 @@
|
||||||
var genders = new SelectList(new string[] { "Male", "Female" });
|
var genders = new SelectList(new string[] { "Male", "Female" });
|
||||||
}
|
}
|
||||||
<label asp-for="Gender" class="employee"></label>
|
<label asp-for="Gender" class="employee"></label>
|
||||||
@* Due to #1487, radio button will not be checked. Employee is Female but incorrect information does not match. *@
|
|
||||||
@Html.RadioButton(nameof(Model.Gender), "Female", htmlAttributes: new { disabled = "disabled", @readonly = "readonly" })
|
@Html.RadioButton(nameof(Model.Gender), "Female", htmlAttributes: new { disabled = "disabled", @readonly = "readonly" })
|
||||||
@* Due to #1487, <select> tag helper will not generate expected validation attributes. *@
|
|
||||||
<select asp-for="Gender" asp-items="genders"></select>
|
<select asp-for="Gender" asp-items="genders"></select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
@using HtmlGenerationWebSite.Models
|
@using HtmlGenerationWebSite.Models
|
||||||
@model Gender
|
@model Gender
|
||||||
@Html.RadioButtonFor(m => m, value: "Male") Male
|
@Html.RadioButtonFor(m => m, value: "Male") Male
|
||||||
@* Due to #2662 radio button will not be checked because help ignores this expression." *@
|
|
||||||
@Html.RadioButton(expression: string.Empty, value: "Female") Female
|
@Html.RadioButton(expression: string.Empty, value: "Female") Female
|
||||||
Loading…
Reference in New Issue