Mgbbs/hidden for checkbox render mode (#13014)

* Added CheckBoxHiddenInputRenderMode to HtmlHelperOptions, ViewContext, and html/tag helpers

Fixes #12833
This commit is contained in:
mgbbs 2019-08-26 18:00:00 -05:00 committed by Pranav K
parent a88180472d
commit 90e89e9708
8 changed files with 406 additions and 22 deletions

View File

@ -309,29 +309,32 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
"checkbox"));
}
// hiddenForCheckboxTag always rendered after the returned element
var hiddenForCheckboxTag = Generator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, For.Name);
if (hiddenForCheckboxTag != null)
if (ViewContext.CheckBoxHiddenInputRenderMode != CheckBoxHiddenInputRenderMode.None)
{
var renderingMode =
output.TagMode == TagMode.SelfClosing ? TagRenderMode.SelfClosing : TagRenderMode.StartTag;
hiddenForCheckboxTag.TagRenderMode = renderingMode;
if (!hiddenForCheckboxTag.Attributes.ContainsKey("name") &&
!string.IsNullOrEmpty(Name))
// hiddenForCheckboxTag always rendered after the returned element
var hiddenForCheckboxTag = Generator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, For.Name);
if (hiddenForCheckboxTag != null)
{
// The checkbox and hidden elements should have the same name attribute value. Attributes will
// match if both are present because both have a generated value. Reach here in the special case
// where user provided a non-empty fallback name.
hiddenForCheckboxTag.MergeAttribute("name", Name);
}
var renderingMode =
output.TagMode == TagMode.SelfClosing ? TagRenderMode.SelfClosing : TagRenderMode.StartTag;
hiddenForCheckboxTag.TagRenderMode = renderingMode;
if (!hiddenForCheckboxTag.Attributes.ContainsKey("name") &&
!string.IsNullOrEmpty(Name))
{
// The checkbox and hidden elements should have the same name attribute value. Attributes will
// match if both are present because both have a generated value. Reach here in the special case
// where user provided a non-empty fallback name.
hiddenForCheckboxTag.MergeAttribute("name", Name);
}
if (ViewContext.FormContext.CanRenderAtEndOfForm)
{
ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckboxTag);
}
else
{
output.PostElement.AppendHtml(hiddenForCheckboxTag);
if (ViewContext.CheckBoxHiddenInputRenderMode == CheckBoxHiddenInputRenderMode.EndOfForm && ViewContext.FormContext.CanRenderAtEndOfForm)
{
ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckboxTag);
}
else
{
output.PostElement.AppendHtml(hiddenForCheckboxTag);
}
}
}

View File

