[Fixes #1489] Support for named route in form tag helper

This commit is contained in:
Ajay Bhargav Baaskaran 2015-04-23 13:16:02 -07:00
parent 37e819ce85
commit a80a333fea
4 changed files with 173 additions and 40 deletions

View File

@ -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)
{ {

View File

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

View File

@ -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>

View File

@ -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(