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参数。

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


  • 相关阅读:
    SAP PI 如何实现消息定义查询
    EWM与ERP交互程序
    ITS Mobile Template interpretation failed. Template does not exist
    SAP Material Flow System (MFS) 物料流系统简介
    SAP EWM Table list
    EWM RF 屏幕增强
    SAP EWM TCODE list
    SAP扩展仓库管理(SAPEWM)在线研讨会笔记
    ERP与EWM集成配置ERP端组织架构(二)
    EWM RF(Radio Frequency)简介
  • 原文地址:https://www.cnblogs.com/AI001/p/3996832.html
Copyright © 2011-2022 走看看