Store `GetFullHtmlFieldName()` and `CreateSanitizedId()` values

- #3918
- don't repeat allocations for identical calls; helps w/ e.g. label / input / validation clusters
- add `NameAndIdProvider`; it stores `PreviousNameAndId` in `HttpContext.Items`
 - `PreviousNameAndId` allocated only when `string`s are allocated e.g. "id"s were sanitized
This commit is contained in:
Doug Bunting 2016-09-24 23:59:51 -07:00
parent 3ef7d01bb6
commit 4e1ec39a1f
7 changed files with 306 additions and 58 deletions

View File

@ -0,0 +1,214 @@
// 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 Microsoft.AspNetCore.Mvc.Rendering;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
/// <summary>
/// Provides cached values for "name" and "id" HTML attributes.
/// </summary>
public static class NameAndIdProvider
{
private static readonly object PreviousNameAndIdKey = typeof(PreviousNameAndId);
/// <summary>
/// Returns a valid HTML 4.01 "id" attribute value for an element with the given <paramref name="fullName"/>.
/// </summary>
/// <param name="viewContext">A <see cref="ViewContext"/> instance for the current scope.</param>
/// <param name="fullName">
/// The fully-qualified expression name, ignoring the current model. Also the original HTML element name.
/// </param>
/// <param name="invalidCharReplacement">
/// The <see cref="string"/> (normally a single <see cref="char"/>) to substitute for invalid characters in
/// <paramref name="fullName"/>.
/// </param>
/// <returns>
/// Valid HTML 4.01 "id" attribute value for an element with the given <paramref name="fullName"/>.
/// </returns>
/// <remarks>
/// Similar to <see cref="TagBuilder.CreateSanitizedId"/> but caches value for repeated invocations.
/// </remarks>
public static string CreateSanitizedId(ViewContext viewContext, string fullName, string invalidCharReplacement)
{
if (viewContext == null)
{
throw new ArgumentNullException(nameof(viewContext));
}
if (invalidCharReplacement == null)
{
throw new ArgumentNullException(nameof(invalidCharReplacement));
}
if (string.IsNullOrEmpty(fullName))
{
return string.Empty;
}
// Check cache to avoid whatever TagBuilder.CreateSanitizedId() may do.
var items = viewContext.HttpContext.Items;
object previousNameAndIdObject;
PreviousNameAndId previousNameAndId = null;
if (items.TryGetValue(PreviousNameAndIdKey, out previousNameAndIdObject) &&
(previousNameAndId = (PreviousNameAndId)previousNameAndIdObject) != null &&
string.Equals(previousNameAndId.FullName, fullName, StringComparison.Ordinal))
{
return previousNameAndId.SanitizedId;
}
var sanitizedId = TagBuilder.CreateSanitizedId(fullName, invalidCharReplacement);
if (previousNameAndId == null)
{
// Do not create a PreviousNameAndId when TagBuilder.CreateSanitizedId() only examined fullName.
if (string.Equals(fullName, sanitizedId, StringComparison.Ordinal))
{
return sanitizedId;
}
previousNameAndId = new PreviousNameAndId();
items[PreviousNameAndIdKey] = previousNameAndId;
}
previousNameAndId.FullName = fullName;
previousNameAndId.SanitizedId = sanitizedId;
return previousNameAndId.SanitizedId;
}
/// <summary>
/// Adds a valid HTML 4.01 "id" attribute for an element with the given <paramref name="fullName"/>. Does
/// nothing if <see cref="TagBuilder.Attributes"/> already contains an "id" attribute or the
/// <paramref name="fullName"/> is <c>null</c> or empty.
/// </summary>
/// <param name="viewContext">A <see cref="ViewContext"/> instance for the current scope.</param>
/// <param name="tagBuilder">A <see cref="TagBuilder"/> instance that will contain the "id" attribute.</param>
/// <param name="fullName">
/// The fully-qualified expression name, ignoring the current model. Also the original HTML element name.
/// </param>
/// <param name="invalidCharReplacement">
/// The <see cref="string"/> (normally a single <see cref="char"/>) to substitute for invalid characters in
/// <paramref name="fullName"/>.
/// </param>
/// <remarks>
/// Similar to <see cref="TagBuilder.GenerateId"/> but caches value for repeated invocations.
/// </remarks>
/// <seealso cref="CreateSanitizedId"/>
public static void GenerateId(
ViewContext viewContext,
TagBuilder tagBuilder,
string fullName,
string invalidCharReplacement)
{
if (viewContext == null)
{
throw new ArgumentNullException(nameof(viewContext));
}
if (tagBuilder == null)
{
throw new ArgumentNullException(nameof(tagBuilder));
}
if (invalidCharReplacement == null)
{
throw new ArgumentNullException(nameof(invalidCharReplacement));
}
if (string.IsNullOrEmpty(fullName))
{
return;
}
if (!tagBuilder.Attributes.ContainsKey("id"))
{
var sanitizedId = CreateSanitizedId(viewContext, fullName, invalidCharReplacement);
// Duplicate check for null or empty to cover the corner case where fullName contains only invalid
// characters and invalidCharReplacement is empty.
if (!string.IsNullOrEmpty(sanitizedId))
{
tagBuilder.Attributes["id"] = sanitizedId;
}
}
}
/// <summary>
/// Returns the full HTML element name for the specified <paramref name="expression"/>.
/// </summary>
/// <param name="viewContext">A <see cref="ViewContext"/> instance for the current scope.</param>
/// <param name="expression">Expression name, relative to the current model.</param>
/// <returns>Fully-qualified expression name for <paramref name="expression"/>.</returns>
/// <remarks>
/// Similar to <see cref="TemplateInfo.GetFullHtmlFieldName"/> but caches value for repeated invocations.
/// </remarks>
public static string GetFullHtmlFieldName(ViewContext viewContext, string expression)
{
var htmlFieldPrefix = viewContext.ViewData.TemplateInfo.HtmlFieldPrefix;
if (string.IsNullOrEmpty(expression))
{
return htmlFieldPrefix;
}
if (string.IsNullOrEmpty(htmlFieldPrefix))
{
return expression;
}
// Need to concatenate. See if we've already done that.
var items = viewContext.HttpContext.Items;
object previousNameAndIdObject;
PreviousNameAndId previousNameAndId = null;
if (items.TryGetValue(PreviousNameAndIdKey, out previousNameAndIdObject) &&
(previousNameAndId = (PreviousNameAndId)previousNameAndIdObject) != null &&
string.Equals(previousNameAndId.HtmlFieldPrefix, htmlFieldPrefix, StringComparison.Ordinal) &&
string.Equals(previousNameAndId.Expression, expression, StringComparison.Ordinal))
{
return previousNameAndId.OutputFullName;
}
if (previousNameAndId == null)
{
previousNameAndId = new PreviousNameAndId();
items[PreviousNameAndIdKey] = previousNameAndId;
}
previousNameAndId.HtmlFieldPrefix = htmlFieldPrefix;
previousNameAndId.Expression = expression;
if (expression.StartsWith("[", StringComparison.Ordinal))
{
// The expression might represent an indexer access, in which case with a 'dot' would be invalid.
previousNameAndId.OutputFullName = htmlFieldPrefix + expression;
}
else
{
previousNameAndId.OutputFullName = htmlFieldPrefix + "." + expression;
}
return previousNameAndId.OutputFullName;
}
private class PreviousNameAndId
{
// Cached ambient input for NameAndIdProvider.GetFullHtmlFieldName(). TemplateInfo.HtmlFieldPrefix may
// change during the lifetime of a ViewContext.
public string HtmlFieldPrefix { get; set; }
// Cached input for NameAndIdProvider.GetFullHtmlFieldName().
public string Expression { get; set; }
// Cached return value for NameAndIdProvider.GetFullHtmlFieldName().
public string OutputFullName { get; set; }
// Cached input for NameAndIdProvider.CreateSanitizedId(). Since IHtmlHelper.GenerateIdFromName() is
// available to all, there is no guarantee this is equal to OutputFullName when CreateSanitizedId() is
// called.
public string FullName { get; set; }
// Cached return value for NameAndIdProvider.CreateSanitizedId().
public string SanitizedId { get; set; }
}
}
}

