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
This commit is contained in:
Ajay Bhargav Baaskaran 2015-10-09 17:28:52 -07:00
parent 03625c38af
commit d40e6612a4
14 changed files with 313 additions and 20 deletions

View File

@ -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);
}
}
}
}

View File

@ -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
{
/// <summary>
/// <see cref="ITagHelper"/> implementation targeting all form elements
/// to generate content before the form end tag.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
[HtmlTargetElement("form")]
public class RenderAtEndOfFormTagHelper : TagHelper
{
public override int Order => -1000;
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }
/// <inheritdoc />
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;
}
}
}

View File

@ -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("</form>");
_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<IHtmlEncoder>();
}
foreach (var content in formContext.EndOfFormContent)
{
if (htmlWriter == null)
{
content.WriteTo(writer, htmlEncoder);
}
else
{
htmlWriter.Write(content);
}
}
}
}
}
}

View File

@ -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<string, bool> _renderedFields =
new Dictionary<string, bool>(StringComparer.Ordinal);
private Dictionary<string, object> _formData;
private IList<IHtmlContent> _endOfFormContent;
/// <summary>
/// Property bag for any information you wish to associate with a &lt;form/&gt; in an
@ -29,6 +31,25 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
}
}
public bool HasFormData => _formData != null;
public bool HasEndOfFormContent => _endOfFormContent != null;
public IList<IHtmlContent> EndOfFormContent
{
get
{
if (_endOfFormContent == null)
{
_endOfFormContent = new List<IHtmlContent>();
}
return _endOfFormContent;
}
}
public bool CanRenderAtEndOfForm { get; set; }
public bool RenderedField(string fieldName)
{
if (fieldName == null)

View File

@ -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);
}
/// <inheritdoc />
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)

View File

@ -24,7 +24,7 @@ EmployeeName_0</textarea>
</div>
<div>
<label class="employee" for="z0__Remote">Remote</label>
<input data-val="true" data-val-required="The Remote field is required." id="z0__Remote" name="[0].Remote" type="checkbox" value="true" /><input name="[0].Remote" type="hidden" value="false" />
<input data-val="true" data-val-required="The Remote field is required." id="z0__Remote" name="[0].Remote" type="checkbox" value="true" />
</div>
<div>
<label class="employee" for="z0__OfficeNumber">OfficeNumber</label>
@ -55,7 +55,7 @@ EmployeeName_1</textarea>
</div>
<div>
<label class="employee" for="z1__Remote">Remote</label>
<input data-val="true" data-val-required="The Remote field is required." id="z1__Remote" name="[1].Remote" type="checkbox" value="true" /><input name="[1].Remote" type="hidden" value="false" />
<input data-val="true" data-val-required="The Remote field is required." id="z1__Remote" name="[1].Remote" type="checkbox" value="true" />
</div>
<div>
<label class="employee" for="z1__OfficeNumber">OfficeNumber</label>
@ -86,7 +86,7 @@ EmployeeName_2</textarea>
</div>
<div>
<label class="employee" for="z2__Remote">Remote</label>
<input checked="checked" data-val="true" data-val-required="The Remote field is required." id="z2__Remote" name="[2].Remote" type="checkbox" value="true" /><input name="[2].Remote" type="hidden" value="false" />
<input checked="checked" data-val="true" data-val-required="The Remote field is required." id="z2__Remote" name="[2].Remote" type="checkbox" value="true" />
</div>
<div>
<label class="employee" for="z2__OfficeNumber">OfficeNumber</label>
@ -94,5 +94,5 @@ EmployeeName_2</textarea>
<option>1002</option>
</select>
</div> <input type="submit" />
</form></body>
<input name="[0].Remote" type="hidden" value="false" /><input name="[1].Remote" type="hidden" value="false" /><input name="[2].Remote" type="hidden" value="false" /></form></body>
</html>

View File

