Hello World
Spiga

使用Model Binder绑定Action参数字段时的取舍问题

2009-09-28 13:57 by 老赵, 12365 visits

刚才在看代码的时候忽然发现了一件可能会成为问题的情况,而这个情况还挺隐蔽的。因此,我原本写到一半的东西就暂时停下,顺延至明天,而现在先来谈谈这个问题。这个问题就是在使用DefaultModelBinder在绑定字段时的取舍问题。而您在使用ASP.NET MVC的时候不妨也检查一下,看看有没有这方面的情况。

ASP.NET MVC的一个便利之处就在于,它可以将Route Value,Form或Query String里的数据自动构造成Actoin方法的参数。例如我们ProductController中有个叫做Create的Action方法:

[AcceptVerbs(HttpVerbs.Post)]
public class ProductController : BaseController
{
    public ActionResult Create(Product product)
    {
        var db = new DataContext();
        db.InsertOnSubmit(product);
        db.SubmitChanges();
    }
}

这是一段使用LINQ to SQL向数据库中插入数据的方法,很简单,您一定也看得懂。ASP.NET MVC的DefaultModelBinder类会根据Product类型的字段,从Post过来的数据中获取一些值,然后构造一个Product对象。例如Product类型是这样定义的:

public class Product
{
    public virtual int ProductID { get; set; }

    public virtual string Name { get; set; }

    public virtual float Price { get; set; }

    public virtual int ViewCount { get; set; }
}

四个字段:ID、名称、价格以及浏览量。当用户访问/Product/Create页面时(不是刚才Post Only的Action,而是用于显示创建页的Action),客户端便会获得这样的form表单:

<form action="/Product/Action" method="post">
    <input type="text" name="Name" />
    <input type="text" name="Price" />
    <input type="submit" />
</form>

当然,实际情况下这个表单是不会那么赤裸裸的,会有文字说明,会有校验逻辑等等。不过它的含义总归是向/Product/Action这个URL中Post两个字段:Name和Price,最后在服务器端执行得到的结果便是创建了一个指定了Name和Price的对象,它的ViewCount是0——为什么是0?因为客户端没有提供数据咯。

问题就在这里,既然客户端没有提供数据,于是ViewCount是0,但如果客户端提供了数据又该怎么办呢?例如,用户完全有能力在客户端多加一个字段,变成这样:

<form action="/Product/Action" method="post">
    <input type="text" name="Name" />
    <input type="text" name="Price" />
    <input type="hidden" name="ViewCount" value="1000" />
    <input type="submit" />
</form>

如此创建出的对象,它的ViewCount自然就直接是1000了。您可能会问,那么用户为什么会知道是ViewCount这个字段?我想,他应该可以从网站其他地方获取蛛丝马迹的,例如某个元素的id,例如某次请求所提交的参数等等。于是,他就可以这么一试了。

写到这儿,我忽然又想到,有些朋友在写form的时候可能会偷一下懒,省略了form的action属性,或者这样构造一个表单:

<% using (Html.BeginForm()) { %>

    ...
    
<% }; %>

如此,form便会提交给当前URL。这样做的问题在于,如果用户通过/Product/Create?ViewCount=1000这样的URL来访问创建页面,这样提交后的数据也会自动填写上ViewCount。

当然这个问题很容易解决,例如在创建Product的Create方法中可以主动为product参数的属性设置默认值,或者使用ASP.NET MVC自带的机制:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create([Bind(Exclude="ViewCount")]Product product)
{
    ...
}

您可以为product参数加上BindAttribute标记,指定哪些字段不需要绑定(或哪些需要),这样即使客户端提交了某些字段,DefaultModelBinder也会忽略这些数据了。

Creative Commons License

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

Add your comment

