Skip to content

Commit 90e89e9

Browse files
mgbbspranavkm
authored andcommitted
Mgbbs/hidden for checkbox render mode (#13014)
* Added CheckBoxHiddenInputRenderMode to HtmlHelperOptions, ViewContext, and html/tag helpers Fixes #12833
1 parent a881804 commit 90e89e9

File tree

8 files changed

+407
-23
lines changed

8 files changed

+407
-23
lines changed

src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -309,29 +309,32 @@ private TagBuilder GenerateCheckBox(
309309
"checkbox"));
310310
}
311311

312-
// hiddenForCheckboxTag always rendered after the returned element
313-
var hiddenForCheckboxTag = Generator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, For.Name);
314-
if (hiddenForCheckboxTag != null)
315-
{
316-
var renderingMode =
317-
output.TagMode == TagMode.SelfClosing ? TagRenderMode.SelfClosing : TagRenderMode.StartTag;
318-
hiddenForCheckboxTag.TagRenderMode = renderingMode;
319-
if (!hiddenForCheckboxTag.Attributes.ContainsKey("name") &&
320-
!string.IsNullOrEmpty(Name))
312+
if (ViewContext.CheckBoxHiddenInputRenderMode != CheckBoxHiddenInputRenderMode.None)
313+
{
314+
// hiddenForCheckboxTag always rendered after the returned element
315+
var hiddenForCheckboxTag = Generator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, For.Name);
316+
if (hiddenForCheckboxTag != null)
321317
{
322-
// The checkbox and hidden elements should have the same name attribute value. Attributes will
323-
// match if both are present because both have a generated value. Reach here in the special case
324-
// where user provided a non-empty fallback name.
325-
hiddenForCheckboxTag.MergeAttribute("name", Name);
326-
}
318+
var renderingMode =
319+
output.TagMode == TagMode.SelfClosing ? TagRenderMode.SelfClosing : TagRenderMode.StartTag;
320+
hiddenForCheckboxTag.TagRenderMode = renderingMode;
321+
if (!hiddenForCheckboxTag.Attributes.ContainsKey("name") &&
322+
!string.IsNullOrEmpty(Name))
323+
{
324+
// The checkbox and hidden elements should have the same name attribute value. Attributes will
325+
// match if both are present because both have a generated value. Reach here in the special case
326+
// where user provided a non-empty fallback name.
327+
hiddenForCheckboxTag.MergeAttribute("name", Name);
328+
}
327329

328-
if (ViewContext.FormContext.CanRenderAtEndOfForm)
329-
{
330-
ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckboxTag);
331-
}
332-
else
333-
{
334-
output.PostElement.AppendHtml(hiddenForCheckboxTag);
330+
if (ViewContext.CheckBoxHiddenInputRenderMode == CheckBoxHiddenInputRenderMode.EndOfForm && ViewContext.FormContext.CanRenderAtEndOfForm)
331+
{
332+
ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckboxTag);
333+
}
334+
else
335+
{
336+
output.PostElement.AppendHtml(hiddenForCheckboxTag);
337+
}
335338
}
336339
}
337340

src/Mvc/Mvc.TagHelpers/test/InputTagHelperTest.cs

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,243 @@ public void Process_GeneratesFormattedOutput_ForDateTime(string specifiedType, s
838838
Assert.Equal(expectedTagName, output.TagName);
839839
}
840840

