[Fixes #1489] Support for named route in form tag helper
This commit is contained in:
parent
37e819ce85
commit
a80a333fea
|
|
@ -17,6 +17,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
private const string ActionAttributeName = "asp-action";
|
private const string ActionAttributeName = "asp-action";
|
||||||
private const string AntiForgeryAttributeName = "asp-anti-forgery";
|
private const string AntiForgeryAttributeName = "asp-anti-forgery";
|
||||||
private const string ControllerAttributeName = "asp-controller";
|
private const string ControllerAttributeName = "asp-controller";
|
||||||
|
private const string RouteAttributeName = "asp-route";
|
||||||
private const string RouteAttributePrefix = "asp-route-";
|
private const string RouteAttributePrefix = "asp-route-";
|
||||||
private const string HtmlActionAttributeName = "action";
|
private const string HtmlActionAttributeName = "action";
|
||||||
|
|
||||||
|
|
@ -47,6 +48,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
[HtmlAttributeName(AntiForgeryAttributeName)]
|
[HtmlAttributeName(AntiForgeryAttributeName)]
|
||||||
public bool? AntiForgery { get; set; }
|
public bool? AntiForgery { 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; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Does nothing if user provides an <c>action</c> attribute and <see cref="AntiForgery"/> is <c>null</c> or
|
/// Does nothing if user provides an <c>action</c> attribute and <see cref="AntiForgery"/> is <c>null</c> or
|
||||||
|
|
@ -64,7 +74,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
// If "action" is already set, it means the user is attempting to use a normal <form>.
|
// If "action" is already set, it means the user is attempting to use a normal <form>.
|
||||||
if (output.Attributes.ContainsKey(HtmlActionAttributeName))
|
if (output.Attributes.ContainsKey(HtmlActionAttributeName))
|
||||||
{
|
{
|
||||||
if (Action != null || Controller != null || routePrefixedAttributes.Any())
|
if (Action != null || Controller != null || Route != null || routePrefixedAttributes.Any())
|
||||||
{
|
{
|
||||||
// User also specified bound attributes we cannot use.
|
// User also specified bound attributes we cannot use.
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
|
|
@ -73,6 +83,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
HtmlActionAttributeName,
|
HtmlActionAttributeName,
|
||||||
ActionAttributeName,
|
ActionAttributeName,
|
||||||
ControllerAttributeName,
|
ControllerAttributeName,
|
||||||
|
RouteAttributeName,
|
||||||
RouteAttributePrefix));
|
RouteAttributePrefix));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,13 +93,38 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
TagBuilder tagBuilder;
|
||||||
var routeValues = GetRouteValues(output, routePrefixedAttributes);
|
var routeValues = GetRouteValues(output, routePrefixedAttributes);
|
||||||
var tagBuilder = Generator.GenerateForm(ViewContext,
|
if (Route == null)
|
||||||
Action,
|
{
|
||||||
Controller,
|
tagBuilder = Generator.GenerateForm(
|
||||||
routeValues,
|
ViewContext,
|
||||||
method: null,
|
Action,
|
||||||
htmlAttributes: null);
|
Controller,
|
||||||
|
routeValues,
|
||||||
|
method: null,
|
||||||
|
htmlAttributes: null);
|
||||||
|
}
|
||||||
|
else if (Action != null || Controller != null)
|
||||||
|
{
|
||||||
|
// Route and Action or Controller were specified. Can't determine the action attribute.
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
Resources.FormatFormTagHelper_CannotDetermineActionWithRouteAndActionOrControllerSpecified(
|
||||||
|
"<form>",
|
||||||
|
RouteAttributeName,
|
||||||
|
ActionAttributeName,
|
||||||
|
ControllerAttributeName,
|
||||||
|
HtmlActionAttributeName));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tagBuilder = Generator.GenerateRouteForm(
|
||||||
|
ViewContext,
|
||||||
|
Route,
|
||||||
|
routeValues,
|
||||||
|
method: null,
|
||||||
|
htmlAttributes: null);
|
||||||
|
}
|
||||||
|
|
||||||
if (tagBuilder != null)
|
if (tagBuilder != null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cannot override the '{1}' attribute for {0}. A {0} with a specified '{1}' must not have attributes starting with '{4}' or an '{2}' or '{3}' attribute.
|
/// Cannot override the '{1}' attribute for {0}. A {0} with a specified '{1}' must not have attributes starting with '{5}' or an '{2}' or '{3}' or '{4}' attribute.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static string FormTagHelper_CannotOverrideAction
|
internal static string FormTagHelper_CannotOverrideAction
|
||||||
{
|
{
|
||||||
|
|
@ -51,11 +51,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cannot override the '{1}' attribute for {0}. A {0} with a specified '{1}' must not have attributes starting with '{4}' or an '{2}' or '{3}' attribute.
|
/// Cannot override the '{1}' attribute for {0}. A {0} with a specified '{1}' must not have attributes starting with '{5}' or an '{2}' or '{3}' or '{4}' attribute.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static string FormatFormTagHelper_CannotOverrideAction(object p0, object p1, object p2, object p3, object p4)
|
internal static string FormatFormTagHelper_CannotOverrideAction(object p0, object p1, object p2, object p3, object p4, object p5)
|
||||||
{
|
{
|
||||||
return string.Format(CultureInfo.CurrentCulture, GetString("FormTagHelper_CannotOverrideAction"), p0, p1, p2, p3, p4);
|
return string.Format(CultureInfo.CurrentCulture, GetString("FormTagHelper_CannotOverrideAction"), p0, p1, p2, p3, p4, p5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -138,6 +138,22 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperOutput_AttributeDoesNotExist"), p0, p1);
|
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperOutput_AttributeDoesNotExist"), p0, p1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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 FormTagHelper_CannotDetermineActionWithRouteAndActionOrControllerSpecified
|
||||||
|
{
|
||||||
|
get { return GetString("FormTagHelper_CannotDetermineActionWithRouteAndActionOrControllerSpecified"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 FormatFormTagHelper_CannotDetermineActionWithRouteAndActionOrControllerSpecified(object p0, object p1, object p2, object p3, object p4)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("FormTagHelper_CannotDetermineActionWithRouteAndActionOrControllerSpecified"), p0, p1, p2, p3, p4);
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetString(string name, params string[] formatterNames)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
var value = _resourceManager.GetString(name);
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<root>
|
<root>
|
||||||
<!--
|
<!--
|
||||||
Microsoft ResX Schema
|
Microsoft ResX Schema
|
||||||
|
|
||||||
Version 2.0
|
Version 2.0
|
||||||
|
|
||||||
The primary goals of this format is to allow a simple XML format
|
The primary goals of this format is to allow a simple XML format
|
||||||
that is mostly human readable. The generation and parsing of the
|
that is mostly human readable. The generation and parsing of the
|
||||||
various data types are done through the TypeConverter classes
|
various data types are done through the TypeConverter classes
|
||||||
associated with the data types.
|
associated with the data types.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
... ado.net/XML headers & schema ...
|
... ado.net/XML headers & schema ...
|
||||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
<resheader name="version">2.0</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>
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
<comment>This is a comment</comment>
|
<comment>This is a comment</comment>
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
There are any number of "resheader" rows that contain simple
|
There are any number of "resheader" rows that contain simple
|
||||||
name/value pairs.
|
name/value pairs.
|
||||||
|
|
||||||
Each data row contains a name, and value. The row also contains a
|
Each data row contains a name, and value. The row also contains a
|
||||||
type or mimetype. Type corresponds to a .NET class that support
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
text/value conversion through the TypeConverter architecture.
|
text/value conversion through the TypeConverter architecture.
|
||||||
Classes that don't support this are serialized and stored with the
|
Classes that don't support this are serialized and stored with the
|
||||||
mimetype set.
|
mimetype set.
|
||||||
|
|
||||||
The mimetype is used for serialized objects, and tells the
|
The mimetype is used for serialized objects, and tells the
|
||||||
ResXResourceReader how to depersist the object. This is currently not
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
extensible. For a given mimetype the value must be set accordingly:
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
that the ResXResourceWriter will generate, however the reader can
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
read any of the formats listed below.
|
read any of the formats listed below.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.binary.base64
|
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
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.soap.base64
|
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
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
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
|
: using a System.ComponentModel.TypeConverter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
-->
|
-->
|
||||||
|
|
@ -124,7 +124,7 @@
|
||||||
<value>Cannot override the '{8}' attribute for {0}. An {0} with a specified '{8}' must not have attributes starting with '{7}' or an '{1}', '{2}', '{3}', '{4}', '{5}', or '{6}' attribute.</value>
|
<value>Cannot override the '{8}' attribute for {0}. An {0} with a specified '{8}' must not have attributes starting with '{7}' or an '{1}', '{2}', '{3}', '{4}', '{5}', or '{6}' attribute.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FormTagHelper_CannotOverrideAction" xml:space="preserve">
|
<data name="FormTagHelper_CannotOverrideAction" xml:space="preserve">
|
||||||
<value>Cannot override the '{1}' attribute for {0}. A {0} with a specified '{1}' must not have attributes starting with '{4}' or an '{2}' or '{3}' attribute.</value>
|
<value>Cannot override the '{1}' attribute for {0}. A {0} with a specified '{1}' must not have attributes starting with '{5}' or an '{2}' or '{3}' or '{4}' attribute.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="InputTagHelper_InvalidExpressionResult" xml:space="preserve">
|
<data name="InputTagHelper_InvalidExpressionResult" xml:space="preserve">
|
||||||
<value>Unexpected '{1}' expression result type '{2}' for {0}. '{1}' must be of type '{3}' if '{4}' is '{5}'.</value>
|
<value>Unexpected '{1}' expression result type '{2}' for {0}. '{1}' must be of type '{3}' if '{4}' is '{5}'.</value>
|
||||||
|
|
@ -141,4 +141,7 @@
|
||||||
<data name="TagHelperOutput_AttributeDoesNotExist" xml:space="preserve">
|
<data name="TagHelperOutput_AttributeDoesNotExist" xml:space="preserve">
|
||||||
<value>The attribute '{0}' does not exist in the {1}.</value>
|
<value>The attribute '{0}' does not exist in the {1}.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="FormTagHelper_CannotDetermineActionWithRouteAndActionOrControllerSpecified" xml:space="preserve">
|
||||||
|
<value>Cannot determine an '{4}' attribute for {0}. A {0} with a specified '{1}' must not have an '{2}' or '{3}' attribute.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -252,11 +252,65 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
Assert.Equal("form", output.TagName);
|
Assert.Equal("form", output.TagName);
|
||||||
Assert.False(output.SelfClosing);
|
Assert.False(output.SelfClosing);
|
||||||
Assert.Empty(output.Attributes);
|
Assert.Empty(output.Attributes);
|
||||||
|
Assert.Empty(output.PreElement.GetContent());
|
||||||
Assert.Empty(output.PreContent.GetContent());
|
Assert.Empty(output.PreContent.GetContent());
|
||||||
Assert.True(output.Content.IsEmpty);
|
Assert.True(output.Content.IsEmpty);
|
||||||
Assert.Empty(output.PostContent.GetContent());
|
Assert.Empty(output.PostContent.GetContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessAsync_CallsIntoGenerateRouteFormWithExpectedParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var viewContext = CreateViewContext();
|
||||||
|
var context = new TagHelperContext(
|
||||||
|
allAttributes: new Dictionary<string, object>(),
|
||||||
|
items: new Dictionary<object, object>(),
|
||||||
|
uniqueId: "test",
|
||||||
|
getChildContentAsync: () =>
|
||||||
|
{
|
||||||
|
var tagHelperContent = new DefaultTagHelperContent();
|
||||||
|
tagHelperContent.SetContent("Something");
|
||||||
|
return Task.FromResult<TagHelperContent>(tagHelperContent);
|
||||||
|
});
|
||||||
|
var output = new TagHelperOutput(
|
||||||
|
"form",
|
||||||
|
attributes: new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "asp-route-foo", "bar" }
|
||||||
|
});
|
||||||
|
var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
|
||||||
|
generator
|
||||||
|
.Setup(mock => mock.GenerateRouteForm(
|
||||||
|
viewContext,
|
||||||
|
"Default",
|
||||||
|
It.Is<Dictionary<string, object>>(m => string.Equals(m["foo"], "bar")),
|
||||||
|
null,
|
||||||
|
null))
|
||||||
|
.Returns(new TagBuilder("form", new CommonTestEncoder()))
|
||||||
|
.Verifiable();
|
||||||
|
var formTagHelper = new FormTagHelper
|
||||||
|
{
|
||||||
|
AntiForgery = false,
|
||||||
|
Route = "Default",
|
||||||
|
Generator = generator.Object,
|
||||||
|
ViewContext = viewContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await formTagHelper.ProcessAsync(context, output);
|
||||||
|
generator.Verify();
|
||||||
|
|
||||||
|
Assert.Equal("form", output.TagName);
|
||||||
|
Assert.False(output.SelfClosing);
|
||||||
|
Assert.Empty(output.Attributes);
|
||||||
|
Assert.Empty(output.PreElement.GetContent());
|
||||||
|
Assert.Empty(output.PreContent.GetContent());
|
||||||
|
Assert.True(output.Content.IsEmpty);
|
||||||
|
Assert.Empty(output.PostContent.GetContent());
|
||||||
|
Assert.Empty(output.PostElement.GetContent());
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(true, "<input />")]
|
[InlineData(true, "<input />")]
|
||||||
[InlineData(false, "")]
|
[InlineData(false, "")]
|
||||||
|
|
@ -333,7 +387,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
|
|
||||||
var expectedErrorMessage = "Cannot override the 'action' attribute for <form>. A <form> with a specified " +
|
var expectedErrorMessage = "Cannot override the 'action' attribute for <form>. A <form> with a specified " +
|
||||||
"'action' must not have attributes starting with 'asp-route-' or an " +
|
"'action' must not have attributes starting with 'asp-route-' or an " +
|
||||||
"'asp-action' or 'asp-controller' attribute.";
|
"'asp-action' or 'asp-controller' or 'asp-route' attribute.";
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
|
@ -342,6 +396,30 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
Assert.Equal(expectedErrorMessage, ex.Message);
|
Assert.Equal(expectedErrorMessage, ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Action")]
|
||||||
|
[InlineData("Controller")]
|
||||||
|
public async Task ProcessAsync_ThrowsIfRouteAndActionOrControllerProvided(string propertyName)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var formTagHelper = new FormTagHelper
|
||||||
|
{
|
||||||
|
Route = "Default",
|
||||||
|
};
|
||||||
|
typeof(FormTagHelper).GetProperty(propertyName).SetValue(formTagHelper, "Home");
|
||||||
|
var output = new TagHelperOutput(
|
||||||
|
"form",
|
||||||
|
attributes: new Dictionary<string, object>());
|
||||||
|
var expectedErrorMessage = "Cannot determine an 'action' attribute for <form>. A <form> with a specified " +
|
||||||
|
"'asp-route' must not have an 'asp-action' or 'asp-controller' attribute.";
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => formTagHelper.ProcessAsync(context: null, output: output));
|
||||||
|
|
||||||
|
Assert.Equal(expectedErrorMessage, ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
private static ViewContext CreateViewContext()
|
private static ViewContext CreateViewContext()
|
||||||
{
|
{
|
||||||
var actionContext = new ActionContext(
|
var actionContext = new ActionContext(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue