Hello World
Spiga

Model Binder机制的缺陷

2009-03-02 09:08 by 老赵, 8409 visits

在ASP.NET MVC中,每个请求都被映射到一个Action方法,而Action方法的参数由Model Binder根据Request中的数据转化而成。例如,URL Routing将Request URL解析成数据——这往往是一个字符串,然后该字符串可以被转换一个整型的值;还有可能是从服务器端POST过来的数据中获取特别字段的值进行转化——这还是一个字符串。由于HTTP协议是基于文本的,因此一切都是字符串到某个特定类型的转化。

在ASP.NET MVC中,这个转化的过程由一类特殊的组件来完成,那就是Model Binder。虽然框架中已经提供了一个非常强大的DefaultModelBinder类,已经为我们节省了80%的工作量,但是这种字符串到具体类型对象的转换始终不是一件“自然”的事情。由于业务的不同,我们可能对字符串的“格式”有着不一样的要求,在此时我们就需要定义自己的Model Binder。

Model Binder是一个非常简单而优秀的转化机制,将这部分的关注点分离到一个独立的层次上去,大大简化了框架的使用与测试。不过,Model Binder也不是框架内建的“唯一”解决方案的,在没有得到合适指引的情况下也很容易被滥用。现在我们来做一个小测试,看看您是否得了传说中的……咳咳……其实是老赵提出的“Model Binder强迫症”:

假设DemoController中有个Action方法,它接受一个DateTime作为参数,如下:

public ActionResult Date(DateTime date) { ... }

URL中已经进行了正确的配置(当然,您也可以为date使用正则表达式进行限制):

routes.MapRoute(
    "Demo.BadDate",
    "Demo/BadDate/{date}",
    new { controller = "Demo", action = "BadDate" });

现在,您会使用什么方法将yyyy-MM-dd格式的字符串转化为date参数呢?没错,Model Binder……不过还有其他更好的方法吗?如果您觉得Model Binder是唯一的方法,那么经诊断,您患有“Model Binder强迫症”的几率为80%……玩笑,玩笑。诚然,这个情况看似是Model Binder的一个典型使用场景,我们也可以轻易地通过自定义一个Model Binder和Model Binder Attribute来“解决”这个问题:

public class DateTimeModelBinder : IModelBinder
{
    public string Format { get; private set; }

    public DateTimeModelBinder(string format)
    {
        this.Format = format;
    }

    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider[bindingContext.ModelName].RawValue;
        if (value is string)
        {
            return DateTime.ParseExact((string)value, this.Format, null);
        }
        else
        {
            return value;
        }            
    }
}

public class DateTimeAttribute : CustomModelBinderAttribute
{
    public string Format { get; private set; }

    public DateTimeAttribute(string format)
    {
        this.Format = format;
    }

    public override IModelBinder GetBinder()
    {
        return new DateTimeModelBinder(this.Format);
    }
}

于是乎,问题解决了:

public ActionResult Date([DateTime("yyyy-MM-dd")]DateTime date)
{
    this.ViewData["Date"] = date;
    return this.View();
}

没错,问题似乎是解决了。我们使用DateTimeAttribute标记了date参数,这样框架便会用它来获取参数绑定所需的Mode Binder对象。在DateTimeAttribute内部,将会根据指定的日期格式创建一个DateTimeModelBinder对象,而这个对象就会使用这个格式把URL中的字符串解析为一个DateTime对象。现在,当我们请求Demo/Date/2009-03-01这个URL时,便会调用Date方法,而date参数也会得到正确的值“2009年3月1日”。

可是,您不妨想得更远一些,如果别人要在View里写一个面向该Action的链接,又该怎么做?老赵先来做一个演示:

<% var date = (DateTime)this.ViewData["Date"]; %>    
<p>
    <%=
        Html.ActionLink("Yesterday", "Date",
            new { date = date.AddDays(-1).ToString("yyyy-MM-dd") })
    %>        
    <span><%= date.ToShortDateString() %></span>        
    <%=
        Html.ActionLink("Tomorrow", "Date", new { date = date.AddDays(1) })
    %>
</p>

