每周源代码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团队喝彩,感谢他们把资源做得如此简单易读。