Adding Support for LinkGeneration for named Routes.

- Adding HtmlHelpers for route link generation.
- Controller Helpers
- UrlHelper and UrlHelperExtensions
This commit is contained in:
harshgMSFT 2014-04-28 18:56:17 -07:00
parent a1f3c72e08
commit 3765abdfca
12 changed files with 481 additions and 39 deletions

View File

@ -19,7 +19,7 @@ namespace MvcSample.Web
public string Get()
{
// Creates a url like: http://localhost:58195/Home/Details#CoolBeans!
return Url.RouteUrl(new { controller = "Home", action = "Details" }, protocol: "http", host: null, fragment: "CoolBeans!");
return Url.RouteUrl("CoolBeansRoute", new { controller = "Home", action = "Details" }, protocol: "http", host: null, fragment: "CoolBeans!");
}
public string Link1()

View File

@ -27,7 +27,7 @@ namespace MvcSample.Web
new { controller = "Home", action = "Index" });
routes.MapRoute(
"ControllerOnlyRoute",
"controllerRoute",
"{controller}",
new { controller = "Home" });
});

View File

@ -7,21 +7,34 @@ namespace Microsoft.AspNet.Mvc
{
public class RedirectToRouteResult : ActionResult
{
public RedirectToRouteResult([NotNull] IUrlHelper urlHelper, IDictionary<string, object> routeValues)
: this(urlHelper, routeValues, permanent: false)
public RedirectToRouteResult([NotNull] IUrlHelper urlHelper,
object routeValues)
: this(urlHelper, routeName: null, routeValues: routeValues)
{
}
public RedirectToRouteResult([NotNull] IUrlHelper urlHelper,
IDictionary<string, object> routeValues, bool permanent)
public RedirectToRouteResult([NotNull] IUrlHelper urlHelper,
string routeName,
object routeValues)
: this(urlHelper, routeName, routeValues, permanent: false)
{
}
public RedirectToRouteResult([NotNull] IUrlHelper urlHelper,
string routeName,
object routeValues,
bool permanent)
{
UrlHelper = urlHelper;
RouteValues = routeValues;
RouteName = routeName;
RouteValues = TypeHelper.ObjectToDictionary(routeValues);
Permanent = permanent;
}
public IUrlHelper UrlHelper { get; private set; }
public string RouteName { get; private set; }
public IDictionary<string, object> RouteValues { get; private set; }
public bool Permanent { get; private set; }

View File

@ -164,14 +164,34 @@ namespace Microsoft.AspNet.Mvc
TypeHelper.ObjectToDictionary(routeValues), permanent: true);
}
public RedirectToRouteResult RedirectToRoute(string routeName)
{
return RedirectToRoute(routeName, routeValues: null);
}
public RedirectToRouteResult RedirectToRoute(object routeValues)
{
return new RedirectToRouteResult(Url, TypeHelper.ObjectToDictionary(routeValues));
return RedirectToRoute(routeName: null, routeValues: routeValues);
}
public RedirectToRouteResult RedirectToRoute(string routeName, object routeValues)
{
return new RedirectToRouteResult(Url, routeName, routeValues);
}
public RedirectToRouteResult RedirectToRoutePermanent(string routeName)
{
return RedirectToRoutePermanent(routeName, routeValues: null);
}
public RedirectToRouteResult RedirectToRoutePermanent(object routeValues)
{
return new RedirectToRouteResult(Url, TypeHelper.ObjectToDictionary(routeValues), permanent: true);
return RedirectToRoutePermanent(routeName: null, routeValues: routeValues);
}
public RedirectToRouteResult RedirectToRoutePermanent(string routeName, object routeValues)
{
return new RedirectToRouteResult(Url, routeName, routeValues, permanent: true);
}
}
}

View File

@ -8,6 +8,6 @@
bool IsLocalUrl(string url);
string RouteUrl(object values, string protocol, string host, string fragment);
string RouteUrl(string routeName, object values, string protocol, string host, string fragment);
}
}

View File

