每周源代码38: ModelState.IsValid的属性为False,源于ModelBinder从RouteData获取值


[原文发表地址]   The Weekly Source Code 38 - ASP.NET MVC Beta Obscurity - ModelState.IsValid is False because ModelBinder pulls values from RouteData

[原文发表时间]  2008-12-04  06:41 AM

我在我正做的应用程序里发现了一个错误。我不知道那是ASP.NET MVC Framework里的一个错误还是一个新特征。不过我知道,他们的代码和我的都精准地按照编写的在运行。

首先是我看到的状态,然后是一大堆没有必要的技术背景(因为我喜欢听我自己讲解),最后则是我的总结。不管怎样,很有趣!

更新:我彻底被MVC团队愚弄/打败了,他们尖锐地指出和15年前Ayende提出的完全一样。它是促使修复/变更了,新的状态把顺序调整为3,1,2,我是这么理解的。我表示惭愧。它在Release Candidate中被彻底修复,所以此篇博文仅为我个人的CSI:ASPNETMVC。

状态

我的应用在Dinners中是做CRUD的(创建,读取,更新和删除)。你进入一个新的Dinner目标时,你要填个表格并公布你的HTML。

我们在Dinner中读取并储存(我移除了goo 这样看起来比较清楚):

 1: [AcceptVerbs(HttpVerbs.Post)]

 2: [Authorize]

 3: public ActionResult New([Bind(Prefix = "")]Dinner item)

 4: { 

 5: if (ModelState.IsValid)

 6: {

 7: item.UserName = User.Identity.Name;

 8: _dinnerRepository.Add(item);

 9: _dinnerRepository.Save();

 10: TempData["Message"] = item.Title + " Created";

 11: return RedirectToAction("List");

 12: }

 13: }

我所看到的状态是ModelState.IsValid的属性总是False。

注意,当时我不太聪明以至于没有深入ModelState目标去探寻到底是为什么。之后更显示了我的愚笨。

内容

公布的表格是这样的:

Title=Foo&EventDate=2008-12-10&EventTime

=2008-21-10%EventTime=21%3A50&

Description=Bar&HostedBy=shanselman&

LocationName=SUBWAY&MapString=1050+SW

+Baseline+St+Ste+A1%2C+Hillsboro%2C+OR

&ContactPhone=%28503%29+601-0307&

LocationLatitude=45.519978&

LocationLongitude=-123.001934

看到成对的大堆Name/Value怎样出现的吗?如下图,他们大部分与我类下的属性对齐。

Dinner Class Diagram

不过,注意ID并没有在POST中,它不在其中因为它是恒等的(identity),在我们把Dinner保存到database中后,它会被自动生成。

函数New()把Dinner看成一个参数。Dinner是由系统创建的因为使用了DefaultModeBinderBinder会查看HTTP POST中的值,然后将它们与目标中的属性排成一列。注意POST中没有ID。那为什么ModelState.IsValid的属性是false呢?

如果我去看ModelState.Values,我会看到第一个value写着“需要一个值”,ModelState.Keys告诉我那就是“ID”,""被写入。

Watch Window showing error in ModelState.Values

MVC是从哪里获得ID的,为什么是""呢?我不需要ID,我在制作一个Dinner,并不是在编辑。

结果是DefaultValueProvider提供了值,而DefaultModelBinder则在一些地方寻找它的值。从CodePlex上的资源看,注意这些附注:

 1: public virtual ValueProviderResult GetValue(string name) {

 2: if (String.IsNullOrEmpty(name)) {

 3: throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name");

 4: }

 5:  

 6: // Try to get a value for the parameter. We use this order of precedence:

 7:  

 8: // 1. Values from the RouteData (could be from the typed-in URL or from the route's default values)

 9:  

 10: // 2. URI query string

 11:  

 12: // 3. Request form submission (should be culture-aware)

 13:  

 14:  

 15: object rawValue = null;

 16: CultureInfo culture = CultureInfo.InvariantCulture;

 17: string attemptedValue = null;

 18:  

 19: if (ControllerContext.RouteData != null && ControllerContext.RouteData.Values.TryGetValue(name, out rawValue)) {

 20: attemptedValue = Convert.ToString(rawValue, CultureInfo.InvariantCulture);

 21: }

 22: else {

 23: HttpRequestBase request = ControllerContext.HttpContext.Request;

 24: if (request != null) {

 25: if (request.QueryString != null) {

 26: rawValue = request.QueryString.GetValues(name);

 27: attemptedValue = request.QueryString[name];

 28: }

 29: if (rawValue == null && request.Form != null) {

 30: culture = CultureInfo.CurrentCulture;

 31: rawValue = request.Form.GetValues(name);

 32: attemptedValue = request.Form[name];

 33: }

 34: }

 35: }

 36:  

 37: return (rawValue != null) ? new ValueProviderResult(rawValue, attemptedValue, culture) : null;

 38: }

好像我的的假定——Form POST 是Model Binder唯一会去查看的地方其实是错误的,它会去三个地方:

1. RouteData中的值

2. URI查询字符串

3. 申请的提交(需要被重视)

当然强调是我个人意愿。到这里我知道我看到的不是一个错误,而是我过于普通的命名的副作用。我在Dinner上创建了一个ID属性,但在Global.asax.cs中也有默认路径。

 1: routes.MapRoute(

 2: "Default", // Route name

 3: "{controller}/{action}/{id}", // URL with parameters 

 4: new { controller = "Home", action = "List", id = "" } // Parameter defaults

 5: );

注意ID的默认值。这可以在调试器中确认,就像我以前使用断点一样。RouteData选集显示了ID的name/value,其中值显示为""。

Debuger showing the RouteData object

DefaultModelBinder可以看出ID是可用的,而它被设置为"",这和完全省去又不同。如果它没有,就不会被需要。如果在/controller/action/A设定为“A”,那我就会看到一个完全不同的错误,表述为:值“A”无效因为“A”不是一个整数。

总结

我准备改变我的模式,用Dinner.DinnerID而不是Dinner.ID。这样就不会让Route/URL和模型的属性名称重复使用。多有趣的失误啊!只要检查ASP.NET MVC的源代码就解决了我的问题,太棒了。同时也为ASP.NET MVC团队喝彩,感谢他们把资源做得如此简单易读。

Comments (0)

Skip to main content