Hello World
Spiga

对Action方法的参数进行双向转化

2009-10-23 09:47 by 老赵, 16151 visits

昨天有朋友忽然告诉我,在G点中国上搜索URL Routing时,我的《请别埋没了URL Routing》一文排在首位。这不禁让我汗颜,这是因为从现在的角度看起来,这篇文章的内容虽不能算错,但的确也不算是一种非常合适的做法。那篇文章的目的是展示如何利用URL Routing的扩展能力,将URL和Route Values通过Formatter进行双向的转化。这样便可以在Action方法中使用复杂参数的同时,也可以使用复杂参数得到正确的URL。这个目标是好的,可惜当时的思路有些偏差。现在我总结出了更合适的做法,并已经在项目中大量使用,效果不错。

之前提出那种原因,是因为Model Binder是单向的。也就是说,使用Model Binder把URL Routing的结果转化成Action的参数时不会有任何问题。例如,我们可以定义一个DateTimeModelBinder,接受一个字符串作为格式参数,这样便可以把URL Routing过程中得到的字符串转化为Action方法的DateTime类型参数了:

public ActionResult Date([DateTime("yyyy-MM-dd")]DateTime date) { ... }

但是,如果我们想要在视图中使用URL Routing来构造URL:

Html.ActionLink("Tomorrow", "Date", new { date = date.AddDays(1) })

或借助MvcFutures里提供的强类型(表达式树)辅助方法(其实它也是利用了URL Routing):

Html.ActionLink<DemoController>(c => c.Date(date.AddDays(1)), "Tomorrow")

问题就来了,因为它们得到的结果与我们的期望相距甚远:

<a href="/Demo/Date/01/01/2003%2000:00:00">Tomorrow</a>

看这链接中Date后面的那部分,多奇妙,多恶心。出现这个问题的原因在于,我们使用Model Binder可以知道如何将一个字符串正确转化为DateTime对象。但是,在构造URL的时候,如果我们利用了URL Routing,那么直接提供的复杂对象就会通过默认的ToString方法来作为URL的一部分——这显然是有问题的,例如这里的DateTime。因为这一点,在ASP.NET MVC应用程序中使用强类型的表达式树来生成URL几乎是一个不可实现的功能。

当然,我们是程序员,我们可以扩展。因此,我当时扩展了一个FormatRoute,它使用装饰器模式,封装了一个RouteBase对象,并且在GetRouteData时使用Formatter将RouteValueDictionary中的值直接从字符串转化成复杂类型的对象——并且在GetVirtualPath方法中作一个反向的操作。由于Routing功能是双向的,因此我们这么做便可以解决上面这个问题。当然,使用FormatRoute之后,生成Action参数的职责就交到了URL Routing阶段里。在一段时间里,我在项目中定下了这样的“规范”:

  • 如果是从URL中得到的Action参数,那么转化职责交给URL Routing。
  • 如果是从别处(如Post过来的数据)得到的Action参数,转化职责交给Model Binder。

这样,我们在视图中便可以使用强类型的表达式树来生成URL了,同时享受静态检查所得到的便利。

可惜看上去很美,但用起来一般。原因便是——太麻烦了。试想,我们在配置URL Routing的时候,往往会使用同一条Routing规则对应多个不同的URL形式,最终进入不同的Action方法(如默认的{controller}/{action}/{id})。但是,复杂类型参数的转化逻辑是根据Action不同而有所改变的。因此,如果我们要将转化参数的职责交给URL Routing的话,势必需要为每个Action方法指定一个Routing规则。那么好,这样Routing规则是不是会变得泛滥?如果我要修改Action,是不是还要去修改对应的Routing规则?这不是把相关的逻辑分散了吗?于是我又想出了其他一些方式来弥补这个问题,例如由Controller负责Routing规则的配置等等,这些尝试就不多提了。

总之,这个方法的目标正确,但是解决方式有些问题。但是,我们又能如何解决呢?既然事情是出在“URL生成”的方式上,那么我们就来改变一下辅助方法的逻辑吧,反正我们已经为它优化了性能,使它支持指定的Route名称,以及可以忽略某些参数等等。于是我在MvcPatch项目中增加了额外的IRouteBinder接口:

public interface IRouteBinder
{
    RouteValueDictionary BindRoute(RequestContext requestContext, RouteBindingContext bindingContext);
}

public class RouteBindingContext
{
    public object Model { get; set; }

    public Type ModelType { get; set; }

    public string ModelName { get; set; }
}

与Model Binder的功能正好相反,Route Binder的作用是把一个复杂类型的参数转化为一个RouteValueDictionary。在构造URL的辅助方法中会去检查标记在这个参数,或者这个参数类型中的Model Binder(没错,就是ASP.NET MVC本身获取Model Binder的方式),然后如果这个Model Binder还实现了IRouteBinder接口,则会先进行转化再构造URL,否则就和以前一样,直接使用这个参数的值构造URL。

因此,文章之前的DateTime参数的转化,便可以使用这样的一个Binder来处理:

public class DateTimeBinder : IModelBinder, IRouteBinder
{
    public DateTimeBinder(string format)
    {
        this.Format = format;
    }

    public string Format { get; private set; }

    #region IModelBinder Members

    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var modelName = bindingContext.ModelName;
        var rawValue = bindingContext.ValueProvider[modelName].RawValue;
        return DateTime.ParseExact(rawValue.ToString(), this.Format, null);
    }

    #endregion

    #region IRouteBinder Members

    public RouteValueDictionary BindRoute(RequestContext requestContext, RouteBindingContext bindingContext)
    {
        var modelName = bindingContext.ModelName;
        var model = (DateTime)bindingContext.Model;
        var routeValues = new RouteValueDictionary();
        routeValues.Add(modelName, model.ToString(this.Format));
        return routeValues;
    }

    #endregion
}

