Build on be73cd7 to include `<input type="image"/>`, reduce duplication, and add tests

- consolidate `ButtonTagHelper` and `SubmitTagHelper` into `FormActionTagHelper`
- consolidate `ButtonTagHelperTest` and `SubmitTagHelperTest` into `FormActionTagHelperTest`

nits:
- do not allocate dictionaries in the `<a>` or `<form>` tag helpers unless needed
- clean up some hard-to-maintain whitespace
This commit is contained in:
Doug Bunting 2016-08-30 10:42:24 -07:00
parent be73cd77bf
commit 2659904061
10 changed files with 641 additions and 1079 deletions

View File

@ -162,7 +162,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Protocol != null ||
Host != null ||
Fragment != null ||
RouteValues.Count != 0)
(_routeValues != null && _routeValues.Count > 0))
{
// User specified an href and one of the bound attributes; can't determine the href attribute.
throw new InvalidOperationException(
@ -194,7 +194,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
routeValues = new RouteValueDictionary();
}
// Unconditionally replace any value from asp-route-area.
// Unconditionally replace any value from asp-route-area.
routeValues["area"] = Area;
}

View File

@ -12,7 +12,8 @@ using Microsoft.AspNetCore.Routing;
namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
/// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;button&gt; elements.
/// <see cref="ITagHelper"/> implementation targeting &lt;button&gt; elements and &lt;input&gt; elements with
/// their <c>type</c> attribute set to <c>image</c> or <c>submit</c>.
/// </summary>
[HtmlTargetElement("button", Attributes = ActionAttributeName)]
[HtmlTargetElement("button", Attributes = ControllerAttributeName)]
@ -20,22 +21,51 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
[HtmlTargetElement("button", Attributes = RouteAttributeName)]
[HtmlTargetElement("button", Attributes = RouteValuesDictionaryName)]
[HtmlTargetElement("button", Attributes = RouteValuesPrefix + "*")]
public class ButtonTagHelper : TagHelper
[HtmlTargetElement("input", Attributes = ImageActionAttributeSelector, TagStructure = TagStructure.WithoutEndTag)]
[HtmlTargetElement("input", Attributes = ImageControllerAttributeSelector, TagStructure = TagStructure.WithoutEndTag)]
[HtmlTargetElement("input", Attributes = ImageAreaAttributeSelector, TagStructure = TagStructure.WithoutEndTag)]
[HtmlTargetElement("input", Attributes = ImageRouteAttributeSelector, TagStructure = TagStructure.WithoutEndTag)]
[HtmlTargetElement("input", Attributes = ImageRouteValuesDictionarySelector, TagStructure = TagStructure.WithoutEndTag)]
[HtmlTargetElement("input", Attributes = ImageRouteValuesSelector, TagStructure = TagStructure.WithoutEndTag)]
[HtmlTargetElement("input", Attributes = SubmitActionAttributeSelector, TagStructure = TagStructure.WithoutEndTag)]
[HtmlTargetElement("input", Attributes = SubmitControllerAttributeSelector, TagStructure = TagStructure.WithoutEndTag)]
[HtmlTargetElement("input", Attributes = SubmitAreaAttributeSelector, TagStructure = TagStructure.WithoutEndTag)]
[HtmlTargetElement("input", Attributes = SubmitRouteAttributeSelector, TagStructure = TagStructure.WithoutEndTag)]
[HtmlTargetElement("input", Attributes = SubmitRouteValuesDictionarySelector, TagStructure = TagStructure.WithoutEndTag)]
[HtmlTargetElement("input", Attributes = SubmitRouteValuesSelector, TagStructure = TagStructure.WithoutEndTag)]
public class FormActionTagHelper : TagHelper
{
private const string ActionAttributeName = "asp-action";
private const string ControllerAttributeName = "asp-controller";
private const string AreaAttributeName = "asp-area";
private const string ControllerAttributeName = "asp-controller";
private const string RouteAttributeName = "asp-route";
private const string RouteValuesDictionaryName = "asp-all-route-data";
private const string RouteValuesPrefix = "asp-route-";
private const string FormAction = "formaction";
private const string ImageTypeSelector = "[type=image], ";
private const string ImageActionAttributeSelector = ImageTypeSelector + ActionAttributeName;
private const string ImageAreaAttributeSelector = ImageTypeSelector + AreaAttributeName;
private const string ImageControllerAttributeSelector = ImageTypeSelector + ControllerAttributeName;
private const string ImageRouteAttributeSelector = ImageTypeSelector + RouteAttributeName;
private const string ImageRouteValuesDictionarySelector = ImageTypeSelector + RouteValuesDictionaryName;
private const string ImageRouteValuesSelector = ImageTypeSelector + RouteValuesPrefix + "*";
private const string SubmitTypeSelector = "[type=submit], ";
private const string SubmitActionAttributeSelector = SubmitTypeSelector + ActionAttributeName;
private const string SubmitAreaAttributeSelector = SubmitTypeSelector + AreaAttributeName;
private const string SubmitControllerAttributeSelector = SubmitTypeSelector + ControllerAttributeName;
private const string SubmitRouteAttributeSelector = SubmitTypeSelector + RouteAttributeName;
private const string SubmitRouteValuesDictionarySelector = SubmitTypeSelector + RouteValuesDictionaryName;
private const string SubmitRouteValuesSelector = SubmitTypeSelector + RouteValuesPrefix + "*";
private IDictionary<string, string> _routeValues;
/// <summary>
/// Creates a new <see cref="ButtonTagHelper"/>.
/// Creates a new <see cref="FormActionTagHelper"/>.
/// </summary>
/// <param name="urlHelperFactory">The <see cref="IUrlHelperFactory"/>.</param>
public ButtonTagHelper(IUrlHelperFactory urlHelperFactory)
public FormActionTagHelper(IUrlHelperFactory urlHelperFactory)
{
UrlHelperFactory = urlHelperFactory;
}
@ -120,15 +150,20 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
throw new ArgumentNullException(nameof(output));
}
// If "formaction" is already set, it means the user is attempting to use a normal button.
// If "formaction" is already set, it means the user is attempting to use a normal button or input element.
if (output.Attributes.ContainsName(FormAction))
{
if (Action != null || Controller != null || Area != null || Route != null || RouteValues.Count != 0)
if (Action != null ||
Controller != null ||
Area != null ||
Route != null ||
(_routeValues != null && _routeValues.Count > 0))
{
// User specified a formaction and one of the bound attributes; can't determine the formaction attribute.
// User specified a formaction and one of the bound attributes; can't override that formaction
// attribute.
throw new InvalidOperationException(
Resources.FormatButtonTagHelper_CannotOverrideFormAction(
"<button>",
Resources.FormatFormActionTagHelper_CannotOverrideFormAction(
output.TagName,
ActionAttributeName,
ControllerAttributeName,
AreaAttributeName,
@ -152,7 +187,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
routeValues = new RouteValueDictionary();
}
// Unconditionally replace any value from asp-route-area.
// Unconditionally replace any value from asp-route-area.
routeValues["area"] = Area;
}
@ -166,8 +201,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
// Route and Action or Controller were specified. Can't determine the formaction attribute.
throw new InvalidOperationException(
Resources.FormatButtonTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified(
"<button>",
Resources.FormatFormActionTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified(
output.TagName,
RouteAttributeName,
ActionAttributeName,
ControllerAttributeName,

View File

@ -145,7 +145,11 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
// If "action" is already set, it means the user is attempting to use a normal <form>.
if (output.Attributes.ContainsName(HtmlActionAttributeName))
{
if (Action != null || Controller != null || Area != null || Route != null || RouteValues.Count != 0)
if (Action != null ||
Controller != null ||
Area != null ||
Route != null ||
(_routeValues != null && _routeValues.Count > 0))
{
// User also specified bound attributes we cannot use.
throw new InvalidOperationException(

View File

@ -42,70 +42,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
return string.Format(CultureInfo.CurrentCulture, GetString("AnchorTagHelper_CannotOverrideHref"), p0, p1, p2, p3, p4, p5, p6, p7, p8, p9);
}
/// <summary>
/// Cannot determine an '{4}' attribute for {0}. A {0} with a specified '{1}' must not have an '{2}' or '{3}' attribute.
/// </summary>
internal static string ButtonTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified
{
get { return GetString("ButtonTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified"); }
}
/// <summary>
/// Cannot determine an '{4}' attribute for {0}. A {0} with a specified '{1}' must not have an '{2}' or '{3}' attribute.
/// </summary>
internal static string FormatButtonTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified(object p0, object p1, object p2, object p3, object p4)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ButtonTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified"), p0, p1, p2, p3, p4);
}
/// <summary>
/// Cannot override the '{6}' attribute for {0}. A {0} with a specified '{6}' must not have attributes starting with '{5}' or an '{1}', '{2}', '{3}', or '{4}' attribute.
/// </summary>
internal static string ButtonTagHelper_CannotOverrideFormAction
{
get { return GetString("ButtonTagHelper_CannotOverrideFormAction"); }
}
/// <summary>
/// Cannot override the '{6}' attribute for {0}. A {0} with a specified '{6}' must not have attributes starting with '{5}' or an '{1}', '{2}', '{3}', or '{4}' attribute.
/// </summary>
internal static string FormatButtonTagHelper_CannotOverrideFormAction(object p0, object p1, object p2, object p3, object p4, object p5, object p6)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ButtonTagHelper_CannotOverrideFormAction"), p0, p1, p2, p3, p4, p5, p6);
}
/// <summary>
/// Cannot determine an '{4}' attribute for {0}. An {0} with a specified '{1}' must not have an '{2}' or '{3}' attribute.
/// </summary>
internal static string SubmitTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified
{
get { return GetString("SubmitTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified"); }
}
/// <summary>
/// Cannot determine an '{4}' attribute for {0}. An {0} with a specified '{1}' must not have an '{2}' or '{3}' attribute.
/// </summary>
internal static string FormatSubmitTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified(object p0, object p1, object p2, object p3, object p4)
{
return string.Format(CultureInfo.CurrentCulture, GetString("SubmitTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified"), p0, p1, p2, p3, p4);
}
/// <summary>
/// Cannot override the '{6}' attribute for {0}. An {0} with a specified '{6}' must not have attributes starting with '{5}' or an '{1}', '{2}', '{3}', or '{4}' attribute.
/// </summary>
internal static string SubmitTagHelper_CannotOverrideFormAction
{
get { return GetString("SubmitTagHelper_CannotOverrideFormAction"); }
}
/// <summary>
/// Cannot override the '{6}' attribute for {0}. An {0} with a specified '{6}' must not have attributes starting with '{5}' or an '{1}', '{2}', '{3}', or '{4}' attribute.
/// </summary>
internal static string FormatSubmitTagHelper_CannotOverrideFormAction(object p0, object p1, object p2, object p3, object p4, object p5, object p6)
{
return string.Format(CultureInfo.CurrentCulture, GetString("SubmitTagHelper_CannotOverrideFormAction"), p0, p1, p2, p3, p4, p5, p6);
}
/// <summary>
/// Cannot override the '{1}' attribute for {0}. A {0} with a specified '{1}' must not have attributes starting with '{6}' or an '{2}' or '{3}' or '{4}' or '{5}' attribute.
/// </summary>
@ -234,6 +170,38 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
return string.Format(CultureInfo.CurrentCulture, GetString("PropertyOfTypeCannotBeNull"), p0, p1);
}
/// <summary>
/// Cannot override the '{6}' attribute for &lt;{0}&gt;. &lt;{0}&gt; elements with a specified '{6}' must not have attributes starting with '{5}' or an '{1}', '{2}', '{3}', or '{4}' attribute.
/// </summary>
internal static string FormActionTagHelper_CannotOverrideFormAction
{
get { return GetString("FormActionTagHelper_CannotOverrideFormAction"); }
}
/// <summary>
/// Cannot override the '{6}' attribute for &lt;{0}&gt;. &lt;{0}&gt; elements with a specified '{6}' must not have attributes starting with '{5}' or an '{1}', '{2}', '{3}', or '{4}' attribute.
/// </summary>
internal static string FormatFormActionTagHelper_CannotOverrideFormAction(object p0, object p1, object p2, object p3, object p4, object p5, object p6)
{
return string.Format(CultureInfo.CurrentCulture, GetString("FormActionTagHelper_CannotOverrideFormAction"), p0, p1, p2, p3, p4, p5, p6);
}
/// <summary>
/// Cannot determine a '{4}' attribute for &lt;{0}&gt;. &lt;{0}&gt; elements with a specified '{1}' must not have an '{2}' or '{3}' attribute.
/// </summary>
internal static string FormActionTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified
{
get { return GetString("FormActionTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified"); }
}
/// <summary>
/// Cannot determine a '{4}' attribute for &lt;{0}&gt;. &lt;{0}&gt; elements with a specified '{1}' must not have an '{2}' or '{3}' attribute.
/// </summary>
internal static string FormatFormActionTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified(object p0, object p1, object p2, object p3, object p4)
{
return string.Format(CultureInfo.CurrentCulture, GetString("FormActionTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified"), p0, p1, p2, p3, p4);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -147,16 +147,10 @@
<data name="PropertyOfTypeCannotBeNull" xml:space="preserve">
<value>The '{0}' property of '{1}' must not be null.</value>
</data>
<data name="ButtonTagHelper_CannotOverrideFormAction" xml:space="preserve">
<value>Cannot override the '{6}' attribute for {0}. A {0} with a specified '{6}' must not have attributes starting with '{5}' or an '{1}', '{2}', '{3}', or '{4}' attribute.</value>
<data name="FormActionTagHelper_CannotOverrideFormAction" xml:space="preserve">
<value>Cannot override the '{6}' attribute for &lt;{0}&gt;. &lt;{0}&gt; elements with a specified '{6}' must not have attributes starting with '{5}' or an '{1}', '{2}', '{3}', or '{4}' attribute.</value>
</data>
<data name="ButtonTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified" xml:space="preserve">
<value>Cannot determine a '{4}' attribute for {0}. A {0} with a specified '{1}' must not have an '{2}' or '{3}' attribute.</value>
</data>
<data name="SubmitTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified" xml:space="preserve">
<value>Cannot determine a '{4}' attribute for {0}. An {0} with a specified '{1}' must not have an '{2}' or '{3}' attribute.</value>
</data>
<data name="SubmitTagHelper_CannotOverrideFormAction" xml:space="preserve">
<value>Cannot override the '{6}' attribute for {0}. An {0} with a specified '{6}' must not have attributes starting with '{5}' or an '{1}', '{2}', '{3}', or '{4}' attribute.</value>
<data name="FormActionTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified" xml:space="preserve">
<value>Cannot determine a '{4}' attribute for &lt;{0}&gt;. &lt;{0}&gt; elements with a specified '{1}' must not have an '{2}' or '{3}' attribute.</value>
</data>
</root>

View File

@ -1,193 +0,0 @@
// 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.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Routing;
namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
/// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;input&gt; elements with <c>type</c> attribute set to <c>submit</c>.
/// </summary>
[HtmlTargetElement("input", Attributes = ActionAttributeSelector)]
[HtmlTargetElement("input", Attributes = ControllerAttributeSelector)]
[HtmlTargetElement("input", Attributes = AreaAttributeSelector)]
[HtmlTargetElement("input", Attributes = RouteAttributeSelector)]
[HtmlTargetElement("input", Attributes = RouteValuesDictionarySelector)]
[HtmlTargetElement("input", Attributes = RouteValuesSelector)]
public class SubmitTagHelper : TagHelper
{
private const string ActionAttributeSelector = TypeSelector + ActionAttributeName;
private const string ControllerAttributeSelector = TypeSelector + ControllerAttributeName;
private const string AreaAttributeSelector = TypeSelector + AreaAttributeName;
private const string RouteAttributeSelector = TypeSelector + RouteAttributeName;
private const string RouteValuesDictionarySelector = TypeSelector + RouteValuesDictionaryName;
private const string RouteValuesSelector = TypeSelector + RouteValuesPrefix + "*";
private const string TypeSelector = "[type=submit], ";
private const string ActionAttributeName = "asp-action";
private const string ControllerAttributeName = "asp-controller";
private const string AreaAttributeName = "asp-area";
private const string RouteAttributeName = "asp-route";
private const string RouteValuesDictionaryName = "asp-all-route-data";
private const string RouteValuesPrefix = "asp-route-";
private const string FormAction = "formaction";
private IDictionary<string, string> _routeValues;
/// <summary>
/// Creates a new <see cref="SubmitTagHelper"/>.
/// </summary>
/// <param name="urlHelperFactory">The <see cref="IUrlHelperFactory"/>.</param>
public SubmitTagHelper(IUrlHelperFactory urlHelperFactory)
{
UrlHelperFactory = urlHelperFactory;
}
/// <inheritdoc />
public override int Order => -1000;
/// <summary>
/// Gets or sets the <see cref="Rendering.ViewContext"/> for the current request.
/// </summary>
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }
protected IUrlHelperFactory UrlHelperFactory { get; }
/// <summary>
/// The name of the action method.
/// </summary>
[HtmlAttributeName(ActionAttributeName)]
public string Action { get; set; }
/// <summary>
/// The name of the controller.
/// </summary>
[HtmlAttributeName(ControllerAttributeName)]
public string Controller { get; set; }
/// <summary>
/// The name of the area.
/// </summary>
[HtmlAttributeName(AreaAttributeName)]
public string Area { get; set; }
/// <summary>
/// Name of the route.
/// </summary>
/// <remarks>
/// Must be <c>null</c> if <see cref="Action"/> or <see cref="Controller"/> is non-<c>null</c>.
/// </remarks>
[HtmlAttributeName(RouteAttributeName)]
public string Route { get; set; }
/// <summary>
/// Additional parameters for the route.
/// </summary>
[HtmlAttributeName(RouteValuesDictionaryName, DictionaryAttributePrefix = RouteValuesPrefix)]
public IDictionary<string, string> RouteValues
{
get
{
if (_routeValues == null)
{
_routeValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
return _routeValues;
}
set
{
_routeValues = value;
}
}
/// <inheritdoc />
/// <remarks>Does nothing if user provides an <c>formaction</c> attribute.</remarks>
/// <exception cref="InvalidOperationException">
/// Thrown if <c>formaction</c> attribute is provided and <see cref="Action"/>, <see cref="Controller"/>,
/// or <see cref="Route"/> are non-<c>null</c> or if the user provided <c>asp-route-*</c> attributes.
/// Also thrown if <see cref="Route"/> and one or both of <see cref="Action"/> and <see cref="Controller"/>
/// are non-<c>null</c>
/// </exception>
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (output == null)
{
throw new ArgumentNullException(nameof(output));
}
// If "formaction" is already set, it means the user is attempting to use a normal submit input.
if (output.Attributes.ContainsName(FormAction))
{
if (Action != null || Controller != null || Area != null || Route != null || RouteValues.Count != 0)
{
// User specified a formaction and one of the bound attributes; can't determine the formaction attribute.
throw new InvalidOperationException(
Resources.FormatSubmitTagHelper_CannotOverrideFormAction(
"<input>",
ActionAttributeName,
ControllerAttributeName,
AreaAttributeName,
RouteAttributeName,
RouteValuesPrefix,
FormAction));
}
}
else
{
RouteValueDictionary routeValues = null;
if (_routeValues != null && _routeValues.Count > 0)
{
routeValues = new RouteValueDictionary(_routeValues);
}
if (Area != null)
{
if (routeValues == null)
{
routeValues = new RouteValueDictionary();
}
// Unconditionally replace any value from asp-route-area.
routeValues["area"] = Area;
}
if (Route == null)
{
var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext);
var url = urlHelper.Action(Action, Controller, routeValues);
output.Attributes.SetAttribute(FormAction, url);
}
else if (Action != null || Controller != null)
{
// Route and Action or Controller were specified. Can't determine the formaction attribute.
throw new InvalidOperationException(
Resources.FormatSubmitTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified(
"<input>",
RouteAttributeName,
ActionAttributeName,
ControllerAttributeName,
FormAction));
}
else
{
var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext);
var url = urlHelper.RouteUrl(Route, routeValues);
output.Attributes.SetAttribute(FormAction, url);
}
}
}
}
}

View File

@ -57,9 +57,10 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
.Setup(mock => mock.Action(It.IsAny<UrlActionContext>())).Returns("home/index");
var htmlGenerator = new TestableHtmlGenerator(metadataProvider, urlHelper.Object);
var viewContext = TestableHtmlGenerator.GetViewContext(model: null,
htmlGenerator: htmlGenerator,
metadataProvider: metadataProvider);
var viewContext = TestableHtmlGenerator.GetViewContext(
model: null,
htmlGenerator: htmlGenerator,
metadataProvider: metadataProvider);
var anchorTagHelper = new AnchorTagHelper(htmlGenerator)
{
Action = "index",

View File

@ -1,381 +0,0 @@
// 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.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Routing;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
public class ButtonTagHelperTest
{
[Fact]
public async Task ProcessAsync_GeneratesExpectedOutput()
{
// Arrange
var expectedTagName = "not-button";
var metadataProvider = new TestModelMetadataProvider();
var tagHelperContext = new TagHelperContext(
allAttributes: new TagHelperAttributeList
{
{ "id", "mybutton" },
{ "asp-route-name", "value" },
{ "asp-action", "index" },
{ "asp-controller", "home" },
},
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
expectedTagName,
attributes: new TagHelperAttributeList
{
{ "id", "mybutton" },
},
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something Else");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
output.Content.SetContent("Something");
var urlHelper = new Mock<IUrlHelper>();
urlHelper
.Setup(mock => mock.Action(It.IsAny<UrlActionContext>())).Returns("home/index").Verifiable();
var urlHelperFactory = new Mock<IUrlHelperFactory>();
urlHelperFactory
.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
.Returns(urlHelper.Object);
var htmlGenerator = new TestableHtmlGenerator(metadataProvider, urlHelper.Object);
var viewContext = TestableHtmlGenerator.GetViewContext(model: null,
htmlGenerator: htmlGenerator,
metadataProvider: metadataProvider);
var buttonTagHelper = new ButtonTagHelper(urlHelperFactory.Object)
{
Action = "index",
Controller = "home",
RouteValues =
{
{ "name", "value" },
},
ViewContext = viewContext,
};
// Act
await buttonTagHelper.ProcessAsync(tagHelperContext, output);
// Assert
urlHelper.Verify();
Assert.Equal(2, output.Attributes.Count);
var attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("id"));
Assert.Equal("mybutton", attribute.Value);
attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("formaction"));
Assert.Equal("home/index", attribute.Value);
Assert.Equal("Something", output.Content.GetContent());
Assert.Equal(expectedTagName, output.TagName);
}
[Fact]
public async Task ProcessAsync_CallsIntoRouteLinkWithExpectedParameters()
{
// Arrange
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(
Enumerable.Empty<TagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
"button",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
output.Content.SetContent(string.Empty);
var urlHelper = new Mock<IUrlHelper>();
urlHelper
.Setup(mock => mock.RouteUrl(It.IsAny<UrlRouteContext>())).Returns("home/index").Verifiable();
var urlHelperFactory = new Mock<IUrlHelperFactory>();
urlHelperFactory
.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
.Returns(urlHelper.Object);
var metadataProvider = new TestModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider, urlHelper.Object);
var viewContext = TestableHtmlGenerator.GetViewContext(model: null,
htmlGenerator: htmlGenerator,
metadataProvider: metadataProvider);
var buttonTagHelper = new ButtonTagHelper(urlHelperFactory.Object)
{
Route = "Default",
ViewContext = viewContext,
RouteValues =
{
{ "name", "value" },
},
};
// Act
await buttonTagHelper.ProcessAsync(context, output);
// Assert
urlHelper.Verify();
Assert.Equal("button", output.TagName);
Assert.Equal(1, output.Attributes.Count);
var attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("formaction"));
Assert.Equal("home/index", attribute.Value);
Assert.True(output.Content.GetContent().Length == 0);
}
[Fact]
public async Task ProcessAsync_AddsAreaToRouteValuesAndCallsIntoActionLinkWithExpectedParameters()
{
// Arrange
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(
Enumerable.Empty<TagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
"button",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
output.Content.SetContent(string.Empty);
var expectedRouteValues = new RouteValueDictionary(new Dictionary<string, string> { { "area", "Admin" } });
var urlHelper = new Mock<IUrlHelper>();
urlHelper
.Setup(mock => mock.Action(It.IsAny<UrlActionContext>()))
.Returns("admin/dashboard/index")
.Callback<UrlActionContext>(param => Assert.Equal(param.Values, expectedRouteValues))
.Verifiable();
var urlHelperFactory = new Mock<IUrlHelperFactory>();
urlHelperFactory
.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
.Returns(urlHelper.Object);
var buttonTagHelper = new ButtonTagHelper(urlHelperFactory.Object)
{
Action = "Index",
Controller = "Dashboard",
Area = "Admin",
};
// Act
await buttonTagHelper.ProcessAsync(context, output);
// Assert
urlHelper.Verify();
Assert.Equal("button", output.TagName);
Assert.Equal(1, output.Attributes.Count);
var attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("formaction"));
Assert.Equal("admin/dashboard/index", attribute.Value);
Assert.True(output.Content.GetContent().Length == 0);
}
[Fact]
public async Task ProcessAsync_AspAreaOverridesAspRouteArea()
{
// Arrange
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(
Enumerable.Empty<TagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
"button",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
output.Content.SetContent(string.Empty);
var expectedRouteValues = new RouteValueDictionary(new Dictionary<string, string> { { "area", "Admin" } });
var urlHelper = new Mock<IUrlHelper>();
urlHelper
.Setup(mock => mock.Action(It.IsAny<UrlActionContext>()))
.Returns("admin/dashboard/index")
.Callback<UrlActionContext>(param => Assert.Equal(param.Values, expectedRouteValues))
.Verifiable();
var urlHelperFactory = new Mock<IUrlHelperFactory>();
urlHelperFactory
.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
.Returns(urlHelper.Object);
var buttonTagHelper = new ButtonTagHelper(urlHelperFactory.Object)
{
Action = "Index",
Controller = "Dashboard",
Area = "Admin",
RouteValues = new Dictionary<string, string> { { "area", "Home" } }
};
// Act
await buttonTagHelper.ProcessAsync(context, output);
// Assert
urlHelper.Verify();
Assert.Equal("button", output.TagName);
Assert.Equal(1, output.Attributes.Count);
var attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("formaction"));
Assert.Equal("admin/dashboard/index", attribute.Value);
Assert.True(output.Content.GetContent().Length == 0);
}
[Fact]
public async Task ProcessAsync_EmptyStringOnAspAreaIsPassedThroughToRouteValues()
{
// Arrange
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(
Enumerable.Empty<TagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
"button",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
output.Content.SetContent(string.Empty);
var expectedRouteValues = new RouteValueDictionary(new Dictionary<string, string> { { "area", string.Empty } });
var urlHelper = new Mock<IUrlHelper>();
urlHelper
.Setup(mock => mock.Action(It.IsAny<UrlActionContext>()))
.Returns("admin/dashboard/index")
.Callback<UrlActionContext>(param => Assert.Equal(param.Values, expectedRouteValues))
.Verifiable();
var urlHelperFactory = new Mock<IUrlHelperFactory>();
urlHelperFactory
.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
.Returns(urlHelper.Object);
var buttonTagHelper = new ButtonTagHelper(urlHelperFactory.Object)
{
Action = "Index",
Controller = "Dashboard",
Area = string.Empty,
};
// Act
await buttonTagHelper.ProcessAsync(context, output);
// Assert
urlHelper.Verify();
Assert.Equal("button", output.TagName);
Assert.Equal(1, output.Attributes.Count);
var attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("formaction"));
Assert.Equal("admin/dashboard/index", attribute.Value);
Assert.True(output.Content.GetContent().Length == 0);
}
[Theory]
[InlineData("Action")]
[InlineData("Controller")]
[InlineData("Route")]
[InlineData("asp-route-")]
public async Task ProcessAsync_ThrowsIfFormActionConflictsWithBoundAttributes(string propertyName)
{
// Arrange
var urlHelperFactory = new Mock<IUrlHelperFactory>().Object;
var buttonTagHelper = new ButtonTagHelper(urlHelperFactory);
var output = new TagHelperOutput(
"button",
attributes: new TagHelperAttributeList
{
{ "formaction", "my-action" }
},
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
if (propertyName == "asp-route-")
{
buttonTagHelper.RouteValues.Add("name", "value");
}
else
{
typeof(ButtonTagHelper).GetProperty(propertyName).SetValue(buttonTagHelper, "Home");
}
var expectedErrorMessage = "Cannot override the 'formaction' attribute for <button>. A <button> with a specified " +
"'formaction' must not have attributes starting with 'asp-route-' or an " +
"'asp-action', 'asp-controller', 'asp-area', or 'asp-route' attribute.";
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(
Enumerable.Empty<TagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => buttonTagHelper.ProcessAsync(context, output));
Assert.Equal(expectedErrorMessage, ex.Message);
}
[Theory]
[InlineData("Action")]
[InlineData("Controller")]
public async Task ProcessAsync_ThrowsIfRouteAndActionOrControllerProvided(string propertyName)
{
// Arrange
var urlHelperFactory = new Mock<IUrlHelperFactory>().Object;
var buttonTagHelper = new ButtonTagHelper(urlHelperFactory)
{
Route = "Default",
};
typeof(ButtonTagHelper).GetProperty(propertyName).SetValue(buttonTagHelper, "Home");
var output = new TagHelperOutput(
"button",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
var expectedErrorMessage = "Cannot determine a 'formaction' attribute for <button>. A <button> with a specified " +
"'asp-route' must not have an 'asp-action' or 'asp-controller' attribute.";
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(
Enumerable.Empty<TagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => buttonTagHelper.ProcessAsync(context, output));
Assert.Equal(expectedErrorMessage, ex.Message);
}
}
}

View File

@ -0,0 +1,519 @@
// 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.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Routing;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
public class FormActionTagHelperTest
{
[Fact]
public async Task ProcessAsync_GeneratesExpectedOutput()
{
// Arrange
var expectedTagName = "not-button-or-submit";
var metadataProvider = new TestModelMetadataProvider();
var tagHelperContext = new TagHelperContext(
allAttributes: new TagHelperAttributeList
{
{ "id", "my-id" },
},
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
expectedTagName,
attributes: new TagHelperAttributeList
{
{ "id", "my-id" },
},
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something Else"); // ignored
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
var urlHelper = new Mock<IUrlHelper>(MockBehavior.Strict);
urlHelper
.Setup(mock => mock.Action(It.IsAny<UrlActionContext>()))
.Returns<UrlActionContext>(c => $"{c.Controller}/{c.Action}/{(c.Values as RouteValueDictionary)["name"]}");
var viewContext = new ViewContext();
var urlHelperFactory = new Mock<IUrlHelperFactory>(MockBehavior.Strict);
urlHelperFactory
.Setup(f => f.GetUrlHelper(viewContext))
.Returns(urlHelper.Object);
var tagHelper = new FormActionTagHelper(urlHelperFactory.Object)
{
Action = "index",
Controller = "home",
RouteValues =
{
{ "name", "value" },
},
ViewContext = viewContext,
};
// Act
await tagHelper.ProcessAsync(tagHelperContext, output);
// Assert
Assert.Collection(
output.Attributes,
attribute =>
{
Assert.Equal("id", attribute.Name, StringComparer.Ordinal);
Assert.Equal("my-id", attribute.Value as string, StringComparer.Ordinal);
},
attribute =>
{
Assert.Equal("formaction", attribute.Name, StringComparer.Ordinal);
Assert.Equal("home/index/value", attribute.Value as string, StringComparer.Ordinal);
});
Assert.False(output.IsContentModified);
Assert.False(output.PostContent.IsModified);
Assert.False(output.PostElement.IsModified);
Assert.False(output.PreContent.IsModified);
Assert.False(output.PreElement.IsModified);
Assert.Equal(TagMode.StartTagAndEndTag, output.TagMode);
Assert.Equal(expectedTagName, output.TagName);
}
[Fact]
public async Task ProcessAsync_GeneratesExpectedOutput_WithRoute()
{
// Arrange
var expectedTagName = "not-button-or-submit";
var metadataProvider = new TestModelMetadataProvider();
var tagHelperContext = new TagHelperContext(
allAttributes: new TagHelperAttributeList
{
{ "id", "my-id" },
},
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
expectedTagName,
attributes: new TagHelperAttributeList
{
{ "id", "my-id" },
},
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something Else"); // ignored
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
var urlHelper = new Mock<IUrlHelper>(MockBehavior.Strict);
urlHelper
.Setup(mock => mock.RouteUrl(It.IsAny<UrlRouteContext>()))
.Returns<UrlRouteContext>(c => $"{c.RouteName}/{(c.Values as RouteValueDictionary)["name"]}");
var viewContext = new ViewContext();
var urlHelperFactory = new Mock<IUrlHelperFactory>(MockBehavior.Strict);
urlHelperFactory
.Setup(f => f.GetUrlHelper(viewContext))
.Returns(urlHelper.Object);
var tagHelper = new FormActionTagHelper(urlHelperFactory.Object)
{
Route = "routine",
RouteValues =
{
{ "name", "value" },
},
ViewContext = viewContext,
};
// Act
await tagHelper.ProcessAsync(tagHelperContext, output);
// Assert
Assert.Collection(
output.Attributes,
attribute =>
{
Assert.Equal("id", attribute.Name, StringComparer.Ordinal);
Assert.Equal("my-id", attribute.Value as string, StringComparer.Ordinal);
},
attribute =>
{
Assert.Equal("formaction", attribute.Name, StringComparer.Ordinal);
Assert.Equal("routine/value", attribute.Value as string, StringComparer.Ordinal);
});
Assert.False(output.IsContentModified);
Assert.False(output.PostContent.IsModified);
Assert.False(output.PostElement.IsModified);
Assert.False(output.PreContent.IsModified);
Assert.False(output.PreElement.IsModified);
Assert.Equal(TagMode.StartTagAndEndTag, output.TagMode);
Assert.Equal(expectedTagName, output.TagName);
}
// RouteValues property value, expected RouteValuesDictionary content.
public static TheoryData<IDictionary<string, string>, IDictionary<string, object>> RouteValuesData
{
get
{
return new TheoryData<IDictionary<string, string>, IDictionary<string, object>>
{
{ null, null },
// FormActionTagHelper ignores an empty route values dictionary.
{ new Dictionary<string, string>(), null },
{
new Dictionary<string, string> { { "name", "value" } },
new Dictionary<string, object> { { "name", "value" } }
},
{
new SortedDictionary<string, string>(StringComparer.Ordinal)
{
{ "name1", "value1" },
{ "name2", "value2" },
},
new SortedDictionary<string, object>(StringComparer.Ordinal)
{
{ "name1", "value1" },
{ "name2", "value2" },
}
},
};
}
}
[Theory]
[MemberData(nameof(RouteValuesData))]
public async Task ProcessAsync_CallsActionWithExpectedParameters(
IDictionary<string, string> routeValues,
IDictionary<string, object> expectedRouteValues)
{
// Arrange
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(),
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
"button",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
return Task.FromResult<TagHelperContent>(new DefaultTagHelperContent());
});
var urlHelper = new Mock<IUrlHelper>(MockBehavior.Strict);
urlHelper
.Setup(mock => mock.Action(It.IsAny<UrlActionContext>()))
.Callback<UrlActionContext>(param =>
{
Assert.Equal("delete", param.Action, StringComparer.Ordinal);
Assert.Equal("books", param.Controller, StringComparer.Ordinal);
Assert.Null(param.Fragment);
Assert.Null(param.Host);
Assert.Null(param.Protocol);
Assert.Equal<KeyValuePair<string, object>>(expectedRouteValues, param.Values as RouteValueDictionary);
})
.Returns("home/index");
var viewContext = new ViewContext();
var urlHelperFactory = new Mock<IUrlHelperFactory>(MockBehavior.Strict);
urlHelperFactory
.Setup(f => f.GetUrlHelper(viewContext))
.Returns(urlHelper.Object);
var tagHelper = new FormActionTagHelper(urlHelperFactory.Object)
{
Action = "delete",
Controller = "books",
RouteValues = routeValues,
ViewContext = viewContext,
};
// Act
await tagHelper.ProcessAsync(context, output);
// Assert
Assert.Equal("button", output.TagName);
var attribute = Assert.Single(output.Attributes);
Assert.Equal("formaction", attribute.Name);
Assert.Equal("home/index", attribute.Value);
Assert.Empty(output.Content.GetContent());
}
[Theory]
[MemberData(nameof(RouteValuesData))]
public async Task ProcessAsync_CallsRouteUrlWithExpectedParameters(
IDictionary<string, string> routeValues,
IDictionary<string, object> expectedRouteValues)
{
// Arrange
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(),
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
"button",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
return Task.FromResult<TagHelperContent>(new DefaultTagHelperContent());
});
var urlHelper = new Mock<IUrlHelper>(MockBehavior.Strict);
urlHelper
.Setup(mock => mock.RouteUrl(It.IsAny<UrlRouteContext>()))
.Callback<UrlRouteContext>(param =>
{
Assert.Null(param.Fragment);
Assert.Null(param.Host);
Assert.Null(param.Protocol);
Assert.Equal("Default", param.RouteName, StringComparer.Ordinal);
Assert.Equal<KeyValuePair<string, object>>(expectedRouteValues, param.Values as RouteValueDictionary);
})
.Returns("home/index");
var viewContext = new ViewContext();
var urlHelperFactory = new Mock<IUrlHelperFactory>(MockBehavior.Strict);
urlHelperFactory
.Setup(f => f.GetUrlHelper(viewContext))
.Returns(urlHelper.Object);
var tagHelper = new FormActionTagHelper(urlHelperFactory.Object)
{
Route = "Default",
RouteValues = routeValues,
ViewContext = viewContext,
};
// Act
await tagHelper.ProcessAsync(context, output);
// Assert
Assert.Equal("button", output.TagName);
var attribute = Assert.Single(output.Attributes);
Assert.Equal("formaction", attribute.Name);
Assert.Equal("home/index", attribute.Value);
Assert.Empty(output.Content.GetContent());
}
// Area property value, RouteValues property value, expected "area" in final RouteValuesDictionary.
public static TheoryData<string, Dictionary<string, string>, string> AreaRouteValuesData
{
get
{
return new TheoryData<string, Dictionary<string, string>, string>
{
{ "Area", null, "Area" },
// Explicit Area overrides value in the dictionary.
{ "Area", new Dictionary<string, string> { { "area", "Home" } }, "Area" },
// Empty string is also passed through to the helper.
{ string.Empty, null, string.Empty },
{ string.Empty, new Dictionary<string, string> { { "area", "Home" } }, string.Empty },
// Fall back "area" entry in the provided route values if Area is null.
{ null, new Dictionary<string, string> { { "area", "Admin" } }, "Admin" },
};
}
}
[Theory]
[MemberData(nameof(AreaRouteValuesData))]
public async Task ProcessAsync_CallsActionWithExpectedRouteValues(
string area,
Dictionary<string, string> routeValues,
string expectedArea)
{
// Arrange
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(),
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
"submit",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
return Task.FromResult<TagHelperContent>(new DefaultTagHelperContent());
});
var expectedRouteValues = new Dictionary<string, object> { { "area", expectedArea } };
var urlHelper = new Mock<IUrlHelper>(MockBehavior.Strict);
urlHelper
.Setup(mock => mock.Action(It.IsAny<UrlActionContext>()))
.Callback<UrlActionContext>(param => Assert.Equal(expectedRouteValues, param.Values as RouteValueDictionary))
.Returns("admin/dashboard/index");
var viewContext = new ViewContext();
var urlHelperFactory = new Mock<IUrlHelperFactory>(MockBehavior.Strict);
urlHelperFactory
.Setup(f => f.GetUrlHelper(viewContext))
.Returns(urlHelper.Object);
var tagHelper = new FormActionTagHelper(urlHelperFactory.Object)
{
Action = "Index",
Area = area,
Controller = "Dashboard",
RouteValues = routeValues,
ViewContext = viewContext,
};
// Act
await tagHelper.ProcessAsync(context, output);
// Assert
Assert.Equal("submit", output.TagName);
var attribute = Assert.Single(output.Attributes);
Assert.Equal("formaction", attribute.Name);
Assert.Equal("admin/dashboard/index", attribute.Value);
Assert.Empty(output.Content.GetContent());
}
[Theory]
[MemberData(nameof(AreaRouteValuesData))]
public async Task ProcessAsync_CallsRouteUrlWithExpectedRouteValues(
string area,
Dictionary<string, string> routeValues,
string expectedArea)
{
// Arrange
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(),
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
"submit",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
return Task.FromResult<TagHelperContent>(new DefaultTagHelperContent());
});
var expectedRouteValues = new Dictionary<string, object> { { "area", expectedArea } };
var urlHelper = new Mock<IUrlHelper>(MockBehavior.Strict);
urlHelper
.Setup(mock => mock.RouteUrl(It.IsAny<UrlRouteContext>()))
.Callback<UrlRouteContext>(param => Assert.Equal(expectedRouteValues, param.Values as RouteValueDictionary))
.Returns("admin/dashboard/index");
var viewContext = new ViewContext();
var urlHelperFactory = new Mock<IUrlHelperFactory>(MockBehavior.Strict);
urlHelperFactory
.Setup(f => f.GetUrlHelper(viewContext))
.Returns(urlHelper.Object);
var tagHelper = new FormActionTagHelper(urlHelperFactory.Object)
{
Area = area,
Route = "routine",
RouteValues = routeValues,
ViewContext = viewContext,
};
// Act
await tagHelper.ProcessAsync(context, output);
// Assert
Assert.Equal("submit", output.TagName);
var attribute = Assert.Single(output.Attributes);
Assert.Equal("formaction", attribute.Name);
Assert.Equal("admin/dashboard/index", attribute.Value);
Assert.Empty(output.Content.GetContent());
}
[Theory]
[InlineData("button", "Action")]
[InlineData("button", "Controller")]
[InlineData("button", "Route")]
[InlineData("button", "asp-route-")]
[InlineData("submit", "Action")]
[InlineData("submit", "Controller")]
[InlineData("submit", "Route")]
[InlineData("submit", "asp-route-")]
public async Task ProcessAsync_ThrowsIfFormActionConflictsWithBoundAttributes(string tagName, string propertyName)
{
// Arrange
var urlHelperFactory = new Mock<IUrlHelperFactory>().Object;
var tagHelper = new FormActionTagHelper(urlHelperFactory);
var output = new TagHelperOutput(
tagName,
attributes: new TagHelperAttributeList
{
{ "formaction", "my-action" }
},
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
if (propertyName == "asp-route-")
{
tagHelper.RouteValues.Add("name", "value");
}
else
{
typeof(FormActionTagHelper).GetProperty(propertyName).SetValue(tagHelper, "Home");
}
var expectedErrorMessage = $"Cannot override the 'formaction' attribute for <{tagName}>. <{tagName}> " +
"elements with a specified 'formaction' must not have attributes starting with 'asp-route-' or an " +
"'asp-action', 'asp-controller', 'asp-area', or 'asp-route' attribute.";
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(
Enumerable.Empty<TagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => tagHelper.ProcessAsync(context, output));
Assert.Equal(expectedErrorMessage, ex.Message);
}
[Theory]
[InlineData("button", "Action")]
[InlineData("button", "Controller")]
[InlineData("submit", "Action")]
[InlineData("submit", "Controller")]
public async Task ProcessAsync_ThrowsIfRouteAndActionOrControllerProvided(string tagName, string propertyName)
{
// Arrange
var urlHelperFactory = new Mock<IUrlHelperFactory>().Object;
var tagHelper = new FormActionTagHelper(urlHelperFactory)
{
Route = "Default",
};
typeof(FormActionTagHelper).GetProperty(propertyName).SetValue(tagHelper, "Home");
var output = new TagHelperOutput(
tagName,
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
var expectedErrorMessage = $"Cannot determine a 'formaction' attribute for <{tagName}>. <{tagName}> " +
"elements with a specified 'asp-route' must not have an 'asp-action' or 'asp-controller' attribute.";
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(
Enumerable.Empty<TagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => tagHelper.ProcessAsync(context, output));
Assert.Equal(expectedErrorMessage, ex.Message);
}
}
}