这里使用了Html.ActionLink辅助功能来生成一个链接,并提供了date的值作为生成URL所需的参数。但是,在生成Yesterday和Tomorrow时提供的date是不一样的。在生成Testerday链接时,我们提供了一个字符串,其格式满足Action方法的需要,而Tomorrow链接则直接给定了一个DateTime对象。那么两者的结果又有什么区别呢?

<p>
    <a href="/Demo/Date/2002-12-30">Yesterday</a>        
    <span>
        2002/12/31
    </span>        
    <a href="/Demo/Date/01/01/2003%2000:00:00">Tomorrow</a>
</p>

显然,只有Yesterday的链接是对的,而Tomorrow的链接变成了一个错误的样子。朋友们应该很容易看出个种原因:我们在辅助方法中指定了一个DateTime对象之后,框架并不知道该如何其转化为URL的一部分,因此只是调用了它的ToString()方法来生成一个字符串,这种“臆断”自然让我们得到了一个失效的链接。归纳说来,框架会利用Model Binder把一个字符串(确切地说,也可能是其他类型数据)转化为一个参数对象,但是却不知道如何把对象表现为一个正确的URL。这正是Model Binder的缺陷:它的功效是“单向”的

难道,我们只能使用转化为字符串的方式来解决这个问题了吗?可惜在多个地方出现同样格式字符串明显违反了DRY原则。虽然我们可以使用构建常量(如const string DATE_FORMAT)来保存这个字符串并多次使用,但是各种显式的ToString调用也是一件麻烦事,万一有遗漏即会发生错误。即便如此,我们还是无法使用《尽可能地使用强类型数据》提到的实践,即使用辅助方法来构造一个面向Action的链接:

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

我们就只能对此妥协吗?这可不是我们程序员的风骨。就此问题,请参考老赵下一篇文章,《请别埋没了URL Routing》。

Creative Commons License

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

Add your comment