@ -416,6 +416,20 @@ namespace Microsoft.AspNet.Mvc.Rendering
return new HtmlString(value == null ? null : value.ToString());
}
/// <inheritdoc />
public HtmlString RouteLink(
[NotNull] string linkText,
string routeName,
string protocol,
string hostName,
string fragment,
object routeValues,
object htmlAttributes)
{
var url = _urlHelper.RouteUrl(routeName, routeValues, protocol, hostName, fragment);
return GenerateLink(linkText, url, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
/// <inheritdoc />
public HtmlString ValidationMessage(string expression, string message, object htmlAttributes)
{

View File

@ -107,5 +107,104 @@ namespace Microsoft.AspNet.Mvc.Rendering
routeValues: routeValues,
htmlAttributes: htmlAttributes);
}
public static HtmlString RouteLink(
[NotNull] this IHtmlHelper htmlHelper,
[NotNull] string linkText,
object routeValues)
{
return htmlHelper.RouteLink(
linkText,
routeName: null,
protocol: null,
hostName: null,
fragment: null,
routeValues: routeValues,
htmlAttributes: null);
}
public static HtmlString RouteLink(
[NotNull] this IHtmlHelper htmlHelper,
[NotNull] string linkText,
string routeName)
{
return htmlHelper.RouteLink(
linkText,
routeName,
protocol: null,
hostName: null,
fragment: null,
routeValues: null,
htmlAttributes: null);
}
public static HtmlString RouteLink(
[NotNull] this IHtmlHelper htmlHelper,
[NotNull] string linkText,
string routeName,
object routeValues)
{
return htmlHelper.RouteLink(
linkText,
routeName,
protocol: null,
hostName: null,
fragment: null,
routeValues: routeValues,
htmlAttributes: null);
}
public static HtmlString RouteLink(
[NotNull] this IHtmlHelper htmlHelper,
[NotNull] string linkText,
object routeValues,
object htmlAttributes)
{
return htmlHelper.RouteLink(
linkText,
routeName: null,
protocol: null,
hostName: null,
fragment: null,
routeValues: routeValues,
htmlAttributes: htmlAttributes);
}
public static HtmlString RouteLink(
[NotNull] this IHtmlHelper htmlHelper,
[NotNull] string linkText,
string routeName,
object routeValues,
object htmlAttributes)
{
return htmlHelper.RouteLink(
linkText,
routeName,
protocol: null,
hostName: null,
fragment: null,
routeValues: routeValues,
htmlAttributes: htmlAttributes);
}
public static HtmlString RouteLink(
[NotNull] this IHtmlHelper htmlHelper,
[NotNull] string linkText,
string routeName,
string protocol,
string hostName,
string fragment,
object routeValues,
object htmlAttributes)
{
return htmlHelper.RouteLink(
linkText,
routeName,
protocol,
hostName,
fragment,
routeValues,
htmlAttributes);
}
}
}

View File

@ -358,6 +358,35 @@ namespace Microsoft.AspNet.Mvc.Rendering
/// <returns>A task that represents when rendering has completed.</returns>
Task RenderPartialAsync([NotNull] string partialViewName, object model, ViewDataDictionary viewData);
/// <summary>
/// Returns an anchor element (a element) that contains a URL path to the specified route.
/// </summary>
/// <param name="linkText">The inner text of the anchor element.</param>
/// <param name="routeName">The name of the route.</param>
/// <param name="protocol">The protocol for the URL, such as &quot;http&quot; or &quot;https&quot;.</param>
/// <param name="hostName">The host name for the URL.</param>
/// <param name="fragment">The URL fragment name (the anchor name).</param>
/// <param name="routeValues">
/// An object that contains the parameters for a route. The parameters are retrieved through reflection by
/// examining the properties of the object. This object is typically created using object initializer syntax.
/// Alternatively, an <see cref="IDictionary{string, object}"/> instance containing the route parameters.
/// </param>
/// <param name="htmlAttributes">
/// An object that contains the HTML attributes to set for the element. Alternatively, an
/// <see cref="IDictionary{string, object}"/> instance containing the HTML attributes.
/// </param>
/// <returns>
/// An anchor element (a element).
/// </returns>
HtmlString RouteLink(
[NotNull] string linkText,
string routeName,
string protocol,
string hostName,
string fragment,
object routeValues,
object htmlAttributes);
/// <summary>
/// Render a textarea.
/// </summary>
@ -375,7 +404,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
/// </param>
/// <returns>New <see cref="HtmlString"/> containing the rendered HTML.</returns>
HtmlString TextArea(string name, string value, int rows, int columns, object htmlAttributes);
/// <summary>
/// Render an input element of type "text".
/// </summary>

View File

@ -1,12 +1,10 @@
using System;

