Correct examination of `IHtmlHelper.Label()` return value
- #5317 - previously worked only because `TagBuilder` cannot be empty and `HtmlString` overrides `ToString()` - `TagBuilder.ToString()` (now the type's `FullName`) is also never empty - copy `NullHtmlEncoder` from Razor and give it a better name (`PassThroughHtmlEncoder`) - not available in this project and (from its namespace) not intended for general use
This commit is contained in:
parent
27e4822a7b
commit
f222fa4349
|
|
@ -6,6 +6,9 @@ using System.Collections;
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
|
@ -272,12 +275,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
|||
if (!propertyMetadata.HideSurroundingHtml)
|
||||
{
|
||||
var label = htmlHelper.Label(propertyMetadata.PropertyName, labelText: null, htmlAttributes: null);
|
||||
if (!string.IsNullOrEmpty(label.ToString()))
|
||||
using (var writer = new HasContentTextWriter())
|
||||
{
|
||||
var labelTag = new TagBuilder("div");
|
||||
labelTag.AddCssClass("editor-label");
|
||||
labelTag.InnerHtml.SetHtmlContent(label);
|
||||
content.AppendLine(labelTag);
|
||||
label.WriteTo(writer, PassThroughHtmlEncoder.Default);
|
||||
if (writer.HasContent)
|
||||
{
|
||||
var labelTag = new TagBuilder("div");
|
||||
labelTag.AddCssClass("editor-label");
|
||||
labelTag.InnerHtml.SetHtmlContent(label);
|
||||
content.AppendLine(labelTag);
|
||||
}
|
||||
}
|
||||
|
||||
var valueDivTag = new TagBuilder("div");
|
||||
|
|
@ -433,5 +440,111 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
|||
format: null,
|
||||
htmlAttributes: htmlAttributes);
|
||||
}
|
||||
|
||||
private class HasContentTextWriter : TextWriter
|
||||
{
|
||||
public HasContentTextWriter()
|
||||
{
|
||||
}
|
||||
|
||||
public bool HasContent { get; private set; }
|
||||
|
||||
public override Encoding Encoding => Null.Encoding;
|
||||
|
||||
public override void Write(char value)
|
||||
{
|
||||
HasContent = true;
|
||||
}
|
||||
|
||||
public override void Write(char[] buffer, int index, int count)
|
||||
{
|
||||
if (count > 0)
|
||||
{
|
||||
HasContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(string value)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
HasContent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An HTML encoder which passes through all input data. Does no encoding.
|
||||
// Copied from Microsoft.AspNetCore.Razor.TagHelpers.NullHtmlEncoder.
|
||||
private class PassThroughHtmlEncoder : HtmlEncoder
|
||||
{
|
||||
private PassThroughHtmlEncoder()
|
||||
{
|
||||
}
|
||||
|
||||
public static new PassThroughHtmlEncoder Default { get; } = new PassThroughHtmlEncoder();
|
||||
|
||||
public override int MaxOutputCharactersPerInputCharacter => 1;
|
||||
|
||||
public override string Encode(string value)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
public override void Encode(TextWriter output, char[] value, int startIndex, int characterCount)
|
||||
{
|
||||
if (output == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(output));
|
||||
}
|
||||
|
||||
if (characterCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
output.Write(value, startIndex, characterCount);
|
||||
}
|
||||
|
||||
public override void Encode(TextWriter output, string value, int startIndex, int characterCount)
|
||||
{
|
||||
if (output == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(output));
|
||||
}
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
if (characterCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
output.Write(value.Substring(startIndex, characterCount));
|
||||
}
|
||||
|
||||
public override unsafe int FindFirstCharacterToEncode(char* text, int textLength)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
public override unsafe bool TryEncodeUnicodeScalar(
|
||||
int unicodeScalar,
|
||||
char* buffer,
|
||||
int bufferLength,
|
||||
out int numberOfCharactersWritten)
|
||||
{
|
||||
numberOfCharactersWritten = 0;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool WillEncode(int unicodeScalar)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
]
|
||||
},
|
||||
"buildOptions": {
|
||||
"allowUnsafe": true,
|
||||
"warningsAsErrors": true,
|
||||
"keyFile": "../../tools/Key.snk",
|
||||
"nowarn": [
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
|
|
@ -86,22 +87,108 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
|||
}
|
||||
}
|
||||
|
||||
// label's IHtmlContent -> expected label text
|
||||
public static TheoryData<IHtmlContent, string> ObjectTemplate_ChecksWriteTo_NotToStringData
|
||||
{
|
||||
get
|
||||
{
|
||||
// Similar to HtmlString.Empty today.
|
||||
var noopContentWithEmptyToString = new Mock<IHtmlContent>(MockBehavior.Strict);
|
||||
noopContentWithEmptyToString
|
||||
.Setup(c => c.ToString())
|
||||
.Returns(string.Empty);
|
||||
noopContentWithEmptyToString.Setup(c => c.WriteTo(It.IsAny<TextWriter>(), It.IsAny<HtmlEncoder>()));
|
||||
|
||||
// Similar to an empty StringHtmlContent today.
|
||||
var noopContentWithNonEmptyToString = new Mock<IHtmlContent>(MockBehavior.Strict);
|
||||
noopContentWithNonEmptyToString
|
||||
.Setup(c => c.ToString())
|
||||
.Returns(typeof(StringHtmlContent).FullName);
|
||||
noopContentWithNonEmptyToString.Setup(c => c.WriteTo(It.IsAny<TextWriter>(), It.IsAny<HtmlEncoder>()));
|
||||
|
||||
// Makes noop calls on the TextWriter.
|
||||
var busyNoopContentWithNonEmptyToString = new Mock<IHtmlContent>(MockBehavior.Strict);
|
||||
busyNoopContentWithNonEmptyToString
|
||||
.Setup(c => c.ToString())
|
||||
.Returns(typeof(StringHtmlContent).FullName);
|
||||
busyNoopContentWithNonEmptyToString
|
||||
.Setup(c => c.WriteTo(It.IsAny<TextWriter>(), It.IsAny<HtmlEncoder>()))
|
||||
.Callback<TextWriter, HtmlEncoder>((writer, encoder) =>
|
||||
{
|
||||
writer.Write(string.Empty);
|
||||
writer.Write(new char[0]);
|
||||
writer.Write((char[])null);
|
||||
writer.Write((object)null);
|
||||
writer.Write((string)null);
|
||||
writer.Write(format: "{0}", arg0: null);
|
||||
writer.Write(new char[] { 'a', 'b', 'c' }, index: 1, count: 0);
|
||||
});
|
||||
|
||||
// Unrealistic but covers all the bases.
|
||||
var writingContentWithEmptyToString = new Mock<IHtmlContent>(MockBehavior.Strict);
|
||||
writingContentWithEmptyToString
|
||||
.Setup(c => c.ToString())
|
||||
.Returns(string.Empty);
|
||||
writingContentWithEmptyToString
|
||||
.Setup(c => c.WriteTo(It.IsAny<TextWriter>(), It.IsAny<HtmlEncoder>()))
|
||||
.Callback<TextWriter, HtmlEncoder>((writer, encoder) => writer.Write("Some string"));
|
||||
|
||||
// Similar to TagBuilder today.
|
||||
var writingContentWithNonEmptyToString = new Mock<IHtmlContent>(MockBehavior.Strict);
|
||||
writingContentWithNonEmptyToString
|
||||
.Setup(c => c.ToString())
|
||||
.Returns(typeof(TagBuilder).FullName);
|
||||
writingContentWithNonEmptyToString
|
||||
.Setup(c => c.WriteTo(It.IsAny<TextWriter>(), It.IsAny<HtmlEncoder>()))
|
||||
.Callback<TextWriter, HtmlEncoder>((writer, encoder) => writer.Write("Some string"));
|
||||
|
||||
// label's IHtmlContent -> expected label text
|
||||
return new TheoryData<IHtmlContent, string>
|
||||
{
|
||||
// Types HtmlHelper actually uses.
|
||||
{ HtmlString.Empty, string.Empty },
|
||||
{
|
||||
new TagBuilder("label"),
|
||||
"<div class=\"HtmlEncode[[editor-label]]\"><label></label></div>" + Environment.NewLine
|
||||
},
|
||||
|
||||
// Another IHtmlContent implementation that does not override ToString().
|
||||
{ new StringHtmlContent(string.Empty), string.Empty },
|
||||
|
||||
// Mocks
|
||||
{ noopContentWithEmptyToString.Object, string.Empty },
|
||||
{ noopContentWithNonEmptyToString.Object, string.Empty },
|
||||
{ busyNoopContentWithNonEmptyToString.Object, string.Empty },
|
||||
{
|
||||
writingContentWithEmptyToString.Object,
|
||||
"<div class=\"HtmlEncode[[editor-label]]\">Some string</div>" + Environment.NewLine
|
||||
},
|
||||
{
|
||||
writingContentWithNonEmptyToString.Object,
|
||||
"<div class=\"HtmlEncode[[editor-label]]\">Some string</div>" + Environment.NewLine
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObjectTemplateEditsSimplePropertiesOnObjectByDefault()
|
||||
{
|
||||
var expected =
|
||||
"<div class=\"HtmlEncode[[editor-label]]\"><label for=\"HtmlEncode[[Property1]]\">HtmlEncode[[Property1]]</label></div>" + Environment.NewLine
|
||||
+ "<div class=\"HtmlEncode[[editor-field]]\">Model = p1, ModelType = System.String, PropertyName = Property1," +
|
||||
" SimpleDisplayText = p1 " +
|
||||
"<span class=\"HtmlEncode[[field-validation-valid]]\" data-valmsg-for=\"HtmlEncode[[Property1]]\" data-valmsg-replace=\"HtmlEncode[[true]]\">" +
|
||||
"</span></div>" + Environment.NewLine
|
||||
+ "<div class=\"HtmlEncode[[editor-label]]\"><label for=\"HtmlEncode[[Property2]]\">HtmlEncode[[Prop2]]</label></div>" + Environment.NewLine
|
||||
+ "<div class=\"HtmlEncode[[editor-field]]\">Model = (null), ModelType = System.String, PropertyName = Property2," +
|
||||
" SimpleDisplayText = (null) " +
|
||||
"<span class=\"HtmlEncode[[field-validation-valid]]\" data-valmsg-for=\"HtmlEncode[[Property2]]\" data-valmsg-replace=\"HtmlEncode[[true]]\">" +
|
||||
"</span></div>" + Environment.NewLine;
|
||||
|
||||
// Arrange
|
||||
var expected =
|
||||
"<div class=\"HtmlEncode[[editor-label]]\"><label for=\"HtmlEncode[[Property1]]\">HtmlEncode[[Property1]]</label></div>" +
|
||||
Environment.NewLine +
|
||||
"<div class=\"HtmlEncode[[editor-field]]\">Model = p1, ModelType = System.String, PropertyName = Property1, SimpleDisplayText = p1 " +
|
||||
"<span class=\"HtmlEncode[[field-validation-valid]]\" data-valmsg-for=\"HtmlEncode[[Property1]]\" data-valmsg-replace=\"HtmlEncode[[true]]\">" +
|
||||
"</span></div>" +
|
||||
Environment.NewLine +
|
||||
"<div class=\"HtmlEncode[[editor-label]]\"><label for=\"HtmlEncode[[Property2]]\">HtmlEncode[[Prop2]]</label></div>" +
|
||||
Environment.NewLine +
|
||||
"<div class=\"HtmlEncode[[editor-field]]\">Model = (null), ModelType = System.String, PropertyName = Property2, SimpleDisplayText = (null) " +
|
||||
"<span class=\"HtmlEncode[[field-validation-valid]]\" data-valmsg-for=\"HtmlEncode[[Property2]]\" data-valmsg-replace=\"HtmlEncode[[true]]\">" +
|
||||
"</span></div>" +
|
||||
Environment.NewLine;
|
||||
|
||||
var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "p1", Property2 = null };
|
||||
var html = DefaultTemplatesUtilities.GetHtmlHelper(model);
|
||||
|
||||
|
|
@ -112,6 +199,82 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
|||
Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
|
||||
}
|
||||
|
||||
// Expect almost the same HTML as in ObjectTemplateEditsSimplePropertiesOnObjectByDefault(). Only difference is
|
||||
// the <div class="editor-label">...</div> is not present for Property1.
|
||||
[Fact]
|
||||
public void ObjectTemplateSkipsLabel_IfDisplayNameIsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var expected =
|
||||
"<div class=\"HtmlEncode[[editor-field]]\">Model = p1, ModelType = System.String, PropertyName = Property1, SimpleDisplayText = p1 " +
|
||||
"<span class=\"HtmlEncode[[field-validation-valid]]\" data-valmsg-for=\"HtmlEncode[[Property1]]\" data-valmsg-replace=\"HtmlEncode[[true]]\">" +
|
||||
"</span></div>" +
|
||||
Environment.NewLine +
|
||||
"<div class=\"HtmlEncode[[editor-label]]\"><label for=\"HtmlEncode[[Property2]]\">HtmlEncode[[Prop2]]</label></div>" +
|
||||
Environment.NewLine +
|
||||
"<div class=\"HtmlEncode[[editor-field]]\">Model = (null), ModelType = System.String, PropertyName = Property2, SimpleDisplayText = (null) " +
|
||||
"<span class=\"HtmlEncode[[field-validation-valid]]\" data-valmsg-for=\"HtmlEncode[[Property2]]\" data-valmsg-replace=\"HtmlEncode[[true]]\">" +
|
||||
"</span></div>" +
|
||||
Environment.NewLine;
|
||||
|
||||
var provider = new TestModelMetadataProvider();
|
||||
provider
|
||||
.ForProperty<DefaultTemplatesUtilities.ObjectTemplateModel>(
|
||||
nameof(DefaultTemplatesUtilities.ObjectTemplateModel.Property1))
|
||||
.DisplayDetails(dd => dd.DisplayName = () => string.Empty);
|
||||
|
||||
var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "p1", Property2 = null };
|
||||
var html = DefaultTemplatesUtilities.GetHtmlHelper(model, provider);
|
||||
|
||||
// Act
|
||||
var result = DefaultEditorTemplates.ObjectTemplate(html);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ObjectTemplate_ChecksWriteTo_NotToStringData))]
|
||||
public void ObjectTemplate_ChecksWriteTo_NotToString(IHtmlContent labelContent, string expectedLabel)
|
||||
{
|
||||
// Arrange
|
||||
var expected =
|
||||
expectedLabel +
|
||||
"<div class=\"HtmlEncode[[editor-field]]\">Model = (null), ModelType = System.String, PropertyName = Property1, SimpleDisplayText = (null) " +
|
||||
"</div>" +
|
||||
Environment.NewLine +
|
||||
expectedLabel +
|
||||
"<div class=\"HtmlEncode[[editor-field]]\">Model = (null), ModelType = System.String, PropertyName = Property2, SimpleDisplayText = (null) " +
|
||||
"</div>" +
|
||||
Environment.NewLine;
|
||||
|
||||
var helperToCopy = DefaultTemplatesUtilities.GetHtmlHelper();
|
||||
var helperMock = new Mock<IHtmlHelper>(MockBehavior.Strict);
|
||||
helperMock.SetupGet(h => h.ViewContext).Returns(helperToCopy.ViewContext);
|
||||
helperMock.SetupGet(h => h.ViewData).Returns(helperToCopy.ViewData);
|
||||
helperMock
|
||||
.Setup(h => h.Label(
|
||||
It.Is<string>(s => string.Equals("Property1", s, StringComparison.Ordinal) ||
|
||||
string.Equals("Property2", s, StringComparison.Ordinal)),
|
||||
null, // labelText
|
||||
null)) // htmlAttributes
|
||||
.Returns(labelContent);
|
||||
helperMock
|
||||
.Setup(h => h.ValidationMessage(
|
||||
It.Is<string>(s => string.Equals("Property1", s, StringComparison.Ordinal) ||
|
||||
string.Equals("Property2", s, StringComparison.Ordinal)),
|
||||
null, // message
|
||||
null, // htmlAttributes
|
||||
null)) // tag
|
||||
.Returns(HtmlString.Empty);
|
||||
|
||||
// Act
|
||||
var result = DefaultEditorTemplates.ObjectTemplate(helperMock.Object);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObjectTemplateDisplaysNullDisplayTextWithNullModelAndTemplateDepthGreaterThanOne()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue