zoukankan      html  css  js  c++  java
  • 请别埋没了URL Routing

    原文地址:http://www.cnblogs.com/JeffreyZhao/archive/2009/03/05/fully-leverage-url-routing.html

    本文做法不甚妥当,更好的做法请参考:《对Action方法的参数进行双向转化

    实现分析

    既然Model Binder机制有着明显的缺陷,那么我们又该如何处理这样的问题呢?

    我们再来回顾一下目前问题:对于从URL中表现出来的参数,我们可以把URL Routing捕获到的数据使用Model Binder进行转化(例如上例中的DateTimeModelBinder);但是如果我们在生成URL时直接提供复杂参数,则框架只会把它简单的ToString后放入URL。这是因为那些与URL有关的HTML Helper会将数据交给URL Ruoting组件来生成URL,而Route规则在生成URL时不知道一个复杂对象该如何转变为URL,因此……

    慢着,你刚才说,把数据“交给URL Routing组件来生成URL”?URL Routing不是解析URL用的吗?为什么还负责“生成”URL?没错,与Model Binder不同,URL Routing的工作职责是“双向”的。它既负责从URL中提取RouteData,也负责根据Route生成一个URL——可惜微软没有对URL Routing给出足够的资料,有相当多的朋友没有意识到这一点。

    可恶的微软。

    既然问题的原因是Model Binder的“单向性”,那么如果存在一个“双向”的Model Binder就应该可以解决问题。例如,我们可以继承现有的IModelBinder接口进行扩展,那么至少从解析URL到执行Action方法这个流程中所有的功能都不需要任何额外工作。可惜,这种做法对于大多数HTML Helper来说,我们就必须定义新的扩展,才能利用所谓的“双向Model Binder”。不过其实我们可以有更好的解决方案——成本低廉,通用性强。既然上次提到了传说中的“Model Binder强迫症”,那么我们现在就把目光移到Model Binder以外的地方。

    您一定已经猜到我们要从哪里入手了。没错,就是URL Routing。关于这方面,大名鼎鼎的Scott Hanselman同学提出将DateTime类型进行分割,也就是将一个DateTime切成年、月、日多个部分进行表示。这个做法老赵颇不赞同,无论从易用性还是通用性等角度来看,这种做法都是下下之策。说实话,这样的做法其实并没有跳出框架既有功能给定的圈子,它只是通过“迎合框架”来满足自己的需求,而不是让框架为我们的需求服务。

    那么,我们来分析一下URL Routing组件的运作方式吧,这是必要的预备工作:

    • 首先,应用程序为RouteCollection类型的RouteTable.Routes集合添加一些Route规则,每个规则即为一个RouteBase对象。RouteBase是一个抽象类型,其中包含两个抽象方法,GetRouteData和GetVirtualPath。
    • 在捕获URL中数据的时候,URL Routing组件将调用RouteTable.Routes.GetRouteData方法来获得一个RouteData对象。简单来说,它会依次调用每个RouteBase对象的GetRouteData方法,直到得到第一个不为null的RouteData对象。
    • 在生成URL时,URL Routing组件将调用RouteTable.Routes.GetVirtualPath方法来获得一个VirtualPathData对象。简单来说,它会依次调用每个RouteBase对象的GetVirtualPath方法,直到得到第一个不为null的VirualPathData对象。

    显然,光有RouteBase抽象类型是不足以提供任何有用功能的。因此URL Routing框架还提供了一个具体的Route类型供大家使用。说起Route类,它的功能可谓非常强大。我们在使用ASP.NET MVC框架时用到的MapRoute方法,其实就是在向RouteTable.Routes集合中添加Route对象。而其中的URL占位符,默认值,约束等功能,实际上完全由Route对象实现了。多么强大的Route类型!如果想要写一个足以匹敌,并且包含额外功能的RouteBase实现可不是一件容易的事情。幸好我们生活在面向对象的美好世界中,“复用”是我们手中威力非凡的利器。如果我们基于现有的Route类型进行扩展,那么大部分的工作我们弹指间便可完成。

    现有的Route只能从URL中提取字符串类型的数据,同时也只能把任何对象作为字符串来生成URL。而我们将要构造RouteBase实现,就要弥补这一缺陷,让Route规则能够直接从URL中提取出复杂对象,并且知道如何将一个复杂对象转化为一个URL。有了前者,RouteData就能包含复杂类型的对象,以此应对Action方法的参数自然不是问题;有了后者,我们只需要提供一个强类型的复杂对象,Route规则也能顺利地将其转化为可以识别的URL——多么美好。

    Route Formatter

    那么解析字符串,或生成URL的职责由谁来完成呢?于是我们定义一个IRouteFormatter来负责这件事情:

    public interface IRouteFormatter
    {
        bool TryParse(object value, out object output);
    
        bool TryToString(object value, out string output);
    }
    

    TryParse方法负责将一个对象转化为我们需要的复杂类型对象,而TryToString则将一个复杂类型对象转化为字符串(即URL)。两个方法都返回一个布尔值,以表示这次转化是否合法。您可能会发现,TryToString输出的是一个string,而TryParse……他接受的是一个object类型的参数,这是怎么回事呢?原因在于Route规则中的“默认值”设置。在Route规则中我们可以为RouteData中的某个“字段”设定默认值,这样即使URL中无法捕获到这个字段,它也可以出现在RouteData中。从URL中捕获得到的自然是一个字符串,但是默认值则可以设为任意类型的对象。因此Formatter需要可以接受一个object参数,并设法将其转化为我们需要的复杂类型。

    是不是有点绕?请继续看下去,您会了解它的作用的。虽说TryParse需要接受一个object参数,但是在大多数情况下,我们更多是要处理强类型。因此我们不妨再定一个RouteFormatter抽象类,方便强类型IRouteFormatter对象的编写:

    public abstract class RouteFormatter<T> : IRouteFormatter
    {
        public abstract bool TryParse(string value, out T output);
    
        public abstract bool TryToString(T value, out string output);
    
        bool IRouteFormatter.TryParse(object value, out object output)
        {
            if (value is T)
            {
                output = value;
                return true;
            }
    
            string s = value as string;
            if (s == null)
            {
                output = null;
                return false;
            }
            else
            {
                T t;
                var result = this.TryParse(s, out t);
                output = t;
                return result;
            }
        }
    
        bool IRouteFormatter.TryToString(object value, out string output)
        {
            if (value is T)
            {
                return this.TryToString((T)value, out output);
            }
            else
            {
                output = null;
                return false;
            }
        }
    }
    

    RouteFormater<>类接受一个范型参数,并且准备两个强类型的抽象方法让子类实现。至于接口中的两个类型,它们会处理一部分逻辑——主要是类型判断——只在合适的时候将操作交给范型方法来实现。TryToString方法朴实无华,而TryParse方法相对较为有趣,它会首先判断value参数的类型,如果已经符合当前的范型类型,则直接将其转化后返回。这就是为了“默认值”而进行的处理,例如用户准备了一个DateTime类型的默认值,并被Route规则采纳了,则我们的RouteFormatter<DateTime>就会将其直接返回,不做任何转化。

    为了解决目前提出的问题,我们会编写一个DateTimeFormatter,它接受一个Format参数表示日期的格式:

    public class DateTimeFormatter : RouteFormatter<DateTime>
    {
        public string Format { get; private set; }
    
        public DateTimeFormatter(string format)
        {
            this.Format = format;
        }
    
        public override bool TryParse(string value, out DateTime output)
        {
            return DateTime.TryParseExact(value, this.Format, null, DateTimeStyles.None, out output);
        }
    
        public override bool TryToString(DateTime value, out string output)
        {
            output =  value.ToString(this.Format);
            return true;
        }
    }
    

    那么有没有某个Route Formatter需要直接实现IRouteFormatter接口呢?有。之前提到TryParse方法将在value参数符合范型T的情况下直接返回“通过”,如果某个Route Formatter不支持这条判断,则自然无法继承于RouteFormatter<>类型。例如下面的RegexFormatter,将使用正则表达式对某个字段的值进行约束。在我们的RouteBase实现中,RegexFormatter便是Route类中“约束”功能的替代品。如下:

    public class RegexFormatter : IRouteFormatter
    {
        public Regex Regex { get; private set; }
    
        public RegexFormatter(string pattern)
        {
            this.Regex = new Regex(pattern,
                RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled);
        }
    
        public bool TryParse(object value, out object output)
        {
            string s;
            bool result = this.Try(value, out s);
            output = s;
            return result;
        }
    
        public bool TryToString(object value, out string output)
        {
            return this.Try(value, out output);
        }
    
        private bool Try(object value, out string output)
        {
            var s = value as string;
            if (s != null && this.Regex.IsMatch(s))
            {
                output = s;
                return true;
            }
            else
            {
                output = null;
                return false;
            }
        }
    }
    

    RegexFormatter的关键在于Try方法。Try方法首先判断value参数是否为一个字符串,如果是,则使用正则表达式进行验证。当且仅当value为字符串并满足指定的正则表达式时,RegexFormatter才表示“通过”。

    FormatRoute实现

    FormatRoute便是我们RouteBase抽象类的实现,它提供了Route类的所有功能,并可以为每个字段设置一个Route Formatter对象,以此对这个字段进行转换或约束。之前提到,我们会将主要功能委托给现有Route类型,这样可以大大简化我们的工作量。因此,我们会在FormatRoute中包含一个Route类型的对象,此外还会保留所有字段与其Route Formatter的映射关系。请看如下构造函数:

    public class FormatRoute : RouteBase
    {
        private Route m_route;
        private IDictionary<string, IRouteFormatter> m_formatters;
    
        public FormatRoute(
            string url,
            RouteValueDictionary defaults,
            IDictionary<string, IRouteFormatter> formatters,
            RouteValueDictionary constaints,
            RouteValueDictionary dataTokens,
            IRouteHandler routeHandler)
        {
            this.m_formatters = formatters;
            this.m_route = new Route(
                url,
                defaults,
                constaints,
                dataTokens,
                routeHandler);
        }
    
        ...
    }
    

    RouteBase的关键方法便是GetRouteData和GetVirtualPath。有了Route类型的辅助,这两个方法其实非常简单。如下:

    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        var result = this.m_route.GetRouteData(httpContext);
        if (result == null) return null;
    
        var valuesModified = new Dictionary<string, object>();
        foreach (var pair in result.Values)
        {
            var key = pair.Key;
            IRouteFormatter formatter = null;
            if (this.m_formatters.TryGetValue(key, out formatter))
            {
                object o;
                if (formatter.TryParse(pair.Value, out o))
                {
                    valuesModified[key] = o;
                }
                else
                {
                    return null;
                }
            }
        }
    
        foreach (var pair in valuesModified)
        {
            result.Values[pair.Key] = pair.Value;
        }
    
        return result;
    }
    
    public override VirtualPathData GetVirtualPath(
        RequestContext requestContext, RouteValueDictionary values)
    {
        var routeValues = new RouteValueDictionary();
        foreach (var pair in values)
        {
            var key = pair.Key;
            IRouteFormatter formatter = null;
            if (this.m_formatters.TryGetValue(key, out formatter))
            {
                string s;
                if (formatter.TryToString(pair.Value, out s))
                {
                    routeValues[key] = s;
                }
                else
                {
                    return null;
                }
            }
            else
            {
                routeValues[key] = pair.Value;
            }
        }
    
        return this.m_route.GetVirtualPath(requestContext, routeValues);
    }
    

    GetRouteData会接受一个HttpContextBase对象,并调用Route对象的GetRouteData方法获取一个RouteData对象。如果RouteData不为null,则遍历其中的所有字段,如果指定了对应的Route Formater,则还需要通过Route Formatter的检验及转化——没错,经历了Route Formatter之后的RouteData中已经包含了强类型对象。而GetVirtualPath方法则略有不同,它首先遍历values参数中的所有字段,将其中的强类型对象转化为字符串,也就是URL片段,这样交给Route对象来生成VirtualPathData时,便可以得到正确的URL了。

    最后便是FormatRoute的运用:

    routes.Add(
        "Demo.Date",
        new FormatRoute(
            "{controller}/{action}/{date}",
            new RouteValueDictionary(), // defaults
            new Dictionary<string, IRouteFormatter>
            {
                {"controller", new RegexFormatter("Demo")},
                {"action", new RegexFormatter("Date")},
                {"date", new DateTimeFormatter("yyyy-MM-dd")}
            },
            new RouteValueDictionary(), // constaints
            new RouteValueDictionary(), // data tokens
            new MvcRouteHandler()));
    

    除了为date字段指定了转化用的DateTimeFormatter之外,我们也为controller和action字段提供了负责约束的RegexFormatter——这点只是为了演示。更好的做法是直接将URL设为Demo/Date/{date},并在默认值中指定controller和action的值。此外,您也可以使用传统的方式为字段提供约束,而不是使用RegexFormatter。当然,效果几乎可以说是一模一样的。

    总结

    现在我们完美地解决了之前提出的问题。使用FormatRoute可以轻松地处理URL中特定类型对象的提取,并且可以把特定类型的对象转化为URL的片段。除了日期时间之外,我们还可以转化语言文化,查询条件等任意复杂类型。而RouteFormatter对象与Route规则的分离,使得我们可以对RouteFormatter进行独立的单元测试,这也是一件十分理想的事情。这下在视图中,无论是指定Route Values,还是使用强类型的方式,我们都可以正确获得所需的URL了。如下:

    <%= Html.ActionLink("Yesterday", "Date", new { date = date.AddDays(-1) }) %>    
    <span><%= date.ToShortDateString() %></span>        
    <%= Html.ActionLink<DemoController>(c => c.Date(date.AddDays(1)), "Tomorrow") %>
    

    那么,从设计上讲,把数据的提取转移到URL Routing上是否合适呢?答案是肯定的。因为URL Routing的职责原本就是从URL中提取数据——任意类型的数据,以及把数据转化为URL,我们现在只是充分利用了URL Routing的功能而已。事实上,我建议任何使用URL表示的数据,都把转化的职责转移到URL Routing这一层,因为这时我们基本上无可避免地需要根据数据来生成URL。一般情况下,我们要尽可能地使用强类型数据。那么Model Binder难道就没有用了吗?当然不是。URL Routing负责从URL中提取数据,而Model Binder则用于从其他方面来获取参数。例如POST来的数据,例如《最佳实践》中的Url Referrer参数。

    打开视野,发挥程序员的敏捷思路,生活就会变得更加美好。


  • 相关阅读:
    linux 安装 tomcat
    IE条件注释
    了解常见的开源协议(BSD, GPL, LGPL,MIT)
    Ueditor 1.4.3 单独调用上传图片,或文件功能
    javascript代码规范 [转]
    html5 拖曳功能的实现[转]
    几种常用的正则表达式[转]
    MYSQL基础03(日期函数)
    MYSQL基础02(查询)
    OpenCV(7)-图像直方图
  • 原文地址:https://www.cnblogs.com/AI001/p/3996832.html
Copyright © 2011-2022 走看看