using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.AspNet.Abstractions;
using Microsoft.AspNet.DependencyInjection;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Routing;
namespace Microsoft.AspNet.Mvc
@ -69,21 +67,20 @@ namespace Microsoft.AspNet.Mvc
public bool IsLocalUrl(string url)
{
return
return
!string.IsNullOrEmpty(url) &&
// Allows "/" or "/foo" but not "//" or "/\".
((url[0] == '/' && (url.Length == 1 || (url[1] != '/' && url[1] != '\\'))) ||
// Allows "~/" or "~/foo".
(url.Length > 1 && url[0] == '~' && url[1] == '/'));
(url.Length > 1 && url[0] == '~' && url[1] == '/'));
}
public string RouteUrl(object values, string protocol, string host, string fragment)
public string RouteUrl(string routeName, object values, string protocol, string host, string fragment)
{
var valuesDictionary = TypeHelper.ObjectToDictionary(values);
var path = GeneratePathFromRoute(valuesDictionary);
var path = GeneratePathFromRoute(routeName, valuesDictionary);
if (path == null)
{
return null;
@ -94,7 +91,12 @@ namespace Microsoft.AspNet.Mvc
private string GeneratePathFromRoute(IDictionary<string, object> values)
{
var context = new VirtualPathContext(_httpContext, _ambientValues, values);
return GeneratePathFromRoute(routeName: null, values: values);
}
private string GeneratePathFromRoute(string routeName, IDictionary<string, object> values)
{
var context = new VirtualPathContext(_httpContext, _ambientValues, values, routeName);
var path = _router.GetVirtualPath(context);
if (path == null)
{
@ -123,7 +125,7 @@ namespace Microsoft.AspNet.Mvc
return GenerateClientUrl(_httpContext.Request.PathBase, contentPath);
}
private static string GenerateClientUrl([NotNull] PathString applicationPath,
private static string GenerateClientUrl([NotNull] PathString applicationPath,
[NotNull] string path)
{
if (path.StartsWith("~/", StringComparison.Ordinal))
@ -132,12 +134,11 @@ namespace Microsoft.AspNet.Mvc
return applicationPath.Add(segment).Value;
}
return path;
}
}
private string GenerateUrl(string protocol, string host, string path, string fragment)
{
// We should have a robust and centrallized version of this code. See HttpAbstractions#28
Contract.Assert(path != null);
var url = path;
@ -167,4 +168,4 @@ namespace Microsoft.AspNet.Mvc
}
}
}
}
}

View File

@ -56,17 +56,31 @@
public static string RouteUrl([NotNull] this IUrlHelper helper, object values)
{
return helper.RouteUrl(values, protocol: null, host: null, fragment: null);
return helper.RouteUrl(routeName: null, values: values, protocol: null, host: null, fragment: null);
}
public static string RouteUrl([NotNull] this IUrlHelper helper, object values, string protocol)
public static string RouteUrl([NotNull] this IUrlHelper helper, string routeName)
{
return helper.RouteUrl(values, protocol, host: null, fragment: null);
return helper.RouteUrl(routeName, values: null, protocol: null, host: null, fragment: null);
}
public static string RouteUrl([NotNull] this IUrlHelper helper, object values, string protocol, string host)
public static string RouteUrl([NotNull] this IUrlHelper helper, string routeName, object values)
{
return helper.RouteUrl(values, protocol, host, fragment: null);
return helper.RouteUrl(routeName, values, protocol: null, host: null, fragment: null);
}
public static string RouteUrl([NotNull] this IUrlHelper helper, string routeName, object values, string protocol)
{
return helper.RouteUrl(routeName, values, protocol, host: null, fragment: null);
}
public static string RouteUrl([NotNull] this IUrlHelper helper,
string routeName,
object values,
string protocol,
string host)
{
return helper.RouteUrl(routeName, values, protocol, host, fragment: null);
}
}
}

View File