@ -33,7 +33,7 @@
</div>
<div>
<label class="order" for="HtmlEncode[[NeedSpecialHandle]]">HtmlEncode[[NeedSpecialHandle]]</label>
<input checked="HtmlEncode[[checked]]" data-val="HtmlEncode[[true]]" data-val-required="HtmlEncode[[The NeedSpecialHandle field is required.]]" id="HtmlEncode[[NeedSpecialHandle]]" name="HtmlEncode[[NeedSpecialHandle]]" type="HtmlEncode[[checkbox]]" value="HtmlEncode[[true]]" /><input name="HtmlEncode[[NeedSpecialHandle]]" type="HtmlEncode[[hidden]]" value="HtmlEncode[[false]]" />
<input checked="HtmlEncode[[checked]]" data-val="HtmlEncode[[true]]" data-val-required="HtmlEncode[[The NeedSpecialHandle field is required.]]" id="HtmlEncode[[NeedSpecialHandle]]" name="HtmlEncode[[NeedSpecialHandle]]" type="HtmlEncode[[checkbox]]" value="HtmlEncode[[true]]" />
</div>
<div>
<label class="order" for="HtmlEncode[[PaymentMethod]]">HtmlEncode[[PaymentMethod]]</label>
@ -75,6 +75,6 @@
</ul></div>
<input type="HtmlEncode[[hidden]]" id="HtmlEncode[[Customer_Key]]" name="HtmlEncode[[Customer.Key]]" value="HtmlEncode[[KeyA]]" />
<input type="submit" />
<input name="HtmlEncode[[__RequestVerificationToken]]" type="HtmlEncode[[hidden]]" value="{0}" /></form>
<input name="HtmlEncode[[__RequestVerificationToken]]" type="HtmlEncode[[hidden]]" value="{0}" /><input name="HtmlEncode[[NeedSpecialHandle]]" type="HtmlEncode[[hidden]]" value="HtmlEncode[[false]]" /></form>
</body>
</html>

View File

@ -33,7 +33,7 @@
</div>
<div>
<label class="order" for="NeedSpecialHandle">NeedSpecialHandle</label>
<input checked="checked" data-val="true" data-val-required="The NeedSpecialHandle field is required." id="NeedSpecialHandle" name="NeedSpecialHandle" type="checkbox" value="true" /><input name="NeedSpecialHandle" type="hidden" value="false" />
<input checked="checked" data-val="true" data-val-required="The NeedSpecialHandle field is required." id="NeedSpecialHandle" name="NeedSpecialHandle" type="checkbox" value="true" />
</div>
<div>
<label class="order" for="PaymentMethod">PaymentMethod</label>
@ -75,6 +75,6 @@
</ul></div>
<input type="hidden" id="Customer_Key" name="Customer.Key" value="KeyA" />
<input type="submit" />
<input name="__RequestVerificationToken" type="hidden" value="{0}" /></form>
<input name="__RequestVerificationToken" type="hidden" value="{0}" /><input name="NeedSpecialHandle" type="hidden" value="false" /></form>
</body>
</html>

View File

@ -33,7 +33,7 @@
</div>
<div>
<label class="HtmlEncode[[order]]" for="HtmlEncode[[NeedSpecialHandle]]">HtmlEncode[[NeedSpecialHandle]]</label>
<input checked="HtmlEncode[[checked]]" data-val="HtmlEncode[[true]]" data-val-required="HtmlEncode[[The NeedSpecialHandle field is required.]]" id="HtmlEncode[[NeedSpecialHandle]]" name="HtmlEncode[[NeedSpecialHandle]]" type="HtmlEncode[[checkbox]]" value="HtmlEncode[[true]]" /><input name="HtmlEncode[[NeedSpecialHandle]]" type="HtmlEncode[[hidden]]" value="HtmlEncode[[false]]" />
<input checked="HtmlEncode[[checked]]" data-val="HtmlEncode[[true]]" data-val-required="HtmlEncode[[The NeedSpecialHandle field is required.]]" id="HtmlEncode[[NeedSpecialHandle]]" name="HtmlEncode[[NeedSpecialHandle]]" type="HtmlEncode[[checkbox]]" value="HtmlEncode[[true]]" />
</div>
<div>
<label class="HtmlEncode[[order]]" for="HtmlEncode[[PaymentMethod]]">HtmlEncode[[PaymentMethod]]</label>
@ -74,5 +74,5 @@
</ul></div>
<input id="HtmlEncode[[Customer_Key]]" name="HtmlEncode[[Customer.Key]]" type="HtmlEncode[[hidden]]" value="HtmlEncode[[KeyA]]" />
<input type="submit"/>
<input name="HtmlEncode[[__RequestVerificationToken]]" type="HtmlEncode[[hidden]]" value="{0}" /></form></body>
<input name="HtmlEncode[[__RequestVerificationToken]]" type="HtmlEncode[[hidden]]" value="{0}" /><input name="HtmlEncode[[NeedSpecialHandle]]" type="HtmlEncode[[hidden]]" value="HtmlEncode[[false]]" /></form></body>
</html>

