Add `ModelMetadata.HtmlEncode` property

- use new `ModelMetadata.HtmlEncode` property in HTML helpers
 - specifically in default HTML display and editor object templates (e.g.
   `@Html.DisplayFor()`) when value is non-`null` and the template is invoked
   with template depth greater than 1
- similar to MVC 5.2 commit [2b12791aee4f](https://aspnetwebstack.codeplex.com/SourceControl/changeset/2b12791aee4ffc56c7928b623bb45ee425813021)

nits:
- remove dupe `null` check in `DefaultDisplayTemplates.ObjectTemplate()`
- move backing fields initialized with constants together in `ModelMetadata`
This commit is contained in:
Doug Bunting 2014-10-31 22:58:18 -07:00
parent d5515bfbb6
commit 90e41b905a
9 changed files with 144 additions and 17 deletions

View File

@ -213,7 +213,13 @@ namespace Microsoft.AspNet.Mvc.Rendering
if (templateInfo.TemplateDepth > 1) if (templateInfo.TemplateDepth > 1)
{ {
return modelMetadata.Model == null ? modelMetadata.NullDisplayText : modelMetadata.SimpleDisplayText; var text = modelMetadata.SimpleDisplayText;
if (modelMetadata.HtmlEncode)
{
text = html.Encode(text);
}
return text;
} }
var serviceProvider = html.ViewContext.HttpContext.RequestServices; var serviceProvider = html.ViewContext.HttpContext.RequestServices;

View File

@ -232,7 +232,18 @@ namespace Microsoft.AspNet.Mvc.Rendering
if (templateInfo.TemplateDepth > 1) if (templateInfo.TemplateDepth > 1)
{ {
return modelMetadata.Model == null ? modelMetadata.NullDisplayText : modelMetadata.SimpleDisplayText; if (modelMetadata.Model == null)
{
return modelMetadata.NullDisplayText;
}
var text = modelMetadata.SimpleDisplayText;
if (modelMetadata.HtmlEncode)
{
text = html.Encode(text);
}
return text;
} }
var serviceProvider = html.ViewContext.HttpContext.RequestServices; var serviceProvider = html.ViewContext.HttpContext.RequestServices;

View File

@ -205,6 +205,25 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return base.ComputeHideSurroundingHtml(); return base.ComputeHideSurroundingHtml();
} }
/// <summary>
/// Calculate <see cref="ModelMetadata.HtmlEncode"/> based on presence of a
/// <see cref="DisplayFormatAttribute"/> and its <see cref="DisplayFormatAttribute.HtmlEncode"/> value.
/// </summary>
/// <returns>
/// Calculated <see cref="ModelMetadata.HtmlEncode"/> value. <c>false</c> if a
/// <see cref="DisplayFormatAttribute"/> exists and its <see cref="DisplayFormatAttribute.HtmlEncode"/> value
/// is <c>false</c>. <c>true</c> otherwise.
/// </returns>
protected override bool ComputeHtmlEncode()
{
if (PrototypeCache.DisplayFormat != null)
{
return PrototypeCache.DisplayFormat.HtmlEncode;
}
return base.ComputeHtmlEncode();
}
protected override bool ComputeIsReadOnly() protected override bool ComputeIsReadOnly()
{ {
if (PrototypeCache.Editable != null) if (PrototypeCache.Editable != null)

View File

@ -24,6 +24,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private string _editFormatString; private string _editFormatString;
private bool _hasNonDefaultEditFormat; private bool _hasNonDefaultEditFormat;
private bool _hideSurroundingHtml; private bool _hideSurroundingHtml;
private bool _htmlEncode;
private bool _isReadOnly; private bool _isReadOnly;
private bool _isComplexType; private bool _isComplexType;
private bool _isRequired; private bool _isRequired;
@ -39,6 +40,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private bool _editFormatStringComputed; private bool _editFormatStringComputed;
private bool _hasNonDefaultEditFormatComputed; private bool _hasNonDefaultEditFormatComputed;
private bool _hideSurroundingHtmlComputed; private bool _hideSurroundingHtmlComputed;
private bool _htmlEncodeComputed;
private bool _isReadOnlyComputed; private bool _isReadOnlyComputed;
private bool _isComplexTypeComputed; private bool _isComplexTypeComputed;
private bool _isRequiredComputed; private bool _isRequiredComputed;
@ -252,6 +254,27 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
} }
} }
/// <inheritdoc />
public sealed override bool HtmlEncode
{
get
{
if (!_htmlEncodeComputed)
{
_htmlEncode = ComputeHtmlEncode();
_htmlEncodeComputed = true;
}
return _htmlEncode;
}
set
{
_htmlEncode = value;
_htmlEncodeComputed = true;
}
}
public sealed override bool IsReadOnly public sealed override bool IsReadOnly
{ {
get get
@ -419,6 +442,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return base.HideSurroundingHtml; return base.HideSurroundingHtml;
} }
/// <summary>
/// Calculate the <see cref="HtmlEncode"/> value.
/// </summary>
/// <returns>Calculated <see cref="HtmlEncode"/> value.</returns>
protected virtual bool ComputeHtmlEncode()
{
return base.HtmlEncode;
}
protected virtual bool ComputeIsReadOnly() protected virtual bool ComputeIsReadOnly()
{ {
return base.IsReadOnly; return base.IsReadOnly;

View File

@ -19,17 +19,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private EfficientTypePropertyKey<Type, string> _cacheKey; private EfficientTypePropertyKey<Type, string> _cacheKey;
// Backing fields for virtual properties with default values. // Backing fields for virtual properties with default values.
private bool _convertEmptyStringToNull; private bool _convertEmptyStringToNull = true;
private bool _isRequired; private bool _htmlEncode = true;
private bool _showForDisplay = true;
private bool _showForEdit = true;
private object _model; private object _model;
private Func<object> _modelAccessor; private Func<object> _modelAccessor;
private int _order = DefaultOrder; private int _order = DefaultOrder;
private bool _isRequired;
private IEnumerable<ModelMetadata> _properties; private IEnumerable<ModelMetadata> _properties;
private Type _realModelType; private Type _realModelType;
private string _simpleDisplayText; private string _simpleDisplayText;
private bool _showForDisplay = true;
private bool _showForEdit = true;
public ModelMetadata([NotNull] IModelMetadataProvider provider, public ModelMetadata([NotNull] IModelMetadataProvider provider,
Type containerType, Type containerType,
@ -43,7 +44,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
_modelAccessor = modelAccessor; _modelAccessor = modelAccessor;
_modelType = modelType; _modelType = modelType;
_propertyName = propertyName; _propertyName = propertyName;
_convertEmptyStringToNull = true;
_isRequired = !modelType.AllowsNullValue(); _isRequired = !modelType.AllowsNullValue();
} }
@ -111,6 +111,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </summary> /// </summary>
public virtual bool HasNonDefaultEditFormat { get; set; } public virtual bool HasNonDefaultEditFormat { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the value should be HTML-encoded.
/// </summary>
/// <value>If <c>true</c>, value should be HTML-encoded. Default is <c>true</c>.</value>
public virtual bool HtmlEncode
{
get { return _htmlEncode; }
set { _htmlEncode = value; }
}
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the "HiddenInput" display template should return /// Gets or sets a value indicating whether the "HiddenInput" display template should return
/// <c>string.Empty</c> (not the expression value) and whether the "HiddenInput" editor template should not /// <c>string.Empty</c> (not the expression value) and whether the "HiddenInput" editor template should not

View File

@ -2,7 +2,6 @@
// 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;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding;
@ -14,6 +13,33 @@ namespace Microsoft.AspNet.Mvc.Core
{ {
public class DefaultDisplayTemplateTests public class DefaultDisplayTemplateTests
{ {
// Input value; HTML encode; expected value.
public static TheoryData<string, bool, string> HtmlEncodeData
{
get
{
return new TheoryData<string, bool, string>
{
{ "Simple Display Text", false, "Simple Display Text" },
{ "Simple Display Text", true, "Simple Display Text" },
{ "<blink>text</blink>", false, "<blink>text</blink>" },
{ "<blink>text</blink>", true, "&lt;blink&gt;text&lt;/blink&gt;" },
{ "&'\"", false, "&'\"" },
{ "&'\"", true, "&amp;&#39;&quot;" },
{ " ¡ÿĀ", false, " ¡ÿĀ" }, // high ASCII
{ " ¡ÿĀ", true, "&#160;&#161;&#255;Ā" },
{ "Chinese西雅图Chars", false, "Chinese西雅图Chars" },
{ "Chinese西雅图Chars", true, "Chinese西雅图Chars" },
{ "Unicode؃Format؃Char", false, "Unicode؃Format؃Char" }, // class Cf
{ "Unicode؃Format؃Char", true, "Unicode؃Format؃Char" },
{ "UnicodeῼTitlecaseῼChar", false, "UnicodeῼTitlecaseῼChar" }, // class Lt
{ "UnicodeῼTitlecaseῼChar", true, "UnicodeῼTitlecaseῼChar" },
{ "UnicodeCombiningChar", false, "UnicodeCombiningChar" }, // class Mc
{ "UnicodeCombiningChar", true, "UnicodeCombiningChar" },
};
}
}
[Fact] [Fact]
public void ObjectTemplateDisplaysSimplePropertiesOnObjectByDefault() public void ObjectTemplateDisplaysSimplePropertiesOnObjectByDefault()
{ {
@ -54,8 +80,12 @@ namespace Microsoft.AspNet.Mvc.Core
Assert.Equal(metadata.NullDisplayText, result); Assert.Equal(metadata.NullDisplayText, result);
} }
[Fact] [Theory]
public void ObjectTemplateDisplaysSimpleDisplayTextWhenTemplateDepthGreaterThanOne() [MemberData(nameof(HtmlEncodeData))]
public void ObjectTemplateDisplaysSimpleDisplayTextWhenTemplateDepthGreaterThanOne(
string simpleDisplayText,
bool htmlEncode,
string expectedResult)
{ {
// Arrange // Arrange
var model = new DefaultTemplatesUtilities.ObjectTemplateModel(); var model = new DefaultTemplatesUtilities.ObjectTemplateModel();
@ -63,7 +93,8 @@ namespace Microsoft.AspNet.Mvc.Core
var metadata = var metadata =
new EmptyModelMetadataProvider() new EmptyModelMetadataProvider()
.GetMetadataForType(() => model, typeof(DefaultTemplatesUtilities.ObjectTemplateModel)); .GetMetadataForType(() => model, typeof(DefaultTemplatesUtilities.ObjectTemplateModel));
metadata.SimpleDisplayText = "Simple Display Text"; metadata.HtmlEncode = htmlEncode;
metadata.SimpleDisplayText = simpleDisplayText;
html.ViewData.ModelMetadata = metadata; html.ViewData.ModelMetadata = metadata;
html.ViewData.TemplateInfo.AddVisited("foo"); html.ViewData.TemplateInfo.AddVisited("foo");
html.ViewData.TemplateInfo.AddVisited("bar"); html.ViewData.TemplateInfo.AddVisited("bar");
@ -72,7 +103,7 @@ namespace Microsoft.AspNet.Mvc.Core
var result = DefaultDisplayTemplates.ObjectTemplate(html); var result = DefaultDisplayTemplates.ObjectTemplate(html);
// Assert // Assert
Assert.Equal(metadata.SimpleDisplayText, result); Assert.Equal(expectedResult, result);
} }
[Fact] [Fact]

View File

@ -115,8 +115,12 @@ namespace Microsoft.AspNet.Mvc.Core
Assert.Equal(metadata.NullDisplayText, result); Assert.Equal(metadata.NullDisplayText, result);
} }
[Fact] [Theory]
public void ObjectTemplateDisplaysSimpleDisplayTextWithNonNullModelTemplateDepthGreaterThanOne() [MemberData(nameof(DefaultDisplayTemplateTests.HtmlEncodeData), MemberType = typeof(DefaultDisplayTemplateTests))]
public void ObjectTemplateDisplaysSimpleDisplayTextWithNonNullModelTemplateDepthGreaterThanOne(
string simpleDisplayText,
bool htmlEncode,
string expectedResult)
{ {
// Arrange // Arrange
var model = new DefaultTemplatesUtilities.ObjectTemplateModel(); var model = new DefaultTemplatesUtilities.ObjectTemplateModel();
@ -124,9 +128,10 @@ namespace Microsoft.AspNet.Mvc.Core
var metadata = var metadata =
new EmptyModelMetadataProvider() new EmptyModelMetadataProvider()
.GetMetadataForType(() => model, typeof(DefaultTemplatesUtilities.ObjectTemplateModel)); .GetMetadataForType(() => model, typeof(DefaultTemplatesUtilities.ObjectTemplateModel));
html.ViewData.ModelMetadata = metadata; metadata.HtmlEncode = htmlEncode;
metadata.NullDisplayText = "Null Display Text"; metadata.NullDisplayText = "Null Display Text";
metadata.SimpleDisplayText = "Simple Display Text"; metadata.SimpleDisplayText = simpleDisplayText;
html.ViewData.ModelMetadata = metadata;
html.ViewData.TemplateInfo.AddVisited("foo"); html.ViewData.TemplateInfo.AddVisited("foo");
html.ViewData.TemplateInfo.AddVisited("bar"); html.ViewData.TemplateInfo.AddVisited("bar");
@ -134,7 +139,7 @@ namespace Microsoft.AspNet.Mvc.Core
var result = DefaultEditorTemplates.ObjectTemplate(html); var result = DefaultEditorTemplates.ObjectTemplate(html);
// Assert // Assert
Assert.Equal(metadata.SimpleDisplayText, result); Assert.Equal(expectedResult, result);
} }
[Fact] [Fact]

View File

@ -32,6 +32,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.True(metadata.ConvertEmptyStringToNull); Assert.True(metadata.ConvertEmptyStringToNull);
Assert.False(metadata.HasNonDefaultEditFormat); Assert.False(metadata.HasNonDefaultEditFormat);
Assert.False(metadata.HideSurroundingHtml); Assert.False(metadata.HideSurroundingHtml);
Assert.True(metadata.HtmlEncode);
Assert.True(metadata.IsComplexType); Assert.True(metadata.IsComplexType);
Assert.False(metadata.IsReadOnly); Assert.False(metadata.IsReadOnly);
Assert.False(metadata.IsRequired); Assert.False(metadata.IsRequired);
@ -157,6 +158,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
metadata => metadata.HasNonDefaultEditFormat, metadata => metadata.HasNonDefaultEditFormat,
true true
}, },
{
new DisplayFormatAttribute { HtmlEncode = false },
metadata => metadata.HtmlEncode,
false
},
{
new DisplayFormatAttribute { HtmlEncode = true },
metadata => metadata.HtmlEncode,
true
},
{ {
new EditableAttribute(allowEdit: false), new EditableAttribute(allowEdit: false),
metadata => metadata.IsReadOnly, metadata => metadata.IsReadOnly,

View File

@ -22,6 +22,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ m => m.ConvertEmptyStringToNull = false, m => m.ConvertEmptyStringToNull, false }, { m => m.ConvertEmptyStringToNull = false, m => m.ConvertEmptyStringToNull, false },
{ m => m.HasNonDefaultEditFormat = true, m => m.HasNonDefaultEditFormat, true }, { m => m.HasNonDefaultEditFormat = true, m => m.HasNonDefaultEditFormat, true },
{ m => m.HideSurroundingHtml = true, m => m.HideSurroundingHtml, true }, { m => m.HideSurroundingHtml = true, m => m.HideSurroundingHtml, true },
{ m => m.HtmlEncode = false, m => m.HtmlEncode, false },
{ m => m.IsReadOnly = true, m => m.IsReadOnly, true }, { m => m.IsReadOnly = true, m => m.IsReadOnly, true },
{ m => m.IsRequired = true, m => m.IsRequired, true }, { m => m.IsRequired = true, m => m.IsRequired, true },
{ m => m.ShowForDisplay = false, m => m.ShowForDisplay, false }, { m => m.ShowForDisplay = false, m => m.ShowForDisplay, false },
@ -59,6 +60,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.True(metadata.ConvertEmptyStringToNull); Assert.True(metadata.ConvertEmptyStringToNull);
Assert.False(metadata.HasNonDefaultEditFormat); Assert.False(metadata.HasNonDefaultEditFormat);
Assert.False(metadata.HideSurroundingHtml); Assert.False(metadata.HideSurroundingHtml);
Assert.True(metadata.HtmlEncode);
Assert.False(metadata.IsComplexType); Assert.False(metadata.IsComplexType);
Assert.False(metadata.IsNullableValueType); Assert.False(metadata.IsNullableValueType);
Assert.False(metadata.IsReadOnly); Assert.False(metadata.IsReadOnly);