28 条回复

  1. liyou
    *.*.*.*
    链接

    liyou 2009-09-28 14:04:00

    hihi

  2. liyou
    *.*.*.*
    链接

    liyou 2009-09-28 14:07:00

    问个问题:
    我在博客园上的网页里写随笔,可是文章过长,提交很容易报错。有什么好方法发表随笔吗?

  3. 老赵
    admin
    链接

    老赵 2009-09-28 14:17:00

    @liyou
    用Windows LIve Writer,博客园里用TextBox。

  4. CoolCode
    *.*.*.*
    链接

    CoolCode 2009-09-28 14:27:00

    确实有时写着写着会忘记加Attribute标记。

  5. 初始小花
    *.*.*.*
    链接

    初始小花 2009-09-28 14:31:00

    前排占个座再看。

  6. deerchao
    *.*.*.*
    链接

    deerchao 2009-09-28 14:35:00

    这个ViewCount的值本来就应该在创建产品的业务逻辑里检查的吧?
    而且,这里也可以不用领域模型的Type,而使用UI层专用的类型,可以更本就没有ViewCount这个属性。

  7. 老赵
    admin
    链接

    老赵 2009-09-28 14:38:00

    @deerchao
    总之要检查就是了。
    至于用UI层专用的类型,是有这种说法,不过这样就可能要对许多操作搞一下这样的东西,比较复杂。
    从概念上来说Create(Product)也是很成立的。
    所以,还是绑定时注意点,以及校验吧。

  8. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-09-28 15:12:00

    如果忽略了BindAttribute,岂不是很容易被注入了

  9. 老赵
    admin
    链接

    老赵 2009-09-28 15:13:00

    @麒麟.NET
    什么注入?和注入有什么关系?

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

    麒麟.NET 2009-09-28 15:45:00

    通过/Product/Create?ViewCount=1000这样的方式给不想赋值的字段赋值啊

  11. 老赵
    admin
    链接

    老赵 2009-09-28 15:46:00

    @麒麟.NET
    还是不要随意起名字比较好,要起也起一个其他地方没有用过的,否则沟通上容易混乱。

  12. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-09-28 16:02:00

    @Jeffrey Zhao
    呵呵,好好,下次注意

  13. 5254341[未注册用户]
    *.*.*.*
    链接

    5254341[未注册用户] 2009-09-28 16:44:00

    我一般是这样子来做的

    [Bind(Exclude="ViewCount")]
    public class Product
    {
    public virtual int ProductID { get; set; }

    public virtual string Name { get; set; }

    public virtual float Price { get; set; }

    public virtual int ViewCount { get; set; }
    }

  14. 老赵
    admin
    链接

    老赵 2009-09-28 16:48:00

    @5254341
    这也可以啊。
    不过这样不妥当吧。
    一是Model依赖asp.net mvc了。
    二是这种全局指定似乎过于强大了,因为很可能另一个Action就需要ViewCount了。

  15. 5254341[未注册用户]
    *.*.*.*
    链接

    5254341[未注册用户] 2009-09-28 16:50:00

    为什么加 virtual 修饰符?
    ————————————————————————————
    public class Product
    {
    public virtual int ProductID { get; set; }

    public virtual string Name { get; set; }

    public virtual float Price { get; set; }

    public virtual int ViewCount { get; set; }
    }

  16. 老赵
    admin
    链接

    老赵 2009-09-28 16:52:00

    @5254341
    我用了NHibernate。

  17. 5254341[未注册用户]
    *.*.*.*
    链接

    5254341[未注册用户] 2009-09-28 16:53:00

    Jeffrey Zhao:
    @5254341
    这也可以啊。
    不过这样不妥当吧。
    一是Model依赖asp.net mvc了。
    二是这种全局指定似乎过于强大了,因为很可能另一个Action就需要ViewCount了。


    -----------------------
    确实是这样子的,不过要看情况的,如果在Action中加的话也比较麻烦

  18. 5254341[未注册用户]
    *.*.*.*
    链接

    5254341[未注册用户] 2009-09-28 16:55:00

    @Jeffrey Zhao

    Jeffrey Zhao:
    @5254341
    这也可以啊。
    不过这样不妥当吧。
    一是Model依赖asp.net mvc了。
    二是这种全局指定似乎过于强大了,因为很可能另一个Action就需要ViewCount了。


    ---------------------
    我猜也是这样子的,前2天刚看了你这篇文章
    "我对NHibernate的感受(2):何必到处都virtual"

  19. 王德水
    *.*.*.*
    链接

    王德水 2009-09-28 21:10:00

    呵呵,我也忽略了exclude问题。

  20. 小-G
    *.*.*.*
    链接

    小-G 2009-09-29 12:25:00

    数据库操作会在Service中完成,所以我是直接在Service的相应方法中为不需要用户指定的值赋予默认值,比如:
    public bool Create(Product p)
    {
    ...
    p.VoteCount = 0;
    ...
    }

  21. 老赵
    admin
    链接

    老赵 2009-09-29 13:05:00

    @小-G
    那数据访问层就有逻辑了,我倾向于在数据访问层里不放任何和业务有关的逻辑,就是负责对象的持久化。
    // 你的Service层是数据访问层吗?

  22. 小-G
    *.*.*.*
    链接

    小-G 2009-09-29 13:08:00

    @Jeffrey Zhao
    数据访问还是有Linq to sql处理的,我喜欢让Controller里面更干净一些,所以Controller里调用Service,Service再调用数据访问

  23. 老赵
    admin
    链接

    老赵 2009-09-29 13:12:00

    @小-G
    那你的Service是数据访问层,你这么做是很合理的。

  24. 老赵
    admin
    链接

    老赵 2009-09-29 13:13:00

    @小-G
    那你的Service是业务逻辑层,不数据访问层,你这么做是很合理的。

  25. 胖胖鱼
    *.*.*.*
    链接

    胖胖鱼 2009-10-19 20:03:00

    请教老赵:

    mvc中在强类型的页面中,dropdownlist显示<GUID,string>的selectlist键值对正常,但是提交的时候说无法强制转换System.GUID为System.String,请问是什么原因?如何解决?

  26. 老赵
    admin
    链接

    老赵 2009-10-19 20:12:00

    @胖胖鱼
    我不清楚啊,不过你不妨试试看为Guid写一个转化为String的Model Binder?

  27. 杜杜
    125.107.216.*
    链接

    杜杜 2010-06-06 21:33:04

    [Bind(Exclude="ViewCount")] 在哪个命名空间下?

    找不到

    QQ 670608809 bratdu@gmail.com谢谢

  28. 杜杜
    125.107.216.*
    链接

    杜杜 2010-06-06 21:35:47

    知道了!!

    哥们让我多写点!!

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我