View File

@ -33,7 +33,7 @@
</div>
<div>
<label class="order" for="NeedSpecialHandle">NeedSpecialHandle</label>
<input checked="checked" data-val="true" data-val-required="The NeedSpecialHandle field is required." id="NeedSpecialHandle" name="NeedSpecialHandle" type="checkbox" value="true" /><input name="NeedSpecialHandle" type="hidden" value="false" />
<input checked="checked" data-val="true" data-val-required="The NeedSpecialHandle field is required." id="NeedSpecialHandle" name="NeedSpecialHandle" type="checkbox" value="true" />
</div>
<div>
<label class="order" for="PaymentMethod">PaymentMethod</label>
@ -74,5 +74,5 @@
</ul></div>
<input id="Customer_Key" name="Customer.Key" type="hidden" value="KeyA" />
<input type="submit"/>
<input name="__RequestVerificationToken" type="hidden" value="{0}" /></form></body>
<input name="__RequestVerificationToken" type="hidden" value="{0}" /><input name="NeedSpecialHandle" type="hidden" value="false" /></form></body>
</html>

View File

@ -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<List<TagBuilder>, string>
{
{
new List<TagBuilder>
{
GetTagBuilder("input", "SomeName", "hidden", "false", TagRenderMode.SelfClosing)
},
@"<input name=""SomeName"" type=""hidden"" value=""false"" />"
},
{
new List<TagBuilder>
{
GetTagBuilder("input", "SomeName", "hidden", "false", TagRenderMode.SelfClosing),
GetTagBuilder("input", "SomeOtherName", "hidden", "false", TagRenderMode.SelfClosing)
},
@"<input name=""SomeName"" type=""hidden"" value=""false"" />" +
@"<input name=""SomeOtherName"" type=""hidden"" value=""false"" />"
}
};
}
}
[Theory]
[MemberData(nameof(RenderAtEndOfFormTagHelperData))]
public async Task Process_AddsHiddenInputTag_FromEndOfFormContent(List<TagBuilder> tagBuilderList, string expectedOutput)
{
// Arrange
var viewContext = new ViewContext();
var tagHelperOutput = new TagHelperOutput(
tagName: "form",
attributes: new TagHelperAttributeList());
var tagHelperContext = new TagHelperContext(
Enumerable.Empty<IReadOnlyTagHelperAttribute>(),
new Dictionary<object, object>(),
"someId",
(useCachedResult) =>
{
Assert.True(viewContext.FormContext.CanRenderAtEndOfForm);
foreach (var item in tagBuilderList)
{
viewContext.FormContext.EndOfFormContent.Add(item);
}
return Task.FromResult<TagHelperContent>(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;
}
}
}

View File

@ -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(
@"<input checked=""HtmlEncode[[checked]]"" data-val=""HtmlEncode[[true]]"" " +
@"data-val-required=""HtmlEncode[[The Boolean field is required.]]"" id=""HtmlEncode[[Property1]]"" " +
@"name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[checkbox]]"" " +
@"value=""HtmlEncode[[true]]"" />");
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("<input name=\"HtmlEncode[[Property1]]\" type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />",
writer.ToString());
}
[Fact]
public void CheckBoxUsesAttemptedValueFromModelState()
{

View File

@ -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<IServiceProvider>();
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(
"<input name=\"HtmlEncode[[SomeName]]\" type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" /></form>",
builder.ToString());
}
private string GetHtmlAttributesAsString(object htmlAttributes)
{
var dictionary = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);

View File

@ -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"
<html>
<head>