@ -26,7 +26,9 @@ namespace Microsoft.AspNet.Mvc.Core
new Dictionary<string, object>(),
new ActionDescriptor());
IUrlHelper urlHelper = GetMockUrlHelper(expectedUrl);
RedirectToRouteResult result = new RedirectToRouteResult(urlHelper, TypeHelper.ObjectToDictionary(values));
RedirectToRouteResult result = new RedirectToRouteResult(urlHelper,
null,
TypeHelper.ObjectToDictionary(values));
// Act
await result.ExecuteResultAsync(actionContext);
@ -50,7 +52,9 @@ namespace Microsoft.AspNet.Mvc.Core
new ActionDescriptor());
IUrlHelper urlHelper = GetMockUrlHelper(returnValue: null);
RedirectToRouteResult result = new RedirectToRouteResult(urlHelper, new Dictionary<string, object>());
RedirectToRouteResult result = new RedirectToRouteResult(urlHelper,
null,
new Dictionary<string, object>());
// Act & Assert
ExceptionAssert.ThrowsAsync<InvalidOperationException>(
@ -81,7 +85,7 @@ namespace Microsoft.AspNet.Mvc.Core
private static IUrlHelper GetMockUrlHelper(string returnValue)
{
var urlHelper = new Mock<IUrlHelper>();
urlHelper.Setup(o => o.RouteUrl(It.IsAny<object>(), It.IsAny<string>(),
urlHelper.Setup(o => o.RouteUrl(It.IsAny<string>(), It.IsAny<object>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<string>())).Returns(returnValue);
return urlHelper.Object;
}

View File

@ -12,8 +12,8 @@ namespace Microsoft.AspNet.Mvc.Core.Test
[Theory]
[InlineData("", "/Home/About", "/Home/About")]
[InlineData("/myapproot", "/test", "/test")]
public void Content_ReturnsContentPath_WhenItDoesNotStartWithToken(string appRoot,
string contentPath,
public void Content_ReturnsContentPath_WhenItDoesNotStartWithToken(string appRoot,
string contentPath,
string expectedPath)
{
// Arrange
@ -36,8 +36,8 @@ namespace Microsoft.AspNet.Mvc.Core.Test
[InlineData("/myapproot", "~/", "/myapproot/")]
[InlineData("", "~/Home/About", "/Home/About")]
[InlineData("/myapproot", "~/", "/myapproot/")]
public void Content_ReturnsAppRelativePath_WhenItStartsWithToken(string appRoot,
string contentPath,
public void Content_ReturnsAppRelativePath_WhenItStartsWithToken(string appRoot,
string contentPath,
string expectedPath)
{
// Arrange
@ -247,22 +247,223 @@ namespace Microsoft.AspNet.Mvc.Core.Test
Assert.False(result);
}
[Fact]
public void RouteUrlWithDictionary()
{
// Arrange
var urlHelper = CreateUrlHelperWithRouteCollection("/app");
// Act
var url = urlHelper.RouteUrl(values: new RouteValueDictionary(
new
{
Action = "newaction",
Controller = "home2",
id = "someid"
}));
// Assert
Assert.Equal("/app/home2/newaction/someid", url);
}
[Fact]
public void RouteUrlWithEmptyHostName()
{
// Arrange
var urlHelper = CreateUrlHelperWithRouteCollection("/app");
// Act
var url = urlHelper.RouteUrl(routeName: "namedroute",
values: new RouteValueDictionary(
new
{
Action = "newaction",
Controller = "home2",
id = "someid"
}),
protocol: "http",
host: string.Empty);
// Assert
Assert.Equal("http://localhost/app/named/home2/newaction/someid", url);
}
[Fact]
public void RouteUrlWithEmptyProtocol()
{
// Arrange
var urlHelper = CreateUrlHelperWithRouteCollection("/app");
// Act
var url = urlHelper.RouteUrl(routeName: "namedroute",
values: new RouteValueDictionary(
new
{
Action = "newaction",
Controller = "home2",
id = "someid"
}),
protocol: string.Empty,
host: "foo.bar.com");
// Assert
Assert.Equal("http://foo.bar.com/app/named/home2/newaction/someid", url);
}
[Fact]
public void RouteUrlWithNullProtocol()
{
// Arrange
var urlHelper = CreateUrlHelperWithRouteCollection("/app");
// Act
var url = urlHelper.RouteUrl(routeName: "namedroute",
values: new RouteValueDictionary(
new
{
Action = "newaction",
Controller = "home2",
id = "someid"
}),
protocol: null,
host: "foo.bar.com");
// Assert
Assert.Equal("http://foo.bar.com/app/named/home2/newaction/someid", url);
}
[Fact]
public void RouteUrlWithNullProtocolAndNullHostName()
{
// Arrange
var urlHelper = CreateUrlHelperWithRouteCollection("/app");
// Act
var url = urlHelper.RouteUrl(routeName: "namedroute",
values: new RouteValueDictionary(
new
{
Action = "newaction",
Controller = "home2",
id = "someid"
}),
protocol: null,
host: null);
// Assert
Assert.Equal("/app/named/home2/newaction/someid", url);
}
[Fact]
public void RouteUrlWithObjectProperties()
{
// Arrange
var urlHelper = CreateUrlHelperWithRouteCollection("/app");
// Act
var url = urlHelper.RouteUrl(new { Action = "newaction", Controller = "home2", id = "someid" });
// Assert
Assert.Equal("/app/home2/newaction/someid", url);
}
[Fact]
public void RouteUrlWithProtocol()
{
// Arrange
var urlHelper = CreateUrlHelperWithRouteCollection("/app");
// Act
var url = urlHelper.RouteUrl(routeName: "namedroute",
values: new
{
Action = "newaction",
Controller = "home2",
id = "someid"
},
protocol: "https");
// Assert
Assert.Equal("https://localhost/app/named/home2/newaction/someid", url);
}
[Fact]
public void RouteUrlWithRouteNameAndDefaults()
{
// Arrange
var routeCollection = GetRouteCollection("MyRouteName", "any/url");
var urlHelper = CreateUrlHelper("/app", routeCollection);
// Act
var url = urlHelper.RouteUrl("MyRouteName");
// Assert
Assert.Equal("/app/any/url", url);
}
[Fact]
public void RouteUrlWithRouteNameAndDictionary()
{
// Arrange
var urlHelper = CreateUrlHelperWithRouteCollection("/app");
// Act
var url = urlHelper.RouteUrl(routeName: "namedroute",
values: new RouteValueDictionary(
new
{
Action = "newaction",
Controller = "home2",
id = "someid"
}));
// Assert
Assert.Equal("/app/named/home2/newaction/someid", url);
}
[Fact]
public void RouteUrlWithRouteNameAndObjectProperties()
{
// Arrange
var urlHelper = CreateUrlHelperWithRouteCollection("/app");
// Act
var url = urlHelper.RouteUrl(routeName: "namedroute",
values: new
{
Action = "newaction",
Controller = "home2",
id = "someid"
});
// Assert
Assert.Equal("/app/named/home2/newaction/someid", url);
}
private static HttpContext CreateHttpContext(string appRoot)
{
var appRootPath = new PathString(appRoot);
var request = new Mock<HttpRequest>();
request.SetupGet(r => r.PathBase)
.Returns(appRootPath);
request.SetupGet(r => r.Host)
.Returns(new HostString("localhost"));
var context = new Mock<HttpContext>();
context.SetupGet(c => c.Request)
.Returns(request.Object);
return context.Object;
}
private static IContextAccessor<ActionContext> CreateActionContext(HttpContext context)
{
return CreateActionContext(context, (new Mock<IRouter>()).Object);
}
private static IContextAccessor<ActionContext> CreateActionContext(HttpContext context, IRouter router)
{
var actionContext = new ActionContext(context,
Mock.Of<IRouter>(),
router,
new Dictionary<string, object>(),
new ActionDescriptor());
var contextAccessor = new Mock<IContextAccessor<ActionContext>>();
@ -296,5 +497,52 @@ namespace Microsoft.AspNet.Mvc.Core.Test
var actionSelector = new Mock<IActionSelector>(MockBehavior.Strict);
return new UrlHelper(contextAccessor, actionSelector.Object);
}
private static UrlHelper CreateUrlHelper(string appBase, IRouter router)
{
var context = CreateHttpContext(appBase);
var actionContext = CreateActionContext(context, router);
var actionSelector = new Mock<IActionSelector>(MockBehavior.Strict);
return new UrlHelper(actionContext, actionSelector.Object);
}
private static UrlHelper CreateUrlHelperWithRouteCollection(string appPrefix)
{
var routeCollection = GetRouteCollection();
return CreateUrlHelper("/app", routeCollection);
}
private static RouteCollection GetRouteCollection()
{
return GetRouteCollection("mockRoute", "/mockTemplate");
}
private static RouteCollection GetRouteCollection(string mockRouteName, string mockTemplateValue)
{
var rt = new RouteCollection();
var target = new Mock<IRouter>(MockBehavior.Strict);
target
.Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
.Callback<VirtualPathContext>(c => c.IsBound = true)
.Returns<VirtualPathContext>(rc => null);
rt.DefaultHandler = target.Object;
rt.MapRoute(string.Empty,
"{controller}/{action}/{id}",
new RouteValueDictionary(new { id = "defaultid" }));
rt.MapRoute("namedroute",
"named/{controller}/{action}/{id}",
new RouteValueDictionary(new { id = "defaultid" }));
var mockHttpRoute = new Mock<IRouter>();
mockHttpRoute.Setup(mock =>
mock.GetVirtualPath(It.Is<VirtualPathContext>(c => string.Equals(c.RouteName,
mockRouteName)
)))
.Returns(mockTemplateValue);
rt.Add(mockHttpRoute.Object);
return rt;
}
}
}
}