24 条回复

  1. 老赵
    admin
    链接

    老赵 2009-03-02 00:32:00

    这篇文章和下一片文章的内容,其实已经在Webcast里讲过了,不过当时给出了一个错误的解决方案,对此只能说声抱歉了。

  2. Mien Ng
    *.*.*.*
    链接

    Mien Ng 2009-03-02 02:39:00

    赵老师是个夜猫子,还没睡。注意身体呀:)
    《回归URL Routing》,是url routing归来的意思?

  3. Artech
    *.*.*.*
    链接

    Artech 2009-03-02 08:14:00

    老赵最近很勤奋嘛:)

  4. 老赵
    admin
    链接

    老赵 2009-03-02 09:09:00

    --引用--------------------------------------------------
    Artech: 老赵最近很勤奋嘛:)
    --------------------------------------------------------
    不行啊,士气低落中

  5. 老赵
    admin
    链接

    老赵 2009-03-02 09:09:00

    @Mien Ng
    嗯,标题应该再具体一些,意思是某些功能应该交给URL Routing来完成。

  6. xjb
    *.*.*.*
    链接

    xjb 2009-03-02 09:38:00

    学习ing

  7. Tristan G
    *.*.*.*
    链接

    Tristan G 2009-03-02 10:16:00

    如果扩展一个HtmlHelper方法:
    Html.ActionLinkWithDateTime()
    在扩展方面里面格式化日期字符串,是不是也可以对付这个问题呢?

  8. sun@live
    *.*.*.*
    链接

    sun@live 2009-03-02 10:18:00

    public ActionResult Date([DateTime("yyyy-MM-dd")]DateTime date)
    上面这一句,是不是错了?
    public ActionResult Date([DateTimeAttribute("yyyy-MM-dd")]DateTime date)

  9. 老赵
    admin
    链接

    老赵 2009-03-02 10:20:00

    --引用--------------------------------------------------
    sun@live: public ActionResult Date([DateTime("yyyy-MM-dd")]DateTime date)
    上面这一句,是不是错了?
    public ActionResult Date([DateTimeAttribute("yyyy-MM-dd")]DateTime date)
    --------------------------------------------------------
    我没有错,您可以去了解一下Custom Attribute的使用。

  10. 老赵
    admin
    链接

    老赵 2009-03-02 10:20:00

    --引用--------------------------------------------------
    Tristan G: 如果扩展一个HtmlHelper方法:
    Html.ActionLinkWithDateTime()
    在扩展方面里面格式化日期字符串,是不是也可以对付这个问题呢?
    --------------------------------------------------------
    这和直接在view里调用ToString有什么区别呢?还是要强制指定DateTime格式,还是不能使用强类型的Html Helper。而且不是能推而广之的方法。

  11. Tristan G
    *.*.*.*
    链接

    Tristan G 2009-03-02 10:27:00

    @Jeffrey Zhao

    明白。

    hanselman对付datetime的方法竟是把它分割掉

  12. 老赵
    admin
    链接

    老赵 2009-03-02 10:32:00

    @Tristan G
    可能对于DateTime可以这么做,但是很多其他需求是分割不掉的。
    例如,某个搜索用的Query。

  13. Tristan G
    *.*.*.*
    链接

    Tristan G 2009-03-02 20:32:00

    无论如何我也觉得分割不是最佳方法,一样麻烦。

  14. 老赵
    admin
    链接

    老赵 2009-03-02 20:42:00

    --引用--------------------------------------------------
    Tristan G: 无论如何我也觉得分割不是最佳方法,一样麻烦。
    --------------------------------------------------------
    嗯,是的,而且修改起来是麻烦的,恶心的。

  15. 共同学习,共同进步
    *.*.*.*
    链接

    共同学习,共同进步 2009-03-05 20:56:00

    老赵,关于这个demo,有代码提供下载吗?谢谢

  16. 老赵
    admin
    链接

    老赵 2009-03-05 21:07:00

    --引用--------------------------------------------------
    共同学习,共同进步: 老赵,关于这个demo,有代码提供下载吗?谢谢
    --------------------------------------------------------
    代码已经完整贴出了哦。

  17. Cat Chen
    *.*.*.*
    链接

    Cat Chen 2009-04-02 14:09:00

    关于单向的问题,能否通过Extension Method重写ToString()解决?

  18. 老赵
    admin
    链接

    老赵 2009-04-02 14:13:00

    @Cat Chen
    Extension Method只是个编译器的Syntax Sugar而已,它的效果只是体现在“代码”层面上。编译好之后,调用的原本是Object.ToString还是XXX.ToString(this)已经确定的,不会改变的。这不像动态语言了。

  19. leeolevis
    *.*.*.*
    链接

    leeolevis 2009-12-12 13:57:00

    <a href="/News/View/<%=_News. NewsId%>"><%=_News.Color.ToString().Length<1?_News.TitleZ: "<font color=" + _News.Color+ ">" + _News.TitleZ + "</font>" %></a>

    请问老赵您提到尽量多用Model Binder和参数给Controller传值是什么意思?像我这种传值方法是不是不科学呢:)

  20. 老赵
    admin
    链接

    老赵 2009-12-12 14:29:00

    @leeolevis
    我是说服务器端编写Action的方式,不是在说客户端的URL怎么生成。

  21. leeolevis
    *.*.*.*
    链接

    leeolevis 2009-12-14 10:00:00

    我知道,但20楼的确是我的疑惑
    为什么绑定的时候一定要用ActionLink等生成Html标签的方法呢?
    直接写不好吗?不是有些场景不赞成使用自动生成吗?
    Html.RenderPartial("viewName");
    上面的和下面的效果一样,为什么不提倡下面的写法了
    <uc1:Header ID="viewName" runat="server" />
    期待老赵为我解惑:)

  22. 老赵
    admin
    链接

    老赵 2009-12-14 10:24:00

    @leeolevis
    我说的是生成URL,没说Render Partial这些,其实也没有说ActionLink。
    看这里:
    http://www.cnblogs.com/JeffreyZhao/archive/2009/10/29/several-ways-of-generating-url.html

  23. leeolevis
    *.*.*.*
    链接

    leeolevis 2009-12-14 15:40:00

    恩,明白,但是我的问题与标题无关,算是个QA了:)
    目前我只知道html.Encode可以防止JS注入
    但是其他生成html标签的HtmlHelper我还没明白意义所在

  24. 老赵
    admin
    链接

    老赵 2009-12-14 15:42:00

    @leeolevis
    没看我给的链接?

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我