From d40e6612a4e77f701fe99ceab40a238564fec238 Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Fri, 9 Oct 2015 17:28:52 -0700 Subject: [PATCH] Moving the order of generated hidden tags for checkbox to end of the form - #2994 - Affects both HtmlHelper and TagHelper scenarios - Checkboxes not enclosed in a form will generate inline hidden tags - Added necessary properties to FormContext - Added some functional and unit tests --- .../InputTagHelper.cs | 10 ++- .../RenderAtEndOfFormTagHelper.cs | 61 +++++++++++++ .../Rendering/MvcForm.cs | 34 ++++++- .../ViewFeatures/FormContext.cs | 21 +++++ .../ViewFeatures/HtmlHelper.cs | 29 +++++- ...Site.HtmlGeneration_Home.EmployeeList.html | 8 +- ...ite.HtmlGeneration_Home.Order.Encoded.html | 4 +- ...tionWebSite.HtmlGeneration_Home.Order.html | 4 +- ...on_Home.OrderUsingHtmlHelpers.Encoded.html | 4 +- ...Generation_Home.OrderUsingHtmlHelpers.html | 4 +- .../RenderAtEndOfFormTagHelperTest.cs | 90 +++++++++++++++++++ .../Rendering/HtmlHelperCheckboxTest.cs | 29 +++++- .../Rendering/HtmlHelperFormTest.cs | 34 +++++++ .../Views/HtmlGeneration_Home/Order.cshtml | 1 + 14 files changed, 313 insertions(+), 20 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.TagHelpers/RenderAtEndOfFormTagHelper.cs create mode 100644 test/Microsoft.AspNet.Mvc.TagHelpers.Test/RenderAtEndOfFormTagHelperTest.cs diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/InputTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/InputTagHelper.cs index 481c6eab61..30ddaa7ffc 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/InputTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/InputTagHelper.cs @@ -278,7 +278,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers if (hiddenForCheckboxTag != null) { hiddenForCheckboxTag.TagRenderMode = renderingMode; - output.Content.Append(hiddenForCheckboxTag); + + if (ViewContext.FormContext.CanRenderAtEndOfForm) + { + ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckboxTag); + } + else + { + output.Content.Append(hiddenForCheckboxTag); + } } } } diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/RenderAtEndOfFormTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/RenderAtEndOfFormTagHelper.cs new file mode 100644 index 0000000000..1776b49e0f --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/RenderAtEndOfFormTagHelper.cs @@ -0,0 +1,61 @@ +// 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 System.ComponentModel; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Mvc.ViewFeatures; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; + +namespace Microsoft.AspNet.Mvc.TagHelpers +{ + /// + /// implementation targeting all form elements + /// to generate content before the form end tag. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [HtmlTargetElement("form")] + public class RenderAtEndOfFormTagHelper : TagHelper + { + public override int Order => -1000; + + [HtmlAttributeNotBound] + [ViewContext] + public ViewContext ViewContext { get; set; } + + /// + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (output == null) + { + throw new ArgumentNullException(nameof(output)); + } + + // Push the new FormContext. + ViewContext.FormContext = new FormContext + { + CanRenderAtEndOfForm = true + }; + + await context.GetChildContentAsync(); + + var formContext = ViewContext.FormContext; + if (formContext.HasEndOfFormContent) + { + foreach (var content in formContext.EndOfFormContent) + { + output.PostContent.Append(content); + } + } + + // Reset the FormContext + ViewContext.FormContext = null; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/MvcForm.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/MvcForm.cs index 2962a8db60..1d45c28ea0 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/MvcForm.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/MvcForm.cs @@ -3,6 +3,8 @@ using System; using Microsoft.AspNet.Mvc.ViewFeatures; +using Microsoft.Extensions.WebEncoders; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNet.Mvc.Rendering { @@ -19,9 +21,6 @@ namespace Microsoft.AspNet.Mvc.Rendering } _viewContext = viewContext; - - // Push the new FormContext; GenerateEndForm() does the corresponding pop. - _viewContext.FormContext = new FormContext(); } public void Dispose() @@ -40,6 +39,7 @@ namespace Microsoft.AspNet.Mvc.Rendering protected virtual void GenerateEndForm() { + RenderEndOfFormContent(); _viewContext.Writer.Write(""); _viewContext.FormContext = null; } @@ -52,5 +52,33 @@ namespace Microsoft.AspNet.Mvc.Rendering GenerateEndForm(); } } + + private void RenderEndOfFormContent() + { + var formContext = _viewContext.FormContext; + if (formContext.HasEndOfFormContent) + { + var writer = _viewContext.Writer; + var htmlWriter = writer as HtmlTextWriter; + + IHtmlEncoder htmlEncoder = null; + if (htmlWriter == null) + { + htmlEncoder = _viewContext.HttpContext.RequestServices.GetRequiredService(); + } + + foreach (var content in formContext.EndOfFormContent) + { + if (htmlWriter == null) + { + content.WriteTo(writer, htmlEncoder); + } + else + { + htmlWriter.Write(content); + } + } + } + } } } diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/FormContext.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/FormContext.cs index 90965ab5ce..6b9130533f 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/FormContext.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/FormContext.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.AspNet.Html.Abstractions; namespace Microsoft.AspNet.Mvc.ViewFeatures { @@ -11,6 +12,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures private readonly Dictionary _renderedFields = new Dictionary(StringComparer.Ordinal); private Dictionary _formData; + private IList _endOfFormContent; /// /// Property bag for any information you wish to associate with a <form/> in an @@ -29,6 +31,25 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures } } + public bool HasFormData => _formData != null; + + public bool HasEndOfFormContent => _endOfFormContent != null; + + public IList EndOfFormContent + { + get + { + if (_endOfFormContent == null) + { + _endOfFormContent = new List(); + } + + return _endOfFormContent; + } + } + + public bool CanRenderAtEndOfForm { get; set; } + public bool RenderedField(string fieldName) { if (fieldName == null) diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs index dccea37903..fbd327e4d9 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs @@ -272,12 +272,24 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures FormMethod method, object htmlAttributes) { + // Push the new FormContext; MvcForm.GenerateEndForm() does the corresponding pop. + _viewContext.FormContext = new FormContext + { + CanRenderAtEndOfForm = true + }; + return GenerateForm(actionName, controllerName, routeValues, method, htmlAttributes); } /// public MvcForm BeginRouteForm(string routeName, object routeValues, FormMethod method, object htmlAttributes) { + // Push the new FormContext; MvcForm.GenerateEndForm() does the corresponding pop. + _viewContext.FormContext = new FormContext + { + CanRenderAtEndOfForm = true + }; + return GenerateRouteForm(routeName, routeValues, method, htmlAttributes); } @@ -708,13 +720,24 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures isChecked, htmlAttributes); - var hidden = _htmlGenerator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, expression); - if (checkbox == null || hidden == null) + var hiddenForCheckboxTag = _htmlGenerator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, expression); + if (checkbox == null || hiddenForCheckboxTag == null) { return HtmlString.Empty; } - return new BufferedHtmlContent().Append(checkbox).Append(hidden); + var checkboxContent = new BufferedHtmlContent().Append(checkbox); + + if (ViewContext.FormContext.CanRenderAtEndOfForm) + { + ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckboxTag); + } + else + { + checkboxContent.Append(hiddenForCheckboxTag); + } + + return checkboxContent; } protected virtual string GenerateDisplayName(ModelExplorer modelExplorer, string expression) diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.EmployeeList.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.EmployeeList.html index 180133efc1..daacb9b15f 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.EmployeeList.html +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.EmployeeList.html @@ -24,7 +24,7 @@ EmployeeName_0
- +
@@ -55,7 +55,7 @@ EmployeeName_1
- +
@@ -86,7 +86,7 @@ EmployeeName_2
- +
@@ -94,5 +94,5 @@ EmployeeName_2
- + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html index 077dec0141..6f3ae7f3aa 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html @@ -33,7 +33,7 @@
- +
@@ -75,6 +75,6 @@
- + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.html index 0791154918..acd557197b 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.html +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.html @@ -33,7 +33,7 @@
- +
@@ -75,6 +75,6 @@
- + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.OrderUsingHtmlHelpers.Encoded.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.OrderUsingHtmlHelpers.Encoded.html index 51e857c22d..ed4c20ce9b 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.OrderUsingHtmlHelpers.Encoded.html +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.OrderUsingHtmlHelpers.Encoded.html @@ -33,7 +33,7 @@
- +
@@ -74,5 +74,5 @@
- + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.OrderUsingHtmlHelpers.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.OrderUsingHtmlHelpers.html index 1805de1052..3b0fd90fe1 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.OrderUsingHtmlHelpers.html +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.OrderUsingHtmlHelpers.html @@ -33,7 +33,7 @@
- +
@@ -74,5 +74,5 @@
- + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/RenderAtEndOfFormTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/RenderAtEndOfFormTagHelperTest.cs new file mode 100644 index 0000000000..d8be0430a6 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/RenderAtEndOfFormTagHelperTest.cs @@ -0,0 +1,90 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Xunit; + +namespace Microsoft.AspNet.Mvc.TagHelpers +{ + public class RenderAtEndOfFormTagHelperTest + { + public static TheoryData RenderAtEndOfFormTagHelperData + { + get + { + // tagBuilderList, expectedOutput + return new TheoryData, string> + { + { + new List + { + GetTagBuilder("input", "SomeName", "hidden", "false", TagRenderMode.SelfClosing) + }, + @"" + }, + { + new List + { + GetTagBuilder("input", "SomeName", "hidden", "false", TagRenderMode.SelfClosing), + GetTagBuilder("input", "SomeOtherName", "hidden", "false", TagRenderMode.SelfClosing) + }, + @"" + + @"" + } + }; + } + } + + [Theory] + [MemberData(nameof(RenderAtEndOfFormTagHelperData))] + public async Task Process_AddsHiddenInputTag_FromEndOfFormContent(List tagBuilderList, string expectedOutput) + { + // Arrange + var viewContext = new ViewContext(); + var tagHelperOutput = new TagHelperOutput( + tagName: "form", + attributes: new TagHelperAttributeList()); + + var tagHelperContext = new TagHelperContext( + Enumerable.Empty(), + new Dictionary(), + "someId", + (useCachedResult) => + { + Assert.True(viewContext.FormContext.CanRenderAtEndOfForm); + foreach (var item in tagBuilderList) + { + viewContext.FormContext.EndOfFormContent.Add(item); + } + + return Task.FromResult(new DefaultTagHelperContent()); + }); + + var tagHelper = new RenderAtEndOfFormTagHelper + { + ViewContext = viewContext + }; + + // Act + await tagHelper.ProcessAsync(context: tagHelperContext, output: tagHelperOutput); + + // Assert + Assert.Equal(expectedOutput, tagHelperOutput.PostContent.GetContent()); + } + + private static TagBuilder GetTagBuilder(string tag, string name, string type, string value, TagRenderMode mode) + { + var tagBuilder = new TagBuilder(tag); + tagBuilder.MergeAttribute("name", name); + tagBuilder.MergeAttribute("type", type); + tagBuilder.MergeAttribute("value", value); + tagBuilder.TagRenderMode = mode; + + return tagBuilder; + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/HtmlHelperCheckboxTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/HtmlHelperCheckboxTest.cs index 79aa75c029..c3d30b0209 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/HtmlHelperCheckboxTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/HtmlHelperCheckboxTest.cs @@ -5,11 +5,12 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Globalization; +using System.IO; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.TestCommon; using Microsoft.AspNet.Mvc.ViewFeatures; using Microsoft.AspNet.Routing; +using Microsoft.Extensions.WebEncoders.Testing; using Xunit; namespace Microsoft.AspNet.Mvc.Rendering @@ -127,6 +128,32 @@ namespace Microsoft.AspNet.Mvc.Rendering Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(html)); } + [Fact] + public void CheckBox_WithCanRenderAtEndOfFormSet_DoesNotGenerateInlineHiddenTag() + { + // Arrange + // Mono issue - https://github.com/aspnet/External/issues/19 + var expected = PlatformNormalizer.NormalizeContent( + @""); + var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetTestModelViewData()); + helper.ViewContext.FormContext.CanRenderAtEndOfForm = true; + + // 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 CommonTestEncoder()); + Assert.Equal("", + writer.ToString()); + } + [Fact] public void CheckBoxUsesAttemptedValueFromModelState() { diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/HtmlHelperFormTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/HtmlHelperFormTest.cs index be3449887f..0de46d3242 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/HtmlHelperFormTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/HtmlHelperFormTest.cs @@ -2,12 +2,15 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. #if MOCK_SUPPORT +using System; using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.Routing; using Microsoft.AspNet.Mvc.ViewFeatures; +using Microsoft.Extensions.WebEncoders; +using Microsoft.Extensions.WebEncoders.Testing; using Moq; using Xunit; @@ -318,6 +321,37 @@ namespace Microsoft.AspNet.Mvc.Rendering urlHelper.Verify(); } + [Fact] + public void EndForm_RendersHiddenTagForCheckBox() + { + // Arrange + var htmlHelper = DefaultTemplatesUtilities.GetHtmlHelper(); + var serviceProvider = new Mock(); + serviceProvider.Setup(s => s.GetService(typeof(IHtmlEncoder))).Returns(new CommonTestEncoder()); + var viewContext = htmlHelper.ViewContext; + viewContext.HttpContext.RequestServices = serviceProvider.Object; + + var writer = viewContext.Writer as StringWriter; + Assert.NotNull(writer); + var builder = writer.GetStringBuilder(); + + var tagBuilder = new TagBuilder("input"); + tagBuilder.MergeAttribute("name", "SomeName"); + tagBuilder.MergeAttribute("type", "hidden"); + tagBuilder.MergeAttribute("value", "false"); + tagBuilder.TagRenderMode = TagRenderMode.SelfClosing; + + htmlHelper.ViewContext.FormContext.EndOfFormContent.Add(tagBuilder); + + // Act + htmlHelper.EndForm(); + + // Assert + Assert.Equal( + "", + builder.ToString()); + } + private string GetHtmlAttributesAsString(object htmlAttributes) { var dictionary = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes); diff --git a/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Order.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Order.cshtml index d556d26231..22a634d701 100644 --- a/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Order.cshtml +++ b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Order.cshtml @@ -11,6 +11,7 @@ @addTagHelper "Microsoft.AspNet.Mvc.TagHelpers.SelectTagHelper, Microsoft.AspNet.Mvc.TagHelpers" @addTagHelper "Microsoft.AspNet.Mvc.TagHelpers.ValidationMessageTagHelper, Microsoft.AspNet.Mvc.TagHelpers" @addTagHelper "Microsoft.AspNet.Mvc.TagHelpers.ValidationSummaryTagHelper, Microsoft.AspNet.Mvc.TagHelpers" +@addTagHelper "Microsoft.AspNet.Mvc.TagHelpers.RenderAtEndOfFormTagHelper, Microsoft.AspNet.Mvc.TagHelpers"