Add AnchorTagHelper.
- Added a TagHelper that targets the <a> tag and allows users to do the equivalent of Html.ActionLink or Html.RouteLink. - Added tests to validate AnchorTagHelper functionality. #1247
This commit is contained in:
parent
4c98c8fcb9
commit
57d1c542db
|
|
@ -0,0 +1,156 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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 Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ITagHelper"/> implementation targeting <a> elements.
|
||||
/// </summary>
|
||||
[TagName("a")]
|
||||
public class AnchorTagHelper : TagHelper
|
||||
{
|
||||
private const string RouteAttributePrefix = "route-";
|
||||
private const string Href = "href";
|
||||
|
||||
[Activate]
|
||||
private IHtmlGenerator Generator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the action method.
|
||||
/// </summary>
|
||||
/// <remarks>Must be <c>null</c> if <see cref="Route"/> is non-<c>null</c>.</remarks>
|
||||
public string Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the controller.
|
||||
/// </summary>
|
||||
/// <remarks>Must be <c>null</c> if <see cref="Route"/> is non-<c>null</c>.</remarks>
|
||||
public string Controller { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The protocol for the URL, such as "http" or "https".
|
||||
/// </summary>
|
||||
public string Protocol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The host name.
|
||||
/// </summary>
|
||||
public string Host { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The URL fragment name.
|
||||
/// </summary>
|
||||
public string Fragment { 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>
|
||||
public string Route { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>Does nothing if user provides an "href" attribute. Throws an
|
||||
/// <see cref="InvalidOperationException"/> if "href" attribute is provided and <see cref="Action"/>,
|
||||
/// <see cref="Controller"/>, or <see cref="Route"/> are non-<c>null</c>.</remarks>
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
var routePrefixedAttributes = output.FindPrefixedAttributes(RouteAttributePrefix);
|
||||
|
||||
// If there's an "href" on the tag it means it's being used as a normal anchor.
|
||||
if (output.Attributes.ContainsKey(Href))
|
||||
{
|
||||
if (Action != null ||
|
||||
Controller != null ||
|
||||
Route != null ||
|
||||
Protocol != null ||
|
||||
Host != null ||
|
||||
Fragment != null ||
|
||||
routePrefixedAttributes.Any())
|
||||
{
|
||||
// User specified an href and one of the bound attributes; can't determine the href attribute.
|
||||
throw new InvalidOperationException(
|
||||
Resources.FormatAnchorTagHelper_CannotOverrideSpecifiedHref(
|
||||
"<a>",
|
||||
nameof(Action).ToLowerInvariant(),
|
||||
nameof(Controller).ToLowerInvariant(),
|
||||
nameof(Route).ToLowerInvariant(),
|
||||
nameof(Protocol).ToLowerInvariant(),
|
||||
nameof(Host).ToLowerInvariant(),
|
||||
nameof(Fragment).ToLowerInvariant(),
|
||||
RouteAttributePrefix,
|
||||
Href));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TagBuilder tagBuilder;
|
||||
var routeValues = GetRouteValues(output, routePrefixedAttributes);
|
||||
|
||||
if (Route == null)
|
||||
{
|
||||
tagBuilder = Generator.GenerateActionLink(linkText: string.Empty,
|
||||
actionName: Action,
|
||||
controllerName: Controller,
|
||||
protocol: Protocol,
|
||||
hostname: Host,
|
||||
fragment: Fragment,
|
||||
routeValues: routeValues,
|
||||
htmlAttributes: null);
|
||||
}
|
||||
else if (Action != null || Controller != null)
|
||||
{
|
||||
// Route and Action or Controller were specified. Can't determine the href attribute.
|
||||
throw new InvalidOperationException(
|
||||
Resources.FormatAnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified(
|
||||
"<a>",
|
||||
nameof(Route).ToLowerInvariant(),
|
||||
nameof(Action).ToLowerInvariant(),
|
||||
nameof(Controller).ToLowerInvariant(),
|
||||
Href));
|
||||
}
|
||||
else
|
||||
{
|
||||
tagBuilder = Generator.GenerateRouteLink(linkText: string.Empty,
|
||||
routeName: Route,
|
||||
protocol: Protocol,
|
||||
hostName: Host,
|
||||
fragment: Fragment,
|
||||
routeValues: routeValues,
|
||||
htmlAttributes: null);
|
||||
}
|
||||
|
||||
if (tagBuilder != null)
|
||||
{
|
||||
output.MergeAttributes(tagBuilder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We will not need this method once https://github.com/aspnet/Razor/issues/89 is completed.
|
||||
private static Dictionary<string, object> GetRouteValues(
|
||||
TagHelperOutput output, IEnumerable<KeyValuePair<string, string>> routePrefixedAttributes)
|
||||
{
|
||||
Dictionary<string, object> routeValues = null;
|
||||
if (routePrefixedAttributes.Any())
|
||||
{
|
||||
// Prefixed values should be treated as bound attributes, remove them from the output.
|
||||
output.RemoveRange(routePrefixedAttributes);
|
||||
|
||||
// Generator.GenerateForm does not accept a Dictionary<string, string> for route values.
|
||||
routeValues = routePrefixedAttributes.ToDictionary(
|
||||
attribute => attribute.Key.Substring(RouteAttributePrefix.Length),
|
||||
attribute => (object)attribute.Value);
|
||||
}
|
||||
|
||||
return routeValues;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,38 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
private static readonly ResourceManager _resourceManager
|
||||
= new ResourceManager("Microsoft.AspNet.Mvc.TagHelpers.Resources", typeof(Resources).GetTypeInfo().Assembly);
|
||||
|
||||
/// <summary>
|
||||
/// Cannot determine an {4} for {0}. An {0} with a specified {1} must not have an {2} or {3} attribute.
|
||||
/// </summary>
|
||||
internal static string AnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified
|
||||
{
|
||||
get { return GetString("AnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cannot determine an {4} for {0}. An {0} with a specified {1} must not have an {2} or {3} attribute.
|
||||
/// </summary>
|
||||
internal static string FormatAnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified(object p0, object p1, object p2, object p3, object p4)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("AnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified"), p0, p1, p2, p3, p4);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cannot determine an {8} 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.
|
||||
/// </summary>
|
||||
internal static string AnchorTagHelper_CannotOverrideSpecifiedHref
|
||||
{
|
||||
get { return GetString("AnchorTagHelper_CannotOverrideSpecifiedHref"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cannot determine an {8} 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.
|
||||
/// </summary>
|
||||
internal static string FormatAnchorTagHelper_CannotOverrideSpecifiedHref(object p0, object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("AnchorTagHelper_CannotOverrideSpecifiedHref"), p0, p1, p2, p3, p4, p5, p6, p7, p8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cannot determine an {1} for {0}. A {0} with a URL-based {1} must not have attributes starting with {3} or a {2} attribute.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -117,6 +117,12 @@
|
|||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="AnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified" xml:space="preserve">
|
||||
<value>Cannot determine an {4} for {0}. An {0} with a specified {1} must not have an {2} or {3} attribute.</value>
|
||||
</data>
|
||||
<data name="AnchorTagHelper_CannotOverrideSpecifiedHref" xml:space="preserve">
|
||||
<value>Cannot determine an {8} 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_CannotDetermineAction" xml:space="preserve">
|
||||
<value>Cannot determine an {1} for {0}. A {0} with a URL-based {1} must not have attributes starting with {3} or a {2} attribute.</value>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,235 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||
using Microsoft.AspNet.Mvc.Razor;
|
||||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||
{
|
||||
public class AnchorTagHelperTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task ProcessAsync_GeneratesExpectedOutput()
|
||||
{
|
||||
// Arrange
|
||||
var metadataProvider = new DataAnnotationsModelMetadataProvider();
|
||||
var anchorTagHelper = new AnchorTagHelper
|
||||
{
|
||||
Action = "index",
|
||||
Controller = "home",
|
||||
Fragment = "hello=world",
|
||||
Host = "contoso.com",
|
||||
Protocol = "http"
|
||||
};
|
||||
|
||||
var tagHelperContext = new TagHelperContext(
|
||||
allAttributes: new Dictionary<string, object>
|
||||
{
|
||||
{ "id", "myanchor" },
|
||||
{ "route-foo", "bar" },
|
||||
{ "action", "index" },
|
||||
{ "controller", "home" },
|
||||
{ "fragment", "hello=world" },
|
||||
{ "host", "contoso.com" },
|
||||
{ "protocol", "http" }
|
||||
});
|
||||
var output = new TagHelperOutput(
|
||||
"a",
|
||||
attributes: new Dictionary<string, string>
|
||||
{
|
||||
{ "id", "myanchor" },
|
||||
{ "route-foo", "bar" },
|
||||
},
|
||||
content: "Something");
|
||||
|
||||
var urlHelper = new Mock<IUrlHelper>();
|
||||
urlHelper
|
||||
.Setup(mock => mock.Action(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<object>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>()))
|
||||
.Returns("home/index");
|
||||
|
||||
var htmlGenerator = new TestableHtmlGenerator(metadataProvider, urlHelper.Object);
|
||||
var viewContext = TestableHtmlGenerator.GetViewContext(model: null,
|
||||
htmlGenerator: htmlGenerator,
|
||||
metadataProvider: metadataProvider);
|
||||
|
||||
var activator = new DefaultTagHelperActivator();
|
||||
activator.Activate(anchorTagHelper, viewContext);
|
||||
|
||||
// Act
|
||||
await anchorTagHelper.ProcessAsync(tagHelperContext, output);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, output.Attributes.Count);
|
||||
var attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("id"));
|
||||
Assert.Equal("myanchor", attribute.Value);
|
||||
attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("href"));
|
||||
Assert.Equal("home/index", attribute.Value);
|
||||
Assert.Equal("Something", output.Content);
|
||||
Assert.Equal("a", output.TagName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_CallsIntoRouteLinkWithExpectedParameters()
|
||||
{
|
||||
// Arrange
|
||||
var anchorTagHelper = new AnchorTagHelper
|
||||
{
|
||||
Route = "Default",
|
||||
Protocol = "http",
|
||||
Host = "contoso.com",
|
||||
Fragment = "hello=world"
|
||||
};
|
||||
var context = new TagHelperContext(
|
||||
allAttributes: new Dictionary<string, object>());
|
||||
var output = new TagHelperOutput(
|
||||
"a",
|
||||
attributes: new Dictionary<string, string>(),
|
||||
content: string.Empty);
|
||||
|
||||
var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
|
||||
generator
|
||||
.Setup(mock => mock.GenerateRouteLink(
|
||||
string.Empty, "Default", "http", "contoso.com", "hello=world", null, null))
|
||||
.Returns(new TagBuilder("a"))
|
||||
.Verifiable();
|
||||
|
||||
SetGenerator(anchorTagHelper, generator.Object);
|
||||
|
||||
// Act & Assert
|
||||
await anchorTagHelper.ProcessAsync(context, output);
|
||||
generator.Verify();
|
||||
Assert.Equal("a", output.TagName);
|
||||
Assert.Empty(output.Attributes);
|
||||
Assert.Empty(output.Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_CallsIntoActionLinkWithExpectedParameters()
|
||||
{
|
||||
// Arrange
|
||||
var anchorTagHelper = new AnchorTagHelper
|
||||
{
|
||||
Action = "Index",
|
||||
Controller = "Home",
|
||||
Protocol = "http",
|
||||
Host = "contoso.com",
|
||||
Fragment = "hello=world"
|
||||
};
|
||||
var context = new TagHelperContext(
|
||||
allAttributes: new Dictionary<string, object>());
|
||||
var output = new TagHelperOutput(
|
||||
"a",
|
||||
attributes: new Dictionary<string, string>(),
|
||||
content: string.Empty);
|
||||
|
||||
var generator = new Mock<IHtmlGenerator>();
|
||||
generator
|
||||
.Setup(mock => mock.GenerateActionLink(
|
||||
string.Empty, "Index", "Home", "http", "contoso.com", "hello=world", null, null))
|
||||
.Returns(new TagBuilder("a"))
|
||||
.Verifiable();
|
||||
|
||||
SetGenerator(anchorTagHelper, generator.Object);
|
||||
|
||||
// Act & Assert
|
||||
await anchorTagHelper.ProcessAsync(context, output);
|
||||
generator.Verify();
|
||||
Assert.Equal("a", output.TagName);
|
||||
Assert.Empty(output.Attributes);
|
||||
Assert.Empty(output.Content);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Action")]
|
||||
[InlineData("Controller")]
|
||||
[InlineData("Route")]
|
||||
[InlineData("Protocol")]
|
||||
[InlineData("Host")]
|
||||
[InlineData("Fragment")]
|
||||
[InlineData("route-")]
|
||||
public async Task ProcessAsync_ThrowsIfHrefConflictsWithBoundAttributes(string propertyName)
|
||||
{
|
||||
// Arrange
|
||||
var anchorTagHelper = new AnchorTagHelper();
|
||||
var output = new TagHelperOutput(
|
||||
"a",
|
||||
attributes: new Dictionary<string, string>()
|
||||
{
|
||||
{ "href", "http://www.contoso.com" }
|
||||
},
|
||||
content: string.Empty);
|
||||
|
||||
if (propertyName == "route-")
|
||||
{
|
||||
output.Attributes.Add("route-foo", "bar");
|
||||
}
|
||||
else
|
||||
{
|
||||
typeof(AnchorTagHelper).GetProperty(propertyName).SetValue(anchorTagHelper, "Home");
|
||||
}
|
||||
|
||||
var expectedErrorMessage = "Cannot determine an href for <a>. An <a> with a specified href must not " +
|
||||
"have attributes starting with route- or an action, controller, route, " +
|
||||
"protocol, host or fragment attribute.";
|
||||
|
||||
// Act & Assert
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
async () =>
|
||||
{
|
||||
await anchorTagHelper.ProcessAsync(context: null, output: output);
|
||||
});
|
||||
|
||||
Assert.Equal(expectedErrorMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Action")]
|
||||
[InlineData("Controller")]
|
||||
public async Task ProcessAsync_ThrowsIfRouteAndActionOrControllerProvided(string propertyName)
|
||||
{
|
||||
// Arrange
|
||||
var anchorTagHelper = new AnchorTagHelper
|
||||
{
|
||||
Route = "Default"
|
||||
};
|
||||
typeof(AnchorTagHelper).GetProperty(propertyName).SetValue(anchorTagHelper, "Home");
|
||||
var output = new TagHelperOutput(
|
||||
"a",
|
||||
attributes: new Dictionary<string, string>(),
|
||||
content: string.Empty);
|
||||
var expectedErrorMessage = "Cannot determine an href for <a>. An <a> with a " +
|
||||
"specified route must not have an action or controller attribute.";
|
||||
|
||||
// Act & Assert
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
async () =>
|
||||
{
|
||||
await anchorTagHelper.ProcessAsync(context: null, output: output);
|
||||
});
|
||||
|
||||
Assert.Equal(expectedErrorMessage, ex.Message);
|
||||
}
|
||||
|
||||
private void SetGenerator(ITagHelper tagHelper, IHtmlGenerator generator)
|
||||
{
|
||||
var tagHelperType = tagHelper.GetType();
|
||||
|
||||
tagHelperType.GetProperty("Generator", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
.SetValue(tagHelper, generator);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue