每周源代码38: ModelState.IsValid的属性为False,源于ModelBinder从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怎样出现的吗?如下图,他们大部分与我类下的属性对齐。
不过,注意ID并没有在POST中,它不在其中因为它是恒等的(identity),在我们把Dinner保存到database中后,它会被自动生成。
函数New()把Dinner看成一个参数。Dinner是由系统创建的因为使用了DefaultModeBinderBinder会查看HTTP POST中的值,然后将它们与目标中的属性排成一列。注意POST中没有ID。那为什么ModelState.IsValid的属性是false呢?
如果我去看ModelState.Values,我会看到第一个value写着“需要一个值”,ModelState.Keys告诉我那就是“ID”,""被写入。
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,其中值显示为""。
DefaultModelBinder可以看出ID是可用的,而它被设置为"",这和完全省去又不同。如果它没有,就不会被需要。如果在/controller/action/A设定为“A”,那我就会看到一个完全不同的错误,表述为:值“A”无效因为“A”不是一个整数。
总结
我准备改变我的模式,用Dinner.DinnerID而不是Dinner.ID。这样就不会让Route/URL和模型的属性名称重复使用。多有趣的失误啊!只要检查ASP.NET MVC的源代码就解决了我的问题,太棒了。同时也为ASP.NET MVC团队喝彩,感谢他们把资源做得如此简单易读。