View File

@ -109,7 +109,9 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// <summary> /// <summary>
/// Returns a valid HTML 4.01 "id" attribute value for an element with the given <paramref name="name"/>. /// Returns a valid HTML 4.01 "id" attribute value for an element with the given <paramref name="name"/>.
/// </summary> /// </summary>
/// <param name="name">The original element name.</param> /// <param name="name">
/// The fully-qualified expression name, ignoring the current model. Also the original HTML element name.
/// </param>
/// <param name="invalidCharReplacement"> /// <param name="invalidCharReplacement">
/// The <see cref="string"/> (normally a single <see cref="char"/>) to substitute for invalid characters in /// The <see cref="string"/> (normally a single <see cref="char"/>) to substitute for invalid characters in
/// <paramref name="name"/>. /// <paramref name="name"/>.
@ -159,7 +161,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
stringBuffer.Append(firstChar); stringBuffer.Append(firstChar);
// Characters until 'firstIndexOfInvalidCharacter' have already been checked for validity. // Characters until 'firstIndexOfInvalidCharacter' have already been checked for validity.
// So just copying them. This avoids running them through Html401IdUtil.IsValidIdCharacter again. // So just copy them. This avoids running them through Html401IdUtil.IsValidIdCharacter again.
for (var index = 1; index < firstIndexOfInvalidCharacter; index++) for (var index = 1; index < firstIndexOfInvalidCharacter; index++)
{ {
stringBuffer.Append(name[index]); stringBuffer.Append(name[index]);
@ -182,13 +184,18 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
} }
/// <summary> /// <summary>
/// Generates a sanitized ID attribute for the tag by using the specified name. /// Adds a valid HTML 4.01 "id" attribute for an element with the given <paramref name="name"/>. Does
/// nothing if <see cref="Attributes"/> already contains an "id" attribute or the <paramref name="name"/>
/// is <c>null</c> or empty.
/// </summary> /// </summary>
/// <param name="name">The name to use to generate an ID attribute.</param> /// <param name="name">
/// The fully-qualified expression name, ignoring the current model. Also the original HTML element name.
/// </param>
/// <param name="invalidCharReplacement"> /// <param name="invalidCharReplacement">
/// The <see cref="string"/> (normally a single <see cref="char"/>) to substitute for invalid characters in /// The <see cref="string"/> (normally a single <see cref="char"/>) to substitute for invalid characters in
/// <paramref name="name"/>. /// <paramref name="name"/>.
/// </param> /// </param>
/// <seealso cref="CreateSanitizedId(string, string)"/>
public void GenerateId(string name, string invalidCharReplacement) public void GenerateId(string name, string invalidCharReplacement)
{ {
if (invalidCharReplacement == null) if (invalidCharReplacement == null)
@ -196,9 +203,17 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
throw new ArgumentNullException(nameof(invalidCharReplacement)); throw new ArgumentNullException(nameof(invalidCharReplacement));
} }
if (string.IsNullOrEmpty(name))
{
return;
}
if (!Attributes.ContainsKey("id")) if (!Attributes.ContainsKey("id"))
{ {
var sanitizedId = CreateSanitizedId(name, invalidCharReplacement); var sanitizedId = CreateSanitizedId(name, invalidCharReplacement);
// Duplicate check for null or empty to cover the corner case where name contains only invalid
// characters and invalidCharReplacement is empty.
if (!string.IsNullOrEmpty(sanitizedId)) if (!string.IsNullOrEmpty(sanitizedId))
{ {
Attributes["id"] = sanitizedId; Attributes["id"] = sanitizedId;

View File

@ -54,8 +54,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/>.</param> /// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
/// <param name="urlHelperFactory">The <see cref="IUrlHelperFactory"/>.</param> /// <param name="urlHelperFactory">The <see cref="IUrlHelperFactory"/>.</param>
/// <param name="htmlEncoder">The <see cref="HtmlEncoder"/>.</param> /// <param name="htmlEncoder">The <see cref="HtmlEncoder"/>.</param>
/// <param name="clientValidatorCache">The <see cref="ClientValidatorCache"/> that provides /// <param name="clientValidatorCache">
/// a list of <see cref="IClientModelValidator"/>s.</param> /// The <see cref="ClientValidatorCache"/> that provides a list of <see cref="IClientModelValidator"/>s.
/// </param>
[Obsolete("This constructor is obsolete and will be removed in a future version. The recommended " + [Obsolete("This constructor is obsolete and will be removed in a future version. The recommended " +
"alternative is to use the other public constructor.")] "alternative is to use the other public constructor.")]
public DefaultHtmlGenerator( public DefaultHtmlGenerator(
@ -64,14 +65,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
IModelMetadataProvider metadataProvider, IModelMetadataProvider metadataProvider,
IUrlHelperFactory urlHelperFactory, IUrlHelperFactory urlHelperFactory,
HtmlEncoder htmlEncoder, HtmlEncoder htmlEncoder,
ClientValidatorCache clientValidatorCache) : this( ClientValidatorCache clientValidatorCache)
antiforgery, : this(
optionsAccessor, antiforgery,
metadataProvider, optionsAccessor,
urlHelperFactory, metadataProvider,
htmlEncoder, urlHelperFactory,
clientValidatorCache, htmlEncoder,
new DefaultValidationHtmlAttributeProvider(optionsAccessor, metadataProvider, clientValidatorCache)) clientValidatorCache,
new DefaultValidationHtmlAttributeProvider(optionsAccessor, metadataProvider, clientValidatorCache))
{ {
} }
@ -84,8 +86,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/>.</param> /// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
/// <param name="urlHelperFactory">The <see cref="IUrlHelperFactory"/>.</param> /// <param name="urlHelperFactory">The <see cref="IUrlHelperFactory"/>.</param>
/// <param name="htmlEncoder">The <see cref="HtmlEncoder"/>.</param> /// <param name="htmlEncoder">The <see cref="HtmlEncoder"/>.</param>
/// <param name="clientValidatorCache">The <see cref="ClientValidatorCache"/> that provides /// <param name="clientValidatorCache">
/// a list of <see cref="IClientModelValidator"/>s.</param> /// The <see cref="ClientValidatorCache"/> that provides a list of <see cref="IClientModelValidator"/>s.
/// </param>
/// <param name="validationAttributeProvider">The <see cref="ValidationHtmlAttributeProvider"/>.</param> /// <param name="validationAttributeProvider">The <see cref="ValidationHtmlAttributeProvider"/>.</param>
public DefaultHtmlGenerator( public DefaultHtmlGenerator(
IAntiforgery antiforgery, IAntiforgery antiforgery,
@ -280,7 +283,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
tagBuilder.MergeAttribute("value", "false"); tagBuilder.MergeAttribute("value", "false");
tagBuilder.TagRenderMode = TagRenderMode.SelfClosing; tagBuilder.TagRenderMode = TagRenderMode.SelfClosing;
var fullName = GetFullHtmlFieldName(viewContext, expression); var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
tagBuilder.MergeAttribute("name", fullName); tagBuilder.MergeAttribute("name", fullName);
return tagBuilder; return tagBuilder;
@ -424,8 +427,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
} }
var tagBuilder = new TagBuilder("label"); var tagBuilder = new TagBuilder("label");
var idString = var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
TagBuilder.CreateSanitizedId(GetFullHtmlFieldName(viewContext, expression), IdAttributeDotReplacement); var idString = NameAndIdProvider.CreateSanitizedId(viewContext, fullName, IdAttributeDotReplacement);
tagBuilder.Attributes.Add("for", idString); tagBuilder.Attributes.Add("for", idString);
tagBuilder.InnerHtml.SetContent(resolvedLabelText); tagBuilder.InnerHtml.SetContent(resolvedLabelText);
tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(htmlAttributes), replaceExisting: true); tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(htmlAttributes), replaceExisting: true);
@ -599,7 +602,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(viewContext)); throw new ArgumentNullException(nameof(viewContext));
} }
var fullName = GetFullHtmlFieldName(viewContext, expression); var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName)) if (string.IsNullOrEmpty(fullName))
{ {
throw new ArgumentException( throw new ArgumentException(
@ -628,7 +631,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
tagBuilder.InnerHtml.SetHtmlContent(listItemBuilder); tagBuilder.InnerHtml.SetHtmlContent(listItemBuilder);
tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(htmlAttributes)); tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(htmlAttributes));
tagBuilder.MergeAttribute("name", fullName, true /* replaceExisting */); tagBuilder.MergeAttribute("name", fullName, true /* replaceExisting */);
tagBuilder.GenerateId(fullName, IdAttributeDotReplacement); NameAndIdProvider.GenerateId(viewContext, tagBuilder, fullName, IdAttributeDotReplacement);
if (allowMultiple) if (allowMultiple)
{ {
tagBuilder.MergeAttribute("multiple", "multiple"); tagBuilder.MergeAttribute("multiple", "multiple");
@ -675,7 +678,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
Resources.HtmlHelper_TextAreaParameterOutOfRange); Resources.HtmlHelper_TextAreaParameterOutOfRange);
} }
var fullName = GetFullHtmlFieldName(viewContext, expression); var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName)) if (string.IsNullOrEmpty(fullName))
{ {
throw new ArgumentException( throw new ArgumentException(
@ -702,7 +705,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
} }
var tagBuilder = new TagBuilder("textarea"); var tagBuilder = new TagBuilder("textarea");
tagBuilder.GenerateId(fullName, IdAttributeDotReplacement); NameAndIdProvider.GenerateId(viewContext, tagBuilder, fullName, IdAttributeDotReplacement);
tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(htmlAttributes), true); tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(htmlAttributes), true);
if (rows > 0) if (rows > 0)
{ {
@ -776,7 +779,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(viewContext)); throw new ArgumentNullException(nameof(viewContext));
} }
var fullName = GetFullHtmlFieldName(viewContext, expression); var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName)) if (string.IsNullOrEmpty(fullName))
{ {
throw new ArgumentException( throw new ArgumentException(
@ -967,7 +970,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(viewContext)); throw new ArgumentNullException(nameof(viewContext));
} }
var fullName = GetFullHtmlFieldName(viewContext, expression); var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName)) if (string.IsNullOrEmpty(fullName))
{ {
throw new ArgumentException( throw new ArgumentException(
@ -1135,12 +1138,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return tagBuilder; return tagBuilder;
} }
internal static string GetFullHtmlFieldName(ViewContext viewContext, string expression)
{
var fullName = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(expression);
return fullName;
}
internal static object GetModelStateValue(ViewContext viewContext, string key, Type destinationType) internal static object GetModelStateValue(ViewContext viewContext, string key, Type destinationType)
{ {
ModelStateEntry entry; ModelStateEntry entry;
@ -1216,7 +1213,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
// Not valid to use TextBoxForModel() and so on in a top-level view; would end up with an unnamed input // Not valid to use TextBoxForModel() and so on in a top-level view; would end up with an unnamed input
// elements. But we support the *ForModel() methods in any lower-level template, once HtmlFieldPrefix is // elements. But we support the *ForModel() methods in any lower-level template, once HtmlFieldPrefix is
// non-empty. // non-empty.
var fullName = GetFullHtmlFieldName(viewContext, expression); var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName)) if (string.IsNullOrEmpty(fullName))
{ {
throw new ArgumentException( throw new ArgumentException(
@ -1319,7 +1316,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
if (setId) if (setId)
{ {
tagBuilder.GenerateId(fullName, IdAttributeDotReplacement); NameAndIdProvider.GenerateId(viewContext, tagBuilder, fullName, IdAttributeDotReplacement);
} }
// If there are any errors for a named field, we add the CSS attribute. // If there are any errors for a named field, we add the CSS attribute.

View File

@ -342,7 +342,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(fullName)); throw new ArgumentNullException(nameof(fullName));
} }
return TagBuilder.CreateSanitizedId(fullName, IdAttributeDotReplacement); return NameAndIdProvider.CreateSanitizedId(ViewContext, fullName, IdAttributeDotReplacement);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -1001,10 +1001,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
protected virtual string GenerateId(string expression) protected virtual string GenerateId(string expression)
{ {
var fullName = DefaultHtmlGenerator.GetFullHtmlFieldName(ViewContext, expression: expression); var fullName = NameAndIdProvider.GetFullHtmlFieldName(ViewContext, expression);
var id = TagBuilder.CreateSanitizedId(fullName, IdAttributeDotReplacement);
return id; return GenerateIdFromName(fullName);
} }
protected virtual IHtmlContent GenerateLabel( protected virtual IHtmlContent GenerateLabel(
@ -1056,7 +1055,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
protected virtual string GenerateName(string expression) protected virtual string GenerateName(string expression)
{ {
var fullName = DefaultHtmlGenerator.GetFullHtmlFieldName(ViewContext, expression); var fullName = NameAndIdProvider.GetFullHtmlFieldName(ViewContext, expression);
return fullName; return fullName;
} }
@ -1190,7 +1189,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
protected virtual string GenerateValue(string expression, object value, string format, bool useViewData) protected virtual string GenerateValue(string expression, object value, string format, bool useViewData)
{ {
var fullName = DefaultHtmlGenerator.GetFullHtmlFieldName(ViewContext, expression); var fullName = NameAndIdProvider.GetFullHtmlFieldName(ViewContext, expression);
var attemptedValue = var attemptedValue =
(string)DefaultHtmlGenerator.GetModelStateValue(ViewContext, fullName, typeof(string)); (string)DefaultHtmlGenerator.GetModelStateValue(ViewContext, fullName, typeof(string));

View File

@ -29,12 +29,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
UrlEncoder urlEncoder, UrlEncoder urlEncoder,
ExpressionTextCache expressionTextCache) ExpressionTextCache expressionTextCache)
: base( : base(
htmlGenerator, htmlGenerator,
viewEngine, viewEngine,
metadataProvider, metadataProvider,
bufferScope, bufferScope,
htmlEncoder, htmlEncoder,
urlEncoder) urlEncoder)
{ {
if (expressionTextCache == null) if (expressionTextCache == null)
{ {

View File

@ -1,6 +1,7 @@
// 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 Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures namespace Microsoft.AspNetCore.Mvc.ViewFeatures
@ -10,6 +11,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// </summary> /// </summary>
public class HtmlHelperOptions public class HtmlHelperOptions
{ {
private string _idAttributeDotReplacement = "_";
/// <summary> /// <summary>
/// Gets or sets the <see cref="Html5DateRenderingMode.Html5DateRenderingMode"/> value. /// Gets or sets the <see cref="Html5DateRenderingMode.Html5DateRenderingMode"/> value.
/// </summary> /// </summary>
@ -23,7 +26,22 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// <summary> /// <summary>
/// Gets or sets the <see cref="string"/> that replaces periods in the ID attribute of an element. /// Gets or sets the <see cref="string"/> that replaces periods in the ID attribute of an element.
/// </summary> /// </summary>
public string IdAttributeDotReplacement { get; set; } = "_"; public string IdAttributeDotReplacement
{
get
{
return _idAttributeDotReplacement;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_idAttributeDotReplacement = value;
}
}
/// <summary> /// <summary>
/// Gets or sets a value that indicates whether client-side validation is enabled. /// Gets or sets a value that indicates whether client-side validation is enabled.
@ -31,13 +49,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
public bool ClientValidationEnabled { get; set; } = true; public bool ClientValidationEnabled { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets the element name used to wrap a top-level message generated by /// Gets or sets the element name used to wrap a top-level message generated by
/// <see cref="IHtmlHelper.ValidationMessage"/> and other overloads. /// <see cref="IHtmlHelper.ValidationMessage"/> and other overloads.
/// </summary> /// </summary>
public string ValidationMessageElement { get; set; } = "span"; public string ValidationMessageElement { get; set; } = "span";
/// <summary> /// <summary>
/// Gets or sets the element name used to wrap a top-level message generated by /// Gets or sets the element name used to wrap a top-level message generated by
/// <see cref="IHtmlHelper.ValidationSummary"/> and other overloads. /// <see cref="IHtmlHelper.ValidationSummary"/> and other overloads.
/// </summary> /// </summary>
public string ValidationSummaryMessageElement { get; set; } = "span"; public string ValidationSummaryMessageElement { get; set; } = "span";

View File

@ -8,11 +8,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{ {
public class TemplateInfo public class TemplateInfo
{ {
private string _htmlFieldPrefix;
private object _formattedModelValue;
// Keep a collection of visited objects to prevent infinite recursion. // Keep a collection of visited objects to prevent infinite recursion.
private HashSet<object> _visitedObjects; private readonly HashSet<object> _visitedObjects;
private object _formattedModelValue;
private string _htmlFieldPrefix;
public TemplateInfo() public TemplateInfo()
{ {
@ -65,26 +65,31 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return _visitedObjects.Add(value); return _visitedObjects.Add(value);
} }
/// <summary>
/// Returns the full HTML element name for the specified <paramref name="partialFieldName"/>.
/// </summary>
/// <param name="partialFieldName">Expression name, relative to the current model.</param>
/// <returns>Fully-qualified expression name for <paramref name="partialFieldName"/>.</returns>
public string GetFullHtmlFieldName(string partialFieldName) public string GetFullHtmlFieldName(string partialFieldName)
{ {
if (string.IsNullOrEmpty(partialFieldName)) if (string.IsNullOrEmpty(partialFieldName))
{ {
return HtmlFieldPrefix; return HtmlFieldPrefix;
} }
else if (string.IsNullOrEmpty(HtmlFieldPrefix))
if (string.IsNullOrEmpty(HtmlFieldPrefix))
{ {
return partialFieldName; return partialFieldName;
} }
else if (partialFieldName.StartsWith("[", StringComparison.Ordinal))
if (partialFieldName.StartsWith("[", StringComparison.Ordinal))
{ {
// The partialFieldName might represent an indexer access, in which case combining // The partialFieldName might represent an indexer access, in which case combining
// with a 'dot' would be invalid. // with a 'dot' would be invalid.
return HtmlFieldPrefix + partialFieldName; return HtmlFieldPrefix + partialFieldName;
} }
else
{ return HtmlFieldPrefix + "." + partialFieldName;
return HtmlFieldPrefix + "." + partialFieldName;
}
} }
public bool Visited(ModelExplorer modelExplorer) public bool Visited(ModelExplorer modelExplorer)