@ -838,6 +838,243 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Assert.Equal(expectedTagName, output.TagName);
}
[Fact]
public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeNone()
{
var propertyName = "-expression-";
var expectedTagName = "input";
var inputTypeName = "checkbox";
var expectedAttributes = new TagHelperAttributeList
{
{ "name", propertyName },
{ "type", inputTypeName },
{ "value", "true" },
};
var metadataProvider = new EmptyModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var model = false;
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.None;
var tagHelper = new InputTagHelper(htmlGenerator)
{
For = modelExpression,
InputTypeName = inputTypeName,
Name = propertyName,
ViewContext = viewContext,
};
var attributes = new TagHelperAttributeList
{
{ "name", propertyName },
{ "type", inputTypeName },
};
var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
{
TagMode = TagMode.SelfClosing,
};
// Act
await tagHelper.ProcessAsync(context, output);
// Assert
Assert.Equal(expectedAttributes, output.Attributes);
Assert.False(output.IsContentModified);
Assert.Equal(expectedTagName, output.TagName);
Assert.False(viewContext.FormContext.HasEndOfFormContent);
Assert.True(string.IsNullOrEmpty(HtmlContentUtilities.HtmlContentToString(output.PostElement)));
}
[Fact]
public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeInline()
{
var propertyName = "-expression-";
var expectedTagName = "input";
var expectedPostElementContent = $"<input name=\"HtmlEncode[[{propertyName}]]\" " +
"type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />";
var inputTypeName = "checkbox";
var expectedAttributes = new TagHelperAttributeList
{
{ "name", propertyName },
{ "type", inputTypeName },
{ "value", "true" },
};
var metadataProvider = new EmptyModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var model = false;
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
viewContext.FormContext.CanRenderAtEndOfForm = true;
viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.Inline;
var tagHelper = new InputTagHelper(htmlGenerator)
{
For = modelExpression,
InputTypeName = inputTypeName,
Name = propertyName,
ViewContext = viewContext,
};
var attributes = new TagHelperAttributeList
{
{ "name", propertyName },
{ "type", inputTypeName },
};
var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
{
TagMode = TagMode.SelfClosing,
};
// Act
await tagHelper.ProcessAsync(context, output);
// Assert
Assert.Equal(expectedAttributes, output.Attributes);
Assert.False(output.IsContentModified);
Assert.Equal(expectedTagName, output.TagName);
Assert.False(viewContext.FormContext.HasEndOfFormContent);
Assert.Equal(expectedPostElementContent, HtmlContentUtilities.HtmlContentToString(output.PostElement));
}
[Fact]
public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeEndOfForm()
{
var propertyName = "-expression-";
var expectedTagName = "input";
var expectedEndOfFormContent = $"<input name=\"HtmlEncode[[{propertyName}]]\" " +
"type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />";
var inputTypeName = "checkbox";
var expectedAttributes = new TagHelperAttributeList
{
{ "name", propertyName },
{ "type", inputTypeName },
{ "value", "true" },
};
var metadataProvider = new EmptyModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var model = false;
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
viewContext.FormContext.CanRenderAtEndOfForm = true;
viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.EndOfForm;
var tagHelper = new InputTagHelper(htmlGenerator)
{
For = modelExpression,
InputTypeName = inputTypeName,
Name = propertyName,
ViewContext = viewContext,
};
var attributes = new TagHelperAttributeList
{
{ "name", propertyName },
{ "type", inputTypeName },
};
var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
{
TagMode = TagMode.SelfClosing,
};
// Act
await tagHelper.ProcessAsync(context, output);
// Assert
Assert.Equal(expectedAttributes, output.Attributes);
Assert.False(output.IsContentModified);
Assert.Equal(expectedTagName, output.TagName);
Assert.Equal(expectedEndOfFormContent, string.Join("", viewContext.FormContext.EndOfFormContent.Select(html => HtmlContentUtilities.HtmlContentToString(html))));
Assert.True(string.IsNullOrEmpty(HtmlContentUtilities.HtmlContentToString(output.PostElement)));
}
[Fact]
public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeEndOfForm_AndCanRenderAtEndOfFormNotSet()
{
var propertyName = "-expression-";
var expectedTagName = "input";
var expectedPostElementContent = $"<input name=\"HtmlEncode[[{propertyName}]]\" " +
"type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />";
var inputTypeName = "checkbox";
var expectedAttributes = new TagHelperAttributeList
{
{ "name", propertyName },
{ "type", inputTypeName },
{ "value", "true" },
};
var metadataProvider = new EmptyModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var model = false;
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
viewContext.FormContext.CanRenderAtEndOfForm = false;
viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.EndOfForm;
var tagHelper = new InputTagHelper(htmlGenerator)
{
For = modelExpression,
InputTypeName = inputTypeName,
Name = propertyName,
ViewContext = viewContext,
};
var attributes = new TagHelperAttributeList
{
{ "name", propertyName },
{ "type", inputTypeName },
};
var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
{
TagMode = TagMode.SelfClosing,
};
// Act
await tagHelper.ProcessAsync(context, output);
// Assert
Assert.Equal(expectedAttributes, output.Attributes);
Assert.False(output.IsContentModified);
Assert.Equal(expectedTagName, output.TagName);
Assert.False(viewContext.FormContext.HasEndOfFormContent);
Assert.Equal(expectedPostElementContent, HtmlContentUtilities.HtmlContentToString(output.PostElement));
}
[Fact]
public async Task ProcessAsync_CallsGenerateCheckBox_WithExpectedParameters()
{

View File

@ -312,6 +312,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
namespace Microsoft.AspNetCore.Mvc.Rendering
{
public enum CheckBoxHiddenInputRenderMode
{
None = 0,
Inline = 1,
EndOfForm = 2,
}
public enum FormMethod
{
Get = 0,
@ -678,6 +684,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
public ViewContext() { }
public ViewContext(Microsoft.AspNetCore.Mvc.ActionContext actionContext, Microsoft.AspNetCore.Mvc.ViewEngines.IView view, Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary viewData, Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataDictionary tempData, System.IO.TextWriter writer, Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelperOptions htmlHelperOptions) { }
public ViewContext(Microsoft.AspNetCore.Mvc.Rendering.ViewContext viewContext, Microsoft.AspNetCore.Mvc.ViewEngines.IView view, Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary viewData, System.IO.TextWriter writer) { }
public Microsoft.AspNetCore.Mvc.Rendering.CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool ClientValidationEnabled { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string ExecutingFilePath { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public virtual Microsoft.AspNetCore.Mvc.ViewFeatures.FormContext FormContext { get { throw null; } set { } }
@ -1062,6 +1069,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
public partial class HtmlHelperOptions
{
public HtmlHelperOptions() { }
public Microsoft.AspNetCore.Mvc.Rendering.CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool ClientValidationEnabled { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Mvc.Rendering.Html5DateRenderingMode Html5DateRenderingMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string IdAttributeDotReplacement { get { throw null; } set { } }

View File

@ -721,8 +721,18 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
isChecked,
htmlAttributes);
if (checkbox == null)
{
return HtmlString.Empty;
}
if (ViewContext.CheckBoxHiddenInputRenderMode == CheckBoxHiddenInputRenderMode.None)
{
return checkbox;
}
var hiddenForCheckbox = _htmlGenerator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, expression);
if (checkbox == null || hiddenForCheckbox == null)
if (hiddenForCheckbox == null)
{
return HtmlString.Empty;
}
@ -736,7 +746,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
hiddenForCheckbox.MergeAttribute("name", name);
}
if (ViewContext.FormContext.CanRenderAtEndOfForm)
if (ViewContext.CheckBoxHiddenInputRenderMode == CheckBoxHiddenInputRenderMode.EndOfForm && ViewContext.FormContext.CanRenderAtEndOfForm)
{
ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckbox);
return checkbox;

View File

@ -56,5 +56,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// <see cref="IHtmlHelper.ValidationSummary"/> and other overloads.
/// </summary>
public string ValidationSummaryMessageElement { get; set; } = "span";
/// <summary>
/// Gets or sets the way hidden inputs are rendered for checkbox tag helpers and html helpers.
/// </summary>
public CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { get; set; } = CheckBoxHiddenInputRenderMode.EndOfForm;
}
}

View File

@ -0,0 +1,27 @@
// 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.
namespace Microsoft.AspNetCore.Mvc.Rendering
{
/// <summary>
/// Controls the rendering of hidden input fields when using CheckBox tag helpers or html helpers.
/// </summary>
public enum CheckBoxHiddenInputRenderMode
{
/// <summary>
/// Hidden input fields will not be automatically rendered. If checkbox is not checked, no value will be posted.
/// </summary>
None = 0,
/// <summary>
/// Hidden input fields will be rendered inline with each checkbox. Use this for legacy ASP.NET MVC behavior.
/// </summary>
Inline = 1,
/// <summary>
/// Hidden input fields will be rendered for each checkbox at the bottom of the form element. This is the preferred render method and default MVC behavior.
/// If <see cref="Microsoft.AspNetCore.Mvc.ViewFeatures.FormContext.CanRenderAtEndOfForm"/> is <c>false</c>, will fall back on <see cref="Inline"/>.
/// </summary>
EndOfForm = 2
}
}

View File

@ -87,6 +87,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
Html5DateRenderingMode = htmlHelperOptions.Html5DateRenderingMode;
ValidationSummaryMessageElement = htmlHelperOptions.ValidationSummaryMessageElement;
ValidationMessageElement = htmlHelperOptions.ValidationMessageElement;
CheckBoxHiddenInputRenderMode = htmlHelperOptions.CheckBoxHiddenInputRenderMode;
}
/// <summary>
@ -129,6 +130,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
Html5DateRenderingMode = viewContext.Html5DateRenderingMode;
ValidationSummaryMessageElement = viewContext.ValidationSummaryMessageElement;
ValidationMessageElement = viewContext.ValidationMessageElement;
CheckBoxHiddenInputRenderMode = viewContext.CheckBoxHiddenInputRenderMode;
ExecutingFilePath = viewContext.ExecutingFilePath;
View = view;
@ -180,6 +182,11 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// </summary>
public string ValidationMessageElement { get; set; }
/// <summary>
/// Gets or sets the way hidden inputs are rendered for checkbox tag helpers and html helpers.
/// </summary>
public CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { get; set; }
/// <summary>
/// Gets the dynamic view bag.
/// </summary>

View File

@ -169,6 +169,93 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
writer.ToString());
}
[Fact]
public void CheckBox_WithHiddenInputRenderModeNone_DoesNotGenerateHiddenInput()
{
// Arrange
var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("Boolean");
var expected = @"<input checked=""HtmlEncode[[checked]]"" data-val=""HtmlEncode[[true]]"" " +
$@"data-val-required=""HtmlEncode[[{requiredMessage}]]"" id=""HtmlEncode[[Property1]]"" " +
@"name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[checkbox]]"" " +
@"value=""HtmlEncode[[true]]"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetTestModelViewData());
helper.ViewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.None;
// Act
var html = helper.CheckBox("Property1", isChecked: true, htmlAttributes: null);
// Assert
Assert.False(helper.ViewContext.FormContext.HasEndOfFormContent);
Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(html));
}
[Fact]
public void CheckBox_WithHiddenInputRenderModeInline_GeneratesHiddenInput()
{
// Arrange
var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("Boolean");
var expected = @"<input checked=""HtmlEncode[[checked]]"" data-val=""HtmlEncode[[true]]"" " +
$@"data-val-required=""HtmlEncode[[{requiredMessage}]]"" id=""HtmlEncode[[Property1]]"" " +
@"name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[checkbox]]"" " +
@"value=""HtmlEncode[[true]]"" /><input name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[hidden]]"" value=""HtmlEncode[[false]]"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetTestModelViewData());
helper.ViewContext.FormContext.CanRenderAtEndOfForm = true;
helper.ViewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.Inline;
// Act
var html = helper.CheckBox("Property1", isChecked: true, htmlAttributes: null);
// Assert
Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(html));
}
[Fact]
public void CheckBox_WithHiddenInputRenderModeEndOfForm_GeneratesHiddenInput()
{
// Arrange
var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("Boolean");
var expected = @"<input checked=""HtmlEncode[[checked]]"" data-val=""HtmlEncode[[true]]"" " +
$@"data-val-required=""HtmlEncode[[{requiredMessage}]]"" id=""HtmlEncode[[Property1]]"" " +
@"name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[checkbox]]"" " +
@"value=""HtmlEncode[[true]]"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetTestModelViewData());
helper.ViewContext.FormContext.CanRenderAtEndOfForm = true;
helper.ViewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.EndOfForm;
// Act
var html = helper.CheckBox("Property1", isChecked: true, htmlAttributes: null);
// Assert
Assert.True(helper.ViewContext.FormContext.HasEndOfFormContent);
Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(html));
var writer = new StringWriter();
var hiddenTag = Assert.Single(helper.ViewContext.FormContext.EndOfFormContent);
hiddenTag.WriteTo(writer, new HtmlTestEncoder());
Assert.Equal("<input name=\"HtmlEncode[[Property1]]\" type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />",
writer.ToString());
}
[Fact]
public void CheckBox_WithHiddenInputRenderModeEndOfForm_WithCanRenderAtEndOfFormNotSet_GeneratesHiddenInput()
{
// Arrange
var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("Boolean");
var expected = @"<input checked=""HtmlEncode[[checked]]"" data-val=""HtmlEncode[[true]]"" " +
$@"data-val-required=""HtmlEncode[[{requiredMessage}]]"" id=""HtmlEncode[[Property1]]"" " +
@"name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[checkbox]]"" " +
@"value=""HtmlEncode[[true]]"" /><input name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[hidden]]"" value=""HtmlEncode[[false]]"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetTestModelViewData());
helper.ViewContext.FormContext.CanRenderAtEndOfForm = false;
helper.ViewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.EndOfForm;
// Act
var html = helper.CheckBox("Property1", isChecked: true, htmlAttributes: null);
// Assert
Assert.False(helper.ViewContext.FormContext.HasEndOfFormContent);
Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(html));
}
[Fact]
public void CheckBoxUsesAttemptedValueFromModelState()
{