diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/NameAndIdProvider.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/NameAndIdProvider.cs
new file mode 100644
index 0000000000..83f9e518e9
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/NameAndIdProvider.cs
@@ -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
+{
+ ///
+ /// Provides cached values for "name" and "id" HTML attributes.
+ ///
+ public static class NameAndIdProvider
+ {
+ private static readonly object PreviousNameAndIdKey = typeof(PreviousNameAndId);
+
+ ///
+ /// Returns a valid HTML 4.01 "id" attribute value for an element with the given .
+ ///
+ /// A instance for the current scope.
+ ///
+ /// The fully-qualified expression name, ignoring the current model. Also the original HTML element name.
+ ///
+ ///
+ /// The (normally a single ) to substitute for invalid characters in
+ /// .
+ ///
+ ///
+ /// Valid HTML 4.01 "id" attribute value for an element with the given .
+ ///
+ ///
+ /// Similar to but caches value for repeated invocations.
+ ///
+ 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;
+ }
+
+ ///
+ /// Adds a valid HTML 4.01 "id" attribute for an element with the given . Does
+ /// nothing if already contains an "id" attribute or the
+ /// is null or empty.
+ ///
+ /// A instance for the current scope.
+ /// A instance that will contain the "id" attribute.
+ ///
+ /// The fully-qualified expression name, ignoring the current model. Also the original HTML element name.
+ ///
+ ///
+ /// The (normally a single ) to substitute for invalid characters in
+ /// .
+ ///
+ ///
+ /// Similar to but caches value for repeated invocations.
+ ///
+ ///
+ 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;
+ }
+ }
+ }
+
+ ///
+ /// Returns the full HTML element name for the specified .
+ ///
+ /// A instance for the current scope.
+ /// Expression name, relative to the current model.
+ /// Fully-qualified expression name for .
+ ///
+ /// Similar to but caches value for repeated invocations.
+ ///
+ 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; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/TagBuilder.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/TagBuilder.cs
index eed02e76f4..e25a305835 100644
--- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/TagBuilder.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/TagBuilder.cs
@@ -109,7 +109,9 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
///
/// Returns a valid HTML 4.01 "id" attribute value for an element with the given .
///
- /// The original element name.
+ ///
+ /// The fully-qualified expression name, ignoring the current model. Also the original HTML element name.
+ ///
///
/// The (normally a single ) to substitute for invalid characters in
/// .
@@ -159,7 +161,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
stringBuffer.Append(firstChar);
// 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++)
{
stringBuffer.Append(name[index]);
@@ -182,13 +184,18 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
}
///
- /// 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 . Does
+ /// nothing if already contains an "id" attribute or the
+ /// is null or empty.
///
- /// The name to use to generate an ID attribute.
+ ///
+ /// The fully-qualified expression name, ignoring the current model. Also the original HTML element name.
+ ///
///
/// The (normally a single ) to substitute for invalid characters in
/// .
///
+ ///
public void GenerateId(string name, string invalidCharReplacement)
{
if (invalidCharReplacement == null)
@@ -196,9 +203,17 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
throw new ArgumentNullException(nameof(invalidCharReplacement));
}
+ if (string.IsNullOrEmpty(name))
+ {
+ return;
+ }
+
if (!Attributes.ContainsKey("id"))
{
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))
{
Attributes["id"] = sanitizedId;
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs
index 514cde5faf..587f88cfc6 100644
--- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs
@@ -54,8 +54,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// The .
/// The .
/// The .
- /// The that provides
- /// a list of s.
+ ///
+ /// The that provides a list of s.
+ ///
[Obsolete("This constructor is obsolete and will be removed in a future version. The recommended " +
"alternative is to use the other public constructor.")]
public DefaultHtmlGenerator(
@@ -64,14 +65,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
IModelMetadataProvider metadataProvider,
IUrlHelperFactory urlHelperFactory,
HtmlEncoder htmlEncoder,
- ClientValidatorCache clientValidatorCache) : this(
- antiforgery,
- optionsAccessor,
- metadataProvider,
- urlHelperFactory,
- htmlEncoder,
- clientValidatorCache,
- new DefaultValidationHtmlAttributeProvider(optionsAccessor, metadataProvider, clientValidatorCache))
+ ClientValidatorCache clientValidatorCache)
+ : this(
+ antiforgery,
+ optionsAccessor,
+ metadataProvider,
+ urlHelperFactory,
+ htmlEncoder,
+ clientValidatorCache,
+ new DefaultValidationHtmlAttributeProvider(optionsAccessor, metadataProvider, clientValidatorCache))
{
}
@@ -84,8 +86,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// The .
/// The .
/// The .
- /// The that provides
- /// a list of s.
+ ///
+ /// The that provides a list of s.
+ ///
/// The .
public DefaultHtmlGenerator(
IAntiforgery antiforgery,
@@ -280,7 +283,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
tagBuilder.MergeAttribute("value", "false");
tagBuilder.TagRenderMode = TagRenderMode.SelfClosing;
- var fullName = GetFullHtmlFieldName(viewContext, expression);
+ var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
tagBuilder.MergeAttribute("name", fullName);
return tagBuilder;
@@ -424,8 +427,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
var tagBuilder = new TagBuilder("label");
- var idString =
- TagBuilder.CreateSanitizedId(GetFullHtmlFieldName(viewContext, expression), IdAttributeDotReplacement);
+ var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
+ var idString = NameAndIdProvider.CreateSanitizedId(viewContext, fullName, IdAttributeDotReplacement);
tagBuilder.Attributes.Add("for", idString);
tagBuilder.InnerHtml.SetContent(resolvedLabelText);
tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(htmlAttributes), replaceExisting: true);
@@ -599,7 +602,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(viewContext));
}
- var fullName = GetFullHtmlFieldName(viewContext, expression);
+ var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName))
{
throw new ArgumentException(
@@ -628,7 +631,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
tagBuilder.InnerHtml.SetHtmlContent(listItemBuilder);
tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(htmlAttributes));
tagBuilder.MergeAttribute("name", fullName, true /* replaceExisting */);
- tagBuilder.GenerateId(fullName, IdAttributeDotReplacement);
+ NameAndIdProvider.GenerateId(viewContext, tagBuilder, fullName, IdAttributeDotReplacement);
if (allowMultiple)
{
tagBuilder.MergeAttribute("multiple", "multiple");
@@ -675,7 +678,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
Resources.HtmlHelper_TextAreaParameterOutOfRange);
}
- var fullName = GetFullHtmlFieldName(viewContext, expression);
+ var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName))
{
throw new ArgumentException(
@@ -702,7 +705,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
var tagBuilder = new TagBuilder("textarea");
- tagBuilder.GenerateId(fullName, IdAttributeDotReplacement);
+ NameAndIdProvider.GenerateId(viewContext, tagBuilder, fullName, IdAttributeDotReplacement);
tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(htmlAttributes), true);
if (rows > 0)
{
@@ -776,7 +779,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(viewContext));
}
- var fullName = GetFullHtmlFieldName(viewContext, expression);
+ var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName))
{
throw new ArgumentException(
@@ -967,7 +970,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(viewContext));
}
- var fullName = GetFullHtmlFieldName(viewContext, expression);
+ var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName))
{
throw new ArgumentException(
@@ -1135,12 +1138,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
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)
{
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
// elements. But we support the *ForModel() methods in any lower-level template, once HtmlFieldPrefix is
// non-empty.
- var fullName = GetFullHtmlFieldName(viewContext, expression);
+ var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName))
{
throw new ArgumentException(
@@ -1319,7 +1316,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
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.
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs
index 666b6bef78..feaa3b7615 100644
--- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs
@@ -342,7 +342,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(fullName));
}
- return TagBuilder.CreateSanitizedId(fullName, IdAttributeDotReplacement);
+ return NameAndIdProvider.CreateSanitizedId(ViewContext, fullName, IdAttributeDotReplacement);
}
///
@@ -1001,10 +1001,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
protected virtual string GenerateId(string expression)
{
- var fullName = DefaultHtmlGenerator.GetFullHtmlFieldName(ViewContext, expression: expression);
- var id = TagBuilder.CreateSanitizedId(fullName, IdAttributeDotReplacement);
+ var fullName = NameAndIdProvider.GetFullHtmlFieldName(ViewContext, expression);
- return id;
+ return GenerateIdFromName(fullName);
}
protected virtual IHtmlContent GenerateLabel(
@@ -1056,7 +1055,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
protected virtual string GenerateName(string expression)
{
- var fullName = DefaultHtmlGenerator.GetFullHtmlFieldName(ViewContext, expression);
+ var fullName = NameAndIdProvider.GetFullHtmlFieldName(ViewContext, expression);
return fullName;
}
@@ -1190,7 +1189,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
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 =
(string)DefaultHtmlGenerator.GetModelStateValue(ViewContext, fullName, typeof(string));
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOfT.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOfT.cs
index d8625dc0b0..bb61b5867d 100644
--- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOfT.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOfT.cs
@@ -29,12 +29,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
UrlEncoder urlEncoder,
ExpressionTextCache expressionTextCache)
: base(
- htmlGenerator,
- viewEngine,
- metadataProvider,
- bufferScope,
- htmlEncoder,
- urlEncoder)
+ htmlGenerator,
+ viewEngine,
+ metadataProvider,
+ bufferScope,
+ htmlEncoder,
+ urlEncoder)
{
if (expressionTextCache == null)
{
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOptions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOptions.cs
index 85748fb09c..1b132a4491 100644
--- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOptions.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOptions.cs
@@ -1,6 +1,7 @@
// 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
@@ -10,6 +11,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
///
public class HtmlHelperOptions
{
+ private string _idAttributeDotReplacement = "_";
+
///
/// Gets or sets the value.
///
@@ -23,7 +26,22 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
///
/// Gets or sets the that replaces periods in the ID attribute of an element.
///
- public string IdAttributeDotReplacement { get; set; } = "_";
+ public string IdAttributeDotReplacement
+ {
+ get
+ {
+ return _idAttributeDotReplacement;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ _idAttributeDotReplacement = value;
+ }
+ }
///
/// 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;
///
- /// 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
/// and other overloads.
///
public string ValidationMessageElement { get; set; } = "span";
///
- /// 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
/// and other overloads.
///
public string ValidationSummaryMessageElement { get; set; } = "span";
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateInfo.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateInfo.cs
index 34ab4b63b9..76abaa2389 100644
--- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateInfo.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateInfo.cs
@@ -8,11 +8,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
public class TemplateInfo
{
- private string _htmlFieldPrefix;
- private object _formattedModelValue;
-
// Keep a collection of visited objects to prevent infinite recursion.
- private HashSet