于是,这个世界和平了。

当然,这个做法和之前相比也有缺陷,那是因为IRouteBinder的功能是在辅助方法内调用的,因此如果您绕开辅助方法使用URL Routing构造URL的话,复杂类型的参数便无法得到转化了。不过权衡之下,现在这个做法使用更加便捷,由于把双向的转化逻辑放在同一处,其维护性也很好。经过我目前项目的使用,效果良好。

最近,我打算在MvcPatch中构建一个合适的示例程序,不会太复杂也但也足以用上MvcPatch里的各种功能(例如一个类似博客形式的应用程序)。如果您有合适的题材,也请及时告诉我。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

25 条回复

  1. kivenhou
    *.*.*.*
    链接

    kivenhou 2009-10-23 09:48:00

    抢个沙发

  2. Jun1st
    *.*.*.*
    链接

    Jun1st 2009-10-23 10:22:00

    学了一把

    看到一个小错误
    “接受一个字符串作为格式参数,这样便可以便可以把URL Routing过程中得到的字符串转化为Action方法的DateTime类型参数了:”

  3. dongzz
    *.*.*.*
    链接

    dongzz 2009-10-23 10:40:00

    一周内最热博客
    莫问奴归处
    Jeffrey Zhao
    包建强
    李永京
    *王员外*
    紫色永恒
    伍华聪
    Kingthy
    Jialiang
    坚强2002

    老赵你怎么第二啦?第一的那个没什么内容吧。

  4. 老赵
    admin
    链接

    老赵 2009-10-23 11:43:00

    @dongzz
    那个人好像每天都贴很多文章,根据博客园的公式来看,算出这个结果也很正常啊。

  5. 老赵
    admin
    链接

    老赵 2009-10-23 11:44:00

    @Jun1st
    谢谢。

  6. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-10-23 12:17:00

    G点中国......
    发现老赵也很XE

  7. 不若相忘于江湖
    *.*.*.*
    链接

    不若相忘于江湖 2009-10-23 12:36:00



    老赵你的文章有点候看不太懂. 我的水平问题吗?

  8. vczh[未注册用户]
    *.*.*.*
    链接

    vczh[未注册用户] 2009-10-23 12:37:00

    @winter-cn
    看到了你这句话,我专门去搜一下什么是G点,结果……我太CJ了……

  9. 骑着夕阳看着猪
    *.*.*.*
    链接

    骑着夕阳看着猪 2009-10-23 13:29:00

    关注~~~·

  10. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-10-23 13:45:00

    在MvcPatch里不是已经有个Sample了吗?

  11. rocklau
    *.*.*.*
    链接

    rocklau 2009-10-23 13:50:00

    起初看着之前URL Routing的那片就别扭,今天赵给出了微软式的解决方法.

  12. 老赵
    admin
    链接

    老赵 2009-10-23 13:51:00

    @rocklau
    啥叫微软式亚?

  13. 老赵
    admin
    链接

    老赵 2009-10-23 13:51:00

    @麒麟.NET
    基本上算是一个空项目而已,不完整的。

  14. 老赵
    admin
    链接

    老赵 2009-10-23 13:52:00

    不若相忘于江湖:
    老赵你的文章有点候看不太懂. 我的水平问题吗?


    不知道,不懂就问咯。

  15. 不若相忘于江湖
    *.*.*.*
    链接

    不若相忘于江湖 2009-10-23 13:55:00

    vczh:
    @winter-cn
    看到了你这句话,我专门去搜一下什么是G点,结果……我太CJ了……




    呵呵.. 说出结果吧... 什么事让你太JC了.哈哈.

  16. drink
    *.*.*.*
    链接

    drink 2009-10-23 14:07:00

    Jeffrey Zhao:
    @麒麟.NET
    基本上算是一个空项目而已,不完整的。


    要么弄个HR的管理.简历查找、面试安排、评估分类。希望比Asp.net里的Starter Kits里的Employee Info\Job Site Starter Kit完整,能够有NerdDinner的水准。

  17. 老赵
    admin
    链接

    老赵 2009-10-23 14:53:00

    @drink
    不懂这业务啊。
    还有,我应该示例里只关注表现层的。

  18. JiaruiStone
    *.*.*.*
    链接

    JiaruiStone 2009-10-23 16:53:00

    水一下,^_^
    终于弄到tech ed的门票了,嘿嘿,等着一睹老赵“芳容”啊。

  19. rocklau
    *.*.*.*
    链接

    rocklau 2009-10-23 17:23:00

    @Jeffrey Zhao
    符合微软框架扩展风格.

    Jeffrey Zhao:
    @rocklau
    啥叫微软式亚?


  20. Kevin Dai
    *.*.*.*
    链接

    Kevin Dai 2009-10-23 23:49:00

    老赵也是一幽默人。

  21. CoolCode
    *.*.*.*
    链接

    CoolCode 2009-10-24 00:33:00

    Patch越来越精彩啦,哇哈哈~

  22. 前进的步伐
    *.*.*.*
    链接

    前进的步伐 2009-10-25 12:12:00

    老赵,我想问下你有有没有
    MVC中过滤器Filter详细的讲解
    希望你能出一篇这样的文章 期待....

  23. 老赵
    admin
    链接

    老赵 2009-10-25 12:38:00

    @前进的步伐
    我觉得这个最最平常的东西,没有想到怎么说比较好……

  24. 前进的步伐
    *.*.*.*
    链接

    前进的步伐 2009-10-25 13:08:00

    @Jeffrey Zhao
    因为在网上关于这个资料很少..

  25. 金思魁
    111.193.112.*
    链接

    金思魁 2012-02-16 10:19:33

    看懂了,生成Url的辅助方法能否给代码??

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我