View File

@ -1,385 +0,0 @@
// 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.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Routing;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
public class SubmitTagHelperTest
{
[Fact]
public async Task ProcessAsync_GeneratesExpectedOutput()
{
// Arrange
var expectedTagName = "not-input";
var metadataProvider = new TestModelMetadataProvider();
var tagHelperContext = new TagHelperContext(
allAttributes: new TagHelperAttributeList
{
{ "id", "myinput" },
{ "type", "submit" },
{ "asp-route-name", "value" },
{ "asp-action", "index" },
{ "asp-controller", "home" },
},
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
expectedTagName,
attributes: new TagHelperAttributeList
{
{ "id", "myinput" },
{ "type", "submit" },
},
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something Else");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
output.Content.SetContent("Something");
var urlHelper = new Mock<IUrlHelper>();
urlHelper
.Setup(mock => mock.Action(It.IsAny<UrlActionContext>())).Returns("home/index").Verifiable();
var urlHelperFactory = new Mock<IUrlHelperFactory>();
urlHelperFactory
.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
.Returns(urlHelper.Object);
var htmlGenerator = new TestableHtmlGenerator(metadataProvider, urlHelper.Object);
var viewContext = TestableHtmlGenerator.GetViewContext(model: null,
htmlGenerator: htmlGenerator,
metadataProvider: metadataProvider);
var submitTagHelper = new SubmitTagHelper(urlHelperFactory.Object)
{
Action = "index",
Controller = "home",
RouteValues =
{
{ "name", "value" },
},
ViewContext = viewContext,
};
// Act
await submitTagHelper.ProcessAsync(tagHelperContext, output);
// Assert
urlHelper.Verify();
Assert.Equal(3, output.Attributes.Count);
var attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("id"));
Assert.Equal("myinput", attribute.Value);
attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("type"));
Assert.Equal("submit", attribute.Value);
attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("formaction"));
Assert.Equal("home/index", attribute.Value);
Assert.Equal("Something", output.Content.GetContent());
Assert.Equal(expectedTagName, output.TagName);
}
[Fact]
public async Task ProcessAsync_CallsIntoRouteLinkWithExpectedParameters()
{
// Arrange
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(
Enumerable.Empty<TagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
"input",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
output.Content.SetContent(string.Empty);
var urlHelper = new Mock<IUrlHelper>();
urlHelper
.Setup(mock => mock.RouteUrl(It.IsAny<UrlRouteContext>())).Returns("home/index").Verifiable();
var urlHelperFactory = new Mock<IUrlHelperFactory>();
urlHelperFactory
.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
.Returns(urlHelper.Object);
var metadataProvider = new TestModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider, urlHelper.Object);
var viewContext = TestableHtmlGenerator.GetViewContext(model: null,
htmlGenerator: htmlGenerator,
metadataProvider: metadataProvider);
var submitTagHelper = new SubmitTagHelper(urlHelperFactory.Object)
{
Route = "Default",
ViewContext = viewContext,
RouteValues =
{
{ "name", "value" },
},
};
// Act
await submitTagHelper.ProcessAsync(context, output);
// Assert
urlHelper.Verify();
Assert.Equal("input", output.TagName);
Assert.Equal(1, output.Attributes.Count);
var attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("formaction"));
Assert.Equal("home/index", attribute.Value);
Assert.True(output.Content.GetContent().Length == 0);
}
[Fact]
public async Task ProcessAsync_AddsAreaToRouteValuesAndCallsIntoActionLinkWithExpectedParameters()
{
// Arrange
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(
Enumerable.Empty<TagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
"input",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
output.Content.SetContent(string.Empty);
var expectedRouteValues = new RouteValueDictionary(new Dictionary<string, string> { { "area", "Admin" } });
var urlHelper = new Mock<IUrlHelper>();
urlHelper
.Setup(mock => mock.Action(It.IsAny<UrlActionContext>()))
.Returns("admin/dashboard/index")
.Callback<UrlActionContext>(param => Assert.Equal(param.Values, expectedRouteValues))
.Verifiable();
var urlHelperFactory = new Mock<IUrlHelperFactory>();
urlHelperFactory
.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
.Returns(urlHelper.Object);
var submitTagHelper = new SubmitTagHelper(urlHelperFactory.Object)
{
Action = "Index",
Controller = "Dashboard",
Area = "Admin",
};
// Act
await submitTagHelper.ProcessAsync(context, output);
// Assert
urlHelper.Verify();
Assert.Equal("input", output.TagName);
Assert.Equal(1, output.Attributes.Count);
var attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("formaction"));
Assert.Equal("admin/dashboard/index", attribute.Value);
Assert.True(output.Content.GetContent().Length == 0);
}
[Fact]
public async Task ProcessAsync_AspAreaOverridesAspRouteArea()
{
// Arrange
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(
Enumerable.Empty<TagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
"input",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
output.Content.SetContent(string.Empty);
var expectedRouteValues = new RouteValueDictionary(new Dictionary<string, string> { { "area", "Admin" } });
var urlHelper = new Mock<IUrlHelper>();
urlHelper
.Setup(mock => mock.Action(It.IsAny<UrlActionContext>()))
.Returns("admin/dashboard/index")
.Callback<UrlActionContext>(param => Assert.Equal(param.Values, expectedRouteValues))
.Verifiable();
var urlHelperFactory = new Mock<IUrlHelperFactory>();
urlHelperFactory
.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
.Returns(urlHelper.Object);
var submitTagHelper = new SubmitTagHelper(urlHelperFactory.Object)
{
Action = "Index",
Controller = "Dashboard",
Area = "Admin",
RouteValues = new Dictionary<string, string> { { "area", "Home" } }
};
// Act
await submitTagHelper.ProcessAsync(context, output);
// Assert
urlHelper.Verify();
Assert.Equal("input", output.TagName);
Assert.Equal(1, output.Attributes.Count);
var attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("formaction"));
Assert.Equal("admin/dashboard/index", attribute.Value);
Assert.True(output.Content.GetContent().Length == 0);
}
[Fact]
public async Task ProcessAsync_EmptyStringOnAspAreaIsPassedThroughToRouteValues()
{
// Arrange
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(
Enumerable.Empty<TagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
"input",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
output.Content.SetContent(string.Empty);
var expectedRouteValues = new RouteValueDictionary(new Dictionary<string, string> { { "area", string.Empty } });
var urlHelper = new Mock<IUrlHelper>();
urlHelper
.Setup(mock => mock.Action(It.IsAny<UrlActionContext>()))
.Returns("admin/dashboard/index")
.Callback<UrlActionContext>(param => Assert.Equal(param.Values, expectedRouteValues))
.Verifiable();
var urlHelperFactory = new Mock<IUrlHelperFactory>();
urlHelperFactory
.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
.Returns(urlHelper.Object);
var submitTagHelper = new SubmitTagHelper(urlHelperFactory.Object)
{
Action = "Index",
Controller = "Dashboard",
Area = string.Empty,
};
// Act
await submitTagHelper.ProcessAsync(context, output);
// Assert
urlHelper.Verify();
Assert.Equal("input", output.TagName);
Assert.Equal(1, output.Attributes.Count);
var attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("formaction"));
Assert.Equal("admin/dashboard/index", attribute.Value);
Assert.True(output.Content.GetContent().Length == 0);
}
[Theory]
[InlineData("Action")]
[InlineData("Controller")]
[InlineData("Route")]
[InlineData("asp-route-")]
public async Task ProcessAsync_ThrowsIfFormActionConflictsWithBoundAttributes(string propertyName)
{
// Arrange
var urlHelperFactory = new Mock<IUrlHelperFactory>().Object;
var submitTagHelper = new SubmitTagHelper(urlHelperFactory);
var output = new TagHelperOutput(
"input",
attributes: new TagHelperAttributeList
{
{ "formaction", "my-action" }
},
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
if (propertyName == "asp-route-")
{
submitTagHelper.RouteValues.Add("name", "value");
}
else
{
typeof(SubmitTagHelper).GetProperty(propertyName).SetValue(submitTagHelper, "Home");
}
var expectedErrorMessage = "Cannot override the 'formaction' attribute for <input>. An <input> with a specified " +
"'formaction' must not have attributes starting with 'asp-route-' or an " +
"'asp-action', 'asp-controller', 'asp-area', or 'asp-route' attribute.";
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(
Enumerable.Empty<TagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => submitTagHelper.ProcessAsync(context, output));
Assert.Equal(expectedErrorMessage, ex.Message);
}
[Theory]
[InlineData("Action")]
[InlineData("Controller")]
public async Task ProcessAsync_ThrowsIfRouteAndActionOrControllerProvided(string propertyName)
{
// Arrange
var urlHelperFactory = new Mock<IUrlHelperFactory>().Object;
var submitTagHelper = new SubmitTagHelper(urlHelperFactory)
{
Route = "Default",
};
typeof(SubmitTagHelper).GetProperty(propertyName).SetValue(submitTagHelper, "Home");
var output = new TagHelperOutput(
"input",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
var expectedErrorMessage = "Cannot determine a 'formaction' attribute for <input>. An <input> with a specified " +
"'asp-route' must not have an 'asp-action' or 'asp-controller' attribute.";
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(
Enumerable.Empty<TagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => submitTagHelper.ProcessAsync(context, output));
Assert.Equal(expectedErrorMessage, ex.Message);
}
}
}