[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 AntiForgeryAttributeName = "asp-anti-forgery";
|
||||
private const string ControllerAttributeName = "asp-controller";
|
||||
private const string RouteAttributeName = "asp-route";
|
||||
private const string RouteAttributePrefix = "asp-route-";
|
||||
private const string HtmlActionAttributeName = "action";
|
||||
|
||||
|
|
@ -47,6 +48,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
[HtmlAttributeName(AntiForgeryAttributeName)]
|
||||
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 />
|
||||
/// <remarks>
|
||||
/// 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 (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.
|
||||
throw new InvalidOperationException(
|
||||
|
|
@ -73,6 +83,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
HtmlActionAttributeName,
|
||||
ActionAttributeName,
|
||||
ControllerAttributeName,
|
||||
RouteAttributeName,
|
||||
RouteAttributePrefix));
|
||||
}
|
||||
|
||||
|
|
@ -82,13 +93,38 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
}
|
||||
else
|
||||
{
|
||||
TagBuilder tagBuilder;
|
||||
var routeValues = GetRouteValues(output, routePrefixedAttributes);
|
||||
var tagBuilder = Generator.GenerateForm(ViewContext,
|
||||
Action,
|
||||
Controller,
|
||||
routeValues,
|
||||
method: null,
|
||||
htmlAttributes: null);
|
||||
if (Route == null)
|
||||
{
|
||||
tagBuilder = Generator.GenerateForm(
|
||||
ViewContext,
|
||||
Action,
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
}
|
||||
|
||||
/// <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>
|
||||
internal static string FormTagHelper_CannotOverrideAction
|
||||
{
|
||||
|
|
@ -51,11 +51,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
}
|
||||
|
||||
/// <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>
|
||||
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>
|
||||
|
|
@ -138,6 +138,22 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
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)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
-->
|
||||
|
|
@ -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>
|
||||
</data>
|
||||
<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 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>
|
||||
|
|
@ -141,4 +141,7 @@
|
|||
<data name="TagHelperOutput_AttributeDoesNotExist" xml:space="preserve">
|
||||
<value>The attribute '{0}' does not exist in the {1}.</value>
|
||||
</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>
|
||||
|
|
@ -252,11 +252,65 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
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());
|
||||
}
|
||||
|
||||
[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]
|
||||
[InlineData(true, "<input />")]
|
||||
[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 " +
|
||||
"'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
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
|
|
@ -342,6 +396,30 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
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()
|
||||
{
|
||||
var actionContext = new ActionContext(
|
||||
|
|
|
|||
Loading…
Reference in New Issue