上一篇文章中我们构造了DomainRoute类,这是一个将URL Routing扩展至域名的Route组件,于是现在我们便可以轻易地从一个URL的Domain部分中捕获数据并在程序中使用。不过作为URL Routing的另一个重要部分,在URL构建方面,我们还需给DomainRoute补充额外的支持。
我想再次强调一下,URL Routing的功能并不只是从URL中捕获数据,它还有一个作用便是根据数据组装出一个URL。简单地说,URL Routing的功能是“双向”的。这也是为什么默认的Route组件会使用{controller}/{action}这样的简单模式,而不是常用的成熟的正则表达式,这就是因为正则表达式能够根据模式来匹配出值来,但是无法根据“值”反过来构建一个字符串。
在编写ASP.NET MVC的视图时,我们需要在HTML中填充链接的URL。在最初的时刻,我们会直接放上“约定”好的字符串,但是更好的方式显然是使用辅助方法。记得在搞URL Rewrite时,常常有朋友问我“页面中填写的URL怎么变成‘漂亮’的样子”,对此我只能说“没办法,直接写吧”,接着自然是一阵大改。而通过辅助方法来构造URL,由于我们只要向URL Routing提供数据,而Route会自动根据配置构造出一个可供自己识别的URL。如果我们想改变URL样式,只要在一个地方改变URL Routing的配置即可,页面中所有的URL便会同时改变了。
我认为,这也是遵守DRY原则的经典示例之一。
在ASP.NET MVC中已经自带了一部分构造URL所使用的辅助方法,例如在UrlHelper类中已经包含了Action方法(以及许多重载)。但是这些个方法并不适合FormatRoute。因为在这些实现最终都使用了RouteCollection的GetVirtualPath方法,它会把Route所得到的URL作为“虚拟路径”对待,这意味着FormatRoute返回了javascript:void(0)这样的URL之后,也会当作是一个普通的Path,最终在页面上便会出现“/http:/www.cnblogs.com/JeffreyZhao/”这样的URL,这显然是错误的。
这样看来,难道ASP.NET Routing本身是“不打算”支持域名的吗?似乎我们又不仅仅是在“扩展”,又出现“Hack”的意味了。
不过知道了错误原因之后,修改起来就非常容易了。例如,我们可以为UrlHelper补充一个扩展方法叫做ActionEx,实现起来也就几行代码:
public static string ActionEx(this UrlHelper helper, string action, object routeValues)
{
var values = routeValues == null ?
new RouteValueDictionary() :
new RouteValueDictionary(routeValues);
values.Add("action", action);
values.Add("controller", helper.RequestContext.RouteData.Values["controller"]);
var pathData = helper.RouteCollection.GetPath(helper.RequestContext, values);
var url = pathData.VirtualPath;
return IsAbsolute(url) ? url : "/" + url;
}
private static VirtualPathData GetPath(
this RouteCollection routes,
RequestContext requestContext,
RouteValueDictionary values)
{
foreach (RouteBase r in routes)
{
VirtualPathData pathData = r.GetVirtualPath(requestContext, values);
if (pathData != null)
{
return pathData;
}
}
throw new ArgumentException("Invalid values for building URL.");
}
private static bool IsAbsolute(string url)
{
return
url.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) ||
url.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase);
}
要做的事情还是分三步走:
- 收集route values
- 从route配置中获得URL
- 处理URL并返回
根据FormatRoute的逻辑,返回的URL可能有两种情况:
- 如果目标地址与当前请求的域名相同,则地址为一个相对路径,如“Home/Index/5”。
- 如果目标地址与当前请求的域名不同,则地址为一个绝对路径,如“http://space.cnblogs.com/Home/Index/5”。
两种地址的处理方式自然不同,而我们目前的处理方式是最简单的:在相对路径之前加一个斜杠“/”。很明显,这里做了一个“假设”,那就是我们的ASP.NET MVC应用程序是部署在域名的根目录下的(似乎DomainRoute本身也有这样的要求)。如果您的应用程序部署在一个虚拟目录下,这部分逻辑自然是需要修改的。不过作为大部分应用来说,现在的功能应该已经足够使用了。
试验一番吧。例如我们使用这样的Route配置:
var defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" });
var route =
new Route("{controller}/{action}/{id}", defaults, new MvcRouteHandler())
.WithDomain("http://{area}.{*domain}");
routes.Add("Default", route);
然后请求http://www.jeffz-test.com/Home/Index,并在相应视图中写下这样的代码:
<a href="<%= Url.ActionEx("List", null) %>">List</a>
<a href="<%= Url.ActionEx("LogIn", new { id = 10, area = "account" })%>">Accout LogIn</a>
于是在页面上就会生成这样的HTML:
<a href="/Home/List">List</a>
<a href="http://account.jeffz-test.com/Home/LogIn/10">Accout LogIn</a>
看上去不错吧?
ASP.NET MVC中的其他与构造URL功能有关的辅助方法,如ActionLink,其实也都是相同的原理。如果您需要的话,也不妨自己实现一个对应的ActionLinkEx方法。
不过,根据尽可能强类型的原则,我们应该使用的是MvcFutures中定义的基于表达式树的辅助方法。不过MvcFutures里的这些方法有些问题,如最常见的“视ActionNameAttribute于无物”。在使用过程中我也遇到了其他一些问题,下次我们再来改造这些辅助方法。
在条件充分的时候,使用表达式树来表示URL构造,可以说有百利而无一害。