841+
[Fact]
842+
public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeNone()
843+
{
844+
var propertyName = "-expression-";
845+
var expectedTagName = "input";
846+
var inputTypeName = "checkbox";
847+
var expectedAttributes = new TagHelperAttributeList
848+
{
849+
{ "name", propertyName },
850+
{ "type", inputTypeName },
851+
{ "value", "true" },
852+
};
853+
854+
var metadataProvider = new EmptyModelMetadataProvider();
855+
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
856+
var model = false;
857+
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
858+
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
859+
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
860+
861+
viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.None;
862+
863+
var tagHelper = new InputTagHelper(htmlGenerator)
864+
{
865+
For = modelExpression,
866+
InputTypeName = inputTypeName,
867+
Name = propertyName,
868+
ViewContext = viewContext,
869+
};
870+
871+
var attributes = new TagHelperAttributeList
872+
{
873+
{ "name", propertyName },
874+
{ "type", inputTypeName },
875+
};
876+
877+
var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
878+
var output = new TagHelperOutput(
879+
expectedTagName,
880+
new TagHelperAttributeList(),
881+
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
882+
{
883+
TagMode = TagMode.SelfClosing,
884+
};
885+
886+
// Act
887+
await tagHelper.ProcessAsync(context, output);
888+
889+
// Assert
890+
Assert.Equal(expectedAttributes, output.Attributes);
891+
Assert.False(output.IsContentModified);
892+
Assert.Equal(expectedTagName, output.TagName);
893+
894+
Assert.False(viewContext.FormContext.HasEndOfFormContent);
895+
Assert.True(string.IsNullOrEmpty(HtmlContentUtilities.HtmlContentToString(output.PostElement)));
896+
}
897+
898+
[Fact]
899+
public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeInline()
900+
{
901+
var propertyName = "-expression-";
902+
var expectedTagName = "input";
903+
var expectedPostElementContent = $"<input name=\"HtmlEncode[[{propertyName}]]\" " +
904+
"type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />";
905+
var inputTypeName = "checkbox";
906+
var expectedAttributes = new TagHelperAttributeList
907+
{
908+
{ "name", propertyName },
909+
{ "type", inputTypeName },
910+
{ "value", "true" },
911+
};
912+
913+
var metadataProvider = new EmptyModelMetadataProvider();
914+
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
915+
var model = false;
916+
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
917+
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
918+
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
919+
920+
viewContext.FormContext.CanRenderAtEndOfForm = true;
921+
viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.Inline;
922+
923+
var tagHelper = new InputTagHelper(htmlGenerator)
924+
{
925+
For = modelExpression,
926+
InputTypeName = inputTypeName,
927+
Name = propertyName,
928+
ViewContext = viewContext,
929+
};
930+
931+
var attributes = new TagHelperAttributeList
932+
{
933+
{ "name", propertyName },
934+
{ "type", inputTypeName },
935+
};
936+
937+
var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
938+
var output = new TagHelperOutput(
939+
expectedTagName,
940+
new TagHelperAttributeList(),
941+
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
942+
{
943+
TagMode = TagMode.SelfClosing,
944+
};
945+
946+
// Act
947+
await tagHelper.ProcessAsync(context, output);
948+
949+
// Assert
950+
Assert.Equal(expectedAttributes, output.Attributes);
951+
Assert.False(output.IsContentModified);
952+
Assert.Equal(expectedTagName, output.TagName);
953+
954+
Assert.False(viewContext.FormContext.HasEndOfFormContent);
955+
Assert.Equal(expectedPostElementContent, HtmlContentUtilities.HtmlContentToString(output.PostElement));
956+
}
957+
958+
[Fact]
959+
public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeEndOfForm()
960+
{
961+
var propertyName = "-expression-";
962+
var expectedTagName = "input";
963+
var expectedEndOfFormContent = $"<input name=\"HtmlEncode[[{propertyName}]]\" " +
964+
"type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />";
965+
var inputTypeName = "checkbox";
966+
var expectedAttributes = new TagHelperAttributeList
967+
{
968+
{ "name", propertyName },
969+
{ "type", inputTypeName },
970+
{ "value", "true" },
971+
};
972+
973+
var metadataProvider = new EmptyModelMetadataProvider();
974+
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
975+
var model = false;
976+
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
977+
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
978+
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
979+
980+
viewContext.FormContext.CanRenderAtEndOfForm = true;
981+
viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.EndOfForm;
982+
983+
var tagHelper = new InputTagHelper(htmlGenerator)
984+
{
985+
For = modelExpression,
986+
InputTypeName = inputTypeName,
987+
Name = propertyName,
988+
ViewContext = viewContext,
989+
};
990+
991+
var attributes = new TagHelperAttributeList
992+
{
993+
{ "name", propertyName },
994+
{ "type", inputTypeName },
995+
};
996+
997+
var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
998+
var output = new TagHelperOutput(
999+
expectedTagName,
1000+
new TagHelperAttributeList(),
1001+
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
1002+
{
1003+
TagMode = TagMode.SelfClosing,
1004+
};
1005+
1006+
// Act
1007+
await tagHelper.ProcessAsync(context, output);
1008+
1009+
// Assert
1010+
Assert.Equal(expectedAttributes, output.Attributes);
1011+
Assert.False(output.IsContentModified);
1012+
Assert.Equal(expectedTagName, output.TagName);
1013+
1014+
Assert.Equal(expectedEndOfFormContent, string.Join("", viewContext.FormContext.EndOfFormContent.Select(html => HtmlContentUtilities.HtmlContentToString(html))));
1015+
Assert.True(string.IsNullOrEmpty(HtmlContentUtilities.HtmlContentToString(output.PostElement)));
1016+
}
1017+
1018+
[Fact]
1019+
public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeEndOfForm_AndCanRenderAtEndOfFormNotSet()
1020+
{
1021+
var propertyName = "-expression-";
1022+
var expectedTagName = "input";
1023+
var expectedPostElementContent = $"<input name=\"HtmlEncode[[{propertyName}]]\" " +
1024+
"type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />";
1025+
var inputTypeName = "checkbox";
1026+
var expectedAttributes = new TagHelperAttributeList
1027+
{
1028+
{ "name", propertyName },
1029+
{ "type", inputTypeName },
1030+
{ "value", "true" },
1031+
};
1032+
1033+
var metadataProvider = new EmptyModelMetadataProvider();
1034+
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
1035+
var model = false;
1036+
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
1037+
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
1038+
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
1039+
1040+
viewContext.FormContext.CanRenderAtEndOfForm = false;
1041+
viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.EndOfForm;
1042+
1043+
var tagHelper = new InputTagHelper(htmlGenerator)
1044+
{
1045+
For = modelExpression,
1046+
InputTypeName = inputTypeName,
1047+
Name = propertyName,
1048+
ViewContext = viewContext,
1049+
};
1050+
1051+
var attributes = new TagHelperAttributeList
1052+
{
1053+
{ "name", propertyName },
1054+
{ "type", inputTypeName },
1055+
};
1056+
1057+
var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
1058+
var output = new TagHelperOutput(
1059+
expectedTagName,
1060+
new TagHelperAttributeList(),
1061+
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
1062+
{
1063+
TagMode = TagMode.SelfClosing,
1064+
};
1065+
1066+
// Act
1067+
await tagHelper.ProcessAsync(context, output);
1068+
1069+
// Assert
1070+
Assert.Equal(expectedAttributes, output.Attributes);
1071+
Assert.False(output.IsContentModified);
1072+
Assert.Equal(expectedTagName, output.TagName);
1073+
1074+
Assert.False(viewContext.FormContext.HasEndOfFormContent);
1075+
Assert.Equal(expectedPostElementContent, HtmlContentUtilities.HtmlContentToString(output.PostElement));
1076+
}
1077+
8411078
[Fact]
8421079
public async Task ProcessAsync_CallsGenerateCheckBox_WithExpectedParameters()
8431080
{

src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp3.0.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,12 @@ public static void TryAddModelException<TModel>(this Microsoft.AspNetCore.Mvc.Mo
312312
}
313313
namespace Microsoft.AspNetCore.Mvc.Rendering
314314
{
315+
public enum CheckBoxHiddenInputRenderMode
316+
{
317+
None = 0,
318+
Inline = 1,
319+
EndOfForm = 2,
320+
}
315321
public enum FormMethod
316322
{
317323
Get = 0,
@@ -678,6 +684,7 @@ public partial class ViewContext : Microsoft.AspNetCore.Mvc.ActionContext
678684
public ViewContext() { }
679685
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) { }
680686
public ViewContext(Microsoft.AspNetCore.Mvc.Rendering.ViewContext viewContext, Microsoft.AspNetCore.Mvc.ViewEngines.IView view, Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary viewData, System.IO.TextWriter writer) { }
687+
public Microsoft.AspNetCore.Mvc.Rendering.CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
681688
public bool ClientValidationEnabled { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
682689
public string ExecutingFilePath { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
683690
public virtual Microsoft.AspNetCore.Mvc.ViewFeatures.FormContext FormContext { get { throw null; } set { } }
@@ -1062,6 +1069,7 @@ public void EndForm() { }
10621069
public partial class HtmlHelperOptions
10631070
{
10641071
public HtmlHelperOptions() { }
1072+
public Microsoft.AspNetCore.Mvc.Rendering.CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
10651073
public bool ClientValidationEnabled { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
10661074
public Microsoft.AspNetCore.Mvc.Rendering.Html5DateRenderingMode Html5DateRenderingMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
10671075
public string IdAttributeDotReplacement { get { throw null; } set { } }

src/Mvc/Mvc.ViewFeatures/src/HtmlHelper.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -721,8 +721,18 @@ protected virtual IHtmlContent GenerateCheckBox(
721721
isChecked,
722722
htmlAttributes);
723723

724+
if (checkbox == null)
725+
{
726+
return HtmlString.Empty;
727+
}
728+
729+
if (ViewContext.CheckBoxHiddenInputRenderMode == CheckBoxHiddenInputRenderMode.None)
730+
{
731+
return checkbox;
732+
}
733+
724734
var hiddenForCheckbox = _htmlGenerator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, expression);
725-
if (checkbox == null || hiddenForCheckbox == null)
735+
if (hiddenForCheckbox == null)
726736
{
727737
return HtmlString.Empty;
728738
}
@@ -736,7 +746,7 @@ protected virtual IHtmlContent GenerateCheckBox(
736746
hiddenForCheckbox.MergeAttribute("name", name);
737747
}
738748

739-
if (ViewContext.FormContext.CanRenderAtEndOfForm)
749+
if (ViewContext.CheckBoxHiddenInputRenderMode == CheckBoxHiddenInputRenderMode.EndOfForm && ViewContext.FormContext.CanRenderAtEndOfForm)
740750
{
741751
ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckbox);
742752
return checkbox;

src/Mvc/Mvc.ViewFeatures/src/HtmlHelperOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,10 @@ public string IdAttributeDotReplacement
5656
/// <see cref="IHtmlHelper.ValidationSummary"/> and other overloads.
5757
/// </summary>
5858
public string ValidationSummaryMessageElement { get; set; } = "span";
59+
60+
/// <summary>
61+
/// Gets or sets the way hidden inputs are rendered for checkbox tag helpers and html helpers.
62+
/// </summary>
63+
public CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { get; set; } = CheckBoxHiddenInputRenderMode.EndOfForm;
5964
}
6065
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Mvc.Rendering
5+
{
6+
/// <summary>
7+
/// Controls the rendering of hidden input fields when using CheckBox tag helpers or html helpers.
8+
/// </summary>
9+
public enum CheckBoxHiddenInputRenderMode
10+
{
11+
/// <summary>
12+
/// Hidden input fields will not be automatically rendered. If checkbox is not checked, no value will be posted.
13+
/// </summary>
14+
None = 0,
15+
16+
/// <summary>
17+
/// Hidden input fields will be rendered inline with each checkbox. Use this for legacy ASP.NET MVC behavior.
18+
/// </summary>
19+
Inline = 1,
20+
21+
/// <summary>
22+
/// 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.
23+
/// If <see cref="Microsoft.AspNetCore.Mvc.ViewFeatures.FormContext.CanRenderAtEndOfForm"/> is <c>false</c>, will fall back on <see cref="Inline"/>.
24+
/// </summary>
25+
EndOfForm = 2
26+
}
27+
}

0 commit comments

Comments
 (0)