[Fixes #4013] Added support for areas in 'a' and 'form' tag helpers

This commit is contained in:
jacalvar 2016-02-12 12:47:58 -08:00
parent 65858b8d8b
commit ac23e5aec6
6 changed files with 396 additions and 13 deletions

View File

@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
/// </summary>
[HtmlTargetElement("a", Attributes = ActionAttributeName)]
[HtmlTargetElement("a", Attributes = ControllerAttributeName)]
[HtmlTargetElement("a", Attributes = AreaAttributeName)]
[HtmlTargetElement("a", Attributes = FragmentAttributeName)]
[HtmlTargetElement("a", Attributes = HostAttributeName)]
[HtmlTargetElement("a", Attributes = ProtocolAttributeName)]
@ -24,6 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
private const string ActionAttributeName = "asp-action";
private const string ControllerAttributeName = "asp-controller";
private const string AreaAttributeName = "asp-area";
private const string FragmentAttributeName = "asp-fragment";
private const string HostAttributeName = "asp-host";
private const string ProtocolAttributeName = "asp-protocol";
@ -67,6 +69,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
[HtmlAttributeName(ControllerAttributeName)]
public string Controller { get; set; }
/// <summary>
/// The name of the area.
/// </summary>
/// <remarks>Must be <c>null</c> if <see cref="Route"/> is non-<c>null</c>.</remarks>
[HtmlAttributeName(AreaAttributeName)]
public string Area { get; set; }
/// <summary>
/// The protocol for the URL, such as &quot;http&quot; or &quot;https&quot;.
/// </summary>
@ -147,6 +156,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
if (Action != null ||
Controller != null ||
Area != null ||
Route != null ||
Protocol != null ||
Host != null ||
@ -159,6 +169,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
"<a>",
ActionAttributeName,
ControllerAttributeName,
AreaAttributeName,
RouteAttributeName,
ProtocolAttributeName,
HostAttributeName,
@ -180,6 +191,17 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
}
}
if (Area != null)
{
if (routeValues == null)
{
routeValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
// Unconditionally replace any value from asp-route-area.
routeValues["area"] = Area;
}
TagBuilder tagBuilder;
if (Route == null)
{

View File

@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
/// </summary>
[HtmlTargetElement("form", Attributes = ActionAttributeName)]
[HtmlTargetElement("form", Attributes = AntiforgeryAttributeName)]
[HtmlTargetElement("form", Attributes = AreaAttributeName)]
[HtmlTargetElement("form", Attributes = ControllerAttributeName)]
[HtmlTargetElement("form", Attributes = RouteAttributeName)]
[HtmlTargetElement("form", Attributes = RouteValuesDictionaryName)]
@ -23,6 +24,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
private const string ActionAttributeName = "asp-action";
private const string AntiforgeryAttributeName = "asp-antiforgery";
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";
@ -66,6 +68,12 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
[HtmlAttributeName(ControllerAttributeName)]
public string Controller { get; set; }
/// <summary>
/// The name of the area.
/// </summary>
[HtmlAttributeName(AreaAttributeName)]
public string Area { get; set; }
/// <summary>
/// Whether the antiforgery token should be generated.
/// </summary>
@ -141,7 +149,7 @@ 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 || Route != null || RouteValues.Count != 0)
if (Action != null || Controller != null || Area != null || Route != null || RouteValues.Count != 0)
{
// User also specified bound attributes we cannot use.
throw new InvalidOperationException(
@ -150,6 +158,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
HtmlActionAttributeName,
ActionAttributeName,
ControllerAttributeName,
AreaAttributeName,
RouteAttributeName,
RouteValuesPrefix));
}
@ -171,6 +180,17 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
}
}
if (Area != null)
{
if (routeValues == null)
{
routeValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
// Unconditionally replace any value from asp-route-area.
routeValues["area"] = Area;
}
TagBuilder tagBuilder;
if (Route == null)
{

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
}
/// <summary>
/// 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.
/// Cannot override the '{9}' attribute for {0}. An {0} with a specified '{9}' must not have attributes starting with '{8}' or an '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', or '{7}' attribute.
/// </summary>
internal static string AnchorTagHelper_CannotOverrideHref
{
@ -35,15 +35,15 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
}
/// <summary>
/// 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.
/// Cannot override the '{9}' attribute for {0}. An {0} with a specified '{9}' must not have attributes starting with '{8}' or an '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', or '{7}' attribute.
/// </summary>
internal static string FormatAnchorTagHelper_CannotOverrideHref(object p0, object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8)
internal static string FormatAnchorTagHelper_CannotOverrideHref(object p0, object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9)
{
return string.Format(CultureInfo.CurrentCulture, GetString("AnchorTagHelper_CannotOverrideHref"), p0, p1, p2, p3, p4, p5, p6, p7, p8);
return string.Format(CultureInfo.CurrentCulture, GetString("AnchorTagHelper_CannotOverrideHref"), p0, p1, p2, p3, p4, p5, p6, p7, p8, p9);
}
/// <summary>
/// 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.
/// 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>
internal static string FormTagHelper_CannotOverrideAction
{
@ -51,11 +51,11 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
}
/// <summary>
/// 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.
/// 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>
internal static string FormatFormTagHelper_CannotOverrideAction(object p0, object p1, object p2, object p3, object p4, object p5)
internal static string FormatFormTagHelper_CannotOverrideAction(object p0, object p1, object p2, object p3, object p4, object p5, object p6)
{
return string.Format(CultureInfo.CurrentCulture, GetString("FormTagHelper_CannotOverrideAction"), p0, p1, p2, p3, p4, p5);
return string.Format(CultureInfo.CurrentCulture, GetString("FormTagHelper_CannotOverrideAction"), p0, p1, p2, p3, p4, p5, p6);
}
/// <summary>

View File

@ -121,10 +121,10 @@
<value>Cannot determine an '{4}' attribute for {0}. An {0} with a specified '{1}' must not have an '{2}' or '{3}' attribute.</value>
</data>
<data name="AnchorTagHelper_CannotOverrideHref" xml:space="preserve">
<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 '{9}' attribute for {0}. An {0} with a specified '{9}' must not have attributes starting with '{8}' or an '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', or '{7}' 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 '{5}' or an '{2}' or '{3}' or '{4}' attribute.</value>
<value>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.</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>

View File

@ -187,6 +187,178 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Assert.True(output.Content.IsEmpty);
}
[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(
"a",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
output.Content.SetContent(string.Empty);
var generator = new Mock<IHtmlGenerator>();
var expectedRouteValues = new Dictionary<string, object> { { "area", "Admin" } };
generator
.Setup(mock => mock.GenerateActionLink(
It.IsAny<ViewContext>(),
string.Empty,
"Index",
"Home",
"http",
"contoso.com",
"hello=world",
expectedRouteValues,
null))
.Returns(new TagBuilder("a"))
.Verifiable();
var anchorTagHelper = new AnchorTagHelper(generator.Object)
{
Action = "Index",
Controller = "Home",
Area = "Admin",
Fragment = "hello=world",
Host = "contoso.com",
Protocol = "http",
};
// Act
await anchorTagHelper.ProcessAsync(context, output);
// Assert
generator.Verify();
Assert.Equal("a", output.TagName);
Assert.Empty(output.Attributes);
Assert.True(output.Content.IsEmpty);
}
[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(
"a",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
output.Content.SetContent(string.Empty);
var generator = new Mock<IHtmlGenerator>();
var expectedRouteValues = new Dictionary<string, object> { { "area", "Admin" } };
generator
.Setup(mock => mock.GenerateActionLink(
It.IsAny<ViewContext>(),
string.Empty,
"Index",
"Home",
"http",
"contoso.com",
"hello=world",
expectedRouteValues,
null))
.Returns(new TagBuilder("a"))
.Verifiable();
var anchorTagHelper = new AnchorTagHelper(generator.Object)
{
Action = "Index",
Controller = "Home",
Area = "Admin",
Fragment = "hello=world",
Host = "contoso.com",
Protocol = "http",
RouteValues = new Dictionary<string, string> { { "area", "Home" } }
};
// Act
await anchorTagHelper.ProcessAsync(context, output);
// Assert
generator.Verify();
Assert.Equal("a", output.TagName);
Assert.Empty(output.Attributes);
Assert.True(output.Content.IsEmpty);
}
[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(
"a",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
output.Content.SetContent(string.Empty);
var generator = new Mock<IHtmlGenerator>();
var expectedRouteValues = new Dictionary<string, object> { { "area", string.Empty } };
generator
.Setup(mock => mock.GenerateActionLink(
It.IsAny<ViewContext>(),
string.Empty,
"Index",
"Home",
"http",
"contoso.com",
"hello=world",
expectedRouteValues,
null))
.Returns(new TagBuilder("a"))
.Verifiable();
var anchorTagHelper = new AnchorTagHelper(generator.Object)
{
Action = "Index",
Controller = "Home",
Area = string.Empty,
Fragment = "hello=world",
Host = "contoso.com",
Protocol = "http"
};
// Act
await anchorTagHelper.ProcessAsync(context, output);
// Assert
generator.Verify();
Assert.Equal("a", output.TagName);
Assert.Empty(output.Attributes);
Assert.True(output.Content.IsEmpty);
}
[Theory]
[InlineData("Action")]
[InlineData("Controller")]
@ -221,7 +393,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
var expectedErrorMessage = "Cannot override the 'href' attribute for <a>. An <a> with a specified " +
"'href' must not have attributes starting with 'asp-route-' or an " +
"'asp-action', 'asp-controller', 'asp-route', 'asp-protocol', 'asp-host', or " +
"'asp-action', 'asp-controller', 'asp-area', 'asp-route', 'asp-protocol', 'asp-host', or " +
"'asp-fragment' attribute.";
var context = new TagHelperContext(

View File

@ -283,6 +283,175 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Assert.Empty(output.PostContent.GetContent());
}
[Fact]
public async Task ProcessAsync_AspAreaAddsAreaToRouteValues()
{
// Arrange
var viewContext = CreateViewContext();
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(
Enumerable.Empty<TagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
"form",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
var expectedRouteValues = new Dictionary<string, object> { { "area", "Admin" } };
var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
generator
.Setup(mock => mock.GenerateForm(
viewContext,
"Index",
"Home",
expectedRouteValues,
null,
null))
.Returns(new TagBuilder("form"))
.Verifiable();
var formTagHelper = new FormTagHelper(generator.Object)
{
Action = "Index",
Antiforgery = false,
Controller = "Home",
Area = "Admin",
ViewContext = viewContext,
};
// Act
await formTagHelper.ProcessAsync(context, output);
// Assert
generator.Verify();
Assert.Equal("form", output.TagName);
Assert.Equal(TagMode.StartTagAndEndTag, output.TagMode);
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_EmptyStringOnAspAreaIsPassedThroughToRouteValues()
{
// Arrange
var viewContext = CreateViewContext();
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(
Enumerable.Empty<TagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
"form",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
var expectedRouteValues = new Dictionary<string, object> { { "area", string.Empty } };
var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
generator
.Setup(mock => mock.GenerateForm(
viewContext,
"Index",
"Home",
expectedRouteValues,
null,
null))
.Returns(new TagBuilder("form"))
.Verifiable();
var formTagHelper = new FormTagHelper(generator.Object)
{
Action = "Index",
Antiforgery = false,
Controller = "Home",
Area = string.Empty,
ViewContext = viewContext,
};
// Act
await formTagHelper.ProcessAsync(context, output);
// Assert
generator.Verify();
Assert.Equal("form", output.TagName);
Assert.Equal(TagMode.StartTagAndEndTag, output.TagMode);
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_AspAreaOverridesAspRouteArea()
{
// Arrange
var viewContext = CreateViewContext();
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(
Enumerable.Empty<TagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
"form",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
var expectedRouteValues = new Dictionary<string, object> { { "area", "Admin" } };
var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
generator
.Setup(mock => mock.GenerateForm(
viewContext,
"Index",
"Home",
expectedRouteValues,
null,
null))
.Returns(new TagBuilder("form"))
.Verifiable();
var formTagHelper = new FormTagHelper(generator.Object)
{
Action = "Index",
Antiforgery = false,
Controller = "Home",
Area = "Admin",
RouteValues = new Dictionary<string, string> { { "area", "Client" } },
ViewContext = viewContext,
};
// Act
await formTagHelper.ProcessAsync(context, output);
// Assert
generator.Verify();
Assert.Equal("form", output.TagName);
Assert.Equal(TagMode.StartTagAndEndTag, output.TagMode);
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()
{
@ -415,7 +584,7 @@ namespace Microsoft.AspNetCore.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' or 'asp-route' attribute.";
"'asp-action' or 'asp-controller' or 'asp-area' or 'asp-route' attribute.";
var context = new TagHelperContext(
allAttributes: new TagHelperAttributeList(