一、背景
在MVC3項目里,如果Action的參數中有Enum枚舉作為對象屬性的話,使用POST方法提交過來的JSON數據中的枚舉值卻無法正確被識別對應的枚舉值。
二、Demo演示
為了說明問題,我使用MVC3項目創建Controller,並且創建如下代碼演示:
//交通方式枚舉 public enum TrafficEnum { Bus = 0, Boat = 1, Bike = 2, } public class Person { public int ID { get; set; } public TrafficEnum Traffic { get; set; } } public class DemoController : Controller { public ActionResult Index(Person p) { return View(); } }
網站生成成功之后,就可以使用Fiddler來發送HTTP POST請求了,注意需要的是,要在Request Headers加上請求頭content-type:application/json,這樣才能通知服務器端Request Body里的內容為JSON格式。
點擊右上角的Execute執行HTTP請求,在程序斷點情況下,查看參數p,屬性ID已經正確的被識別到了值為9999,而枚舉值屬性Traffic卻被錯認為枚舉中的首個值Bus,這儼然是錯誤的,縱使你將Traffic修改成Bike,也就是值等於2,結果也是一樣。
三、解決方法
方法一:
升級MVC4,親測在MVC4項目下,這個問題已經被修復了;
方法二:
假若因為各種原因,項目不想或者不能升級為MVC4,可以在MVC3項目上做些改動,亦可修復這個問題,
1、在項目中,新建一個類,加入以下代碼,需要引用一下 using System.ComponentModel; using System.Web.Mvc; 命名空間;
/// <summary> /// 處理在MVC3下,提交的JSON枚舉值在Controller不能識別的問題 /// </summary> public class EnumConverterModelBinder : DefaultModelBinder { protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder) { var propertyType = propertyDescriptor.PropertyType; if (propertyType.IsEnum) { var providerValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); if (null != providerValue) { var value = providerValue.RawValue; if (null != value) { var valueType = value.GetType(); if (!valueType.IsEnum) { return Enum.ToObject(propertyType, value); } } } } return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder); } }
2、在Global.asax的Application_Start方法中,進行EnumConverterModelBinder類的實例化操作:
protected void Application_Start() { //處理在MVC3下,提交的JSON枚舉值在Controller不能識別的問題 ModelBinders.Binders.DefaultBinder = new EnumConverterModelBinder(); }
進行配置改造之后,我再次生成網站,重新發送HTTP請求看,MVC Action中的參數里的枚舉就能被正確的識別到了。
四、研究
我覺得這應該是mvc3里面一個小小的缺陷吧,隨着mvc的升級,這已經在新版本里被完善修復了,可還用着mvc3的人如果在項目中遇到這個問題,可以研究一下。
遇到一個問題,去百度谷歌找解決方案是可以,但是復制粘貼完代碼之后,最好問下自己,為什么這樣可以解決問題。
從現象和解決代碼中猜想,應該是在MVC生命周期中的Model Binders 這一環節出了問題。
因為MVC已經開源了,所以我嘗試着調試源碼,首先下載MVC3的源碼,其他項目可以移除,只保留紅色框中的項目即可,然后新建一個MVC3測試項目,並且將此測試項目的system.web.mvc引用移除,轉而引用本解決方案中的system.web.mvc 項目,這樣子,我們才可以對MVC源碼進行調試操作。
搜回來的代碼中可知,我們自定義的類繼承DefaultModelBinder父類,並且重寫了GetPropertyValue方法,那我們就從這點開始,在MVC3源碼中的System.Web.MVC項目中找到該類,在此方法上插入斷點。
F5調試程序,發送一個POST請求。
其實BindProperty方法是會被多次執行的,BindProperties方法會對請求的實體類的屬性進行遍歷,每一個屬性都要經過BindProperty方法的處理;
現在已經截獲到第一個屬性ID了。
緊接着,程序進入propertyBinder.BindModel 方法。
只貼部分關鍵代碼了,通過bindingContext的ValueProvider 獲得屬性的相關信息,如果不等於null的話,轉到執行BindSimpleModel 方法。
在BindSimpleModel方法里,首先通過Type.IsInstanceOfType方法判斷確定指定的對象是否是當前 Type 的實例,如果是,則直接返回rawValue,這里的屬性類型是Int32類型,返回True符合條件,所以直接把rawValue給返回去了。
第一個Int32類型屬性的部分關鍵代碼執行到這里就已經確認到值了,接下來,我們看出了問題的Enum枚舉類型屬性。
循環來到了第二個屬性了,這時我留意到有個Model屬性,對比Int32類型執行的時候,這個屬性當時為0,而此時則為Bus,可見這是一個默認值,指定枚舉中值為0的那個類型(即使你不為枚舉顯式指定值),同樣的,經過BindModel方法來到了BindSimpleModel方法。
此時,對比Int32類型的屬性ID,這次ModelType.IsInstanceOfType(valueProvideResult.RawValue)為False,並且接下來不是string類型就執行以下的判斷,也不是數組類型,所以,來到了最后一個,根據綠色的注釋可以看出,這應該是一個判斷是否collection集合類型的方法,Enum都不是,所以,返回了Null。
這時,Type collectionType變量為Null,執行最后一個case 3
在ConvertProviderResult方法里,也進行了一系列的類型判斷轉換,目的就是將JSON中的數字類型轉換成枚舉值,但是執行過程中拋出異常了,原因是
“No type converter can convert between these types ” 也就是說,在MVC3的機制中,並沒有相應的type converter來處理數值與枚舉的對應。
經過以上這些處理方法,都沒完成把對應的值確認下來,怎么給原來的BindProperty 老大方法交差呢,所以,小的只好將Value=Null 和 modelState.Errors 模型錯誤狀態信息如實帶回去了,讓老大決定怎么做,老大后面處理這里有點繞,但是我看源碼估計也是拿默認值來充當Value了,所以就造成了JSON傳過來的值與對應枚舉的值不對應的情況,無論傳什么值,結果都是第一個枚舉的值。
五、總結
這篇文章只是我在工作上遇到的一個小問題,然后有點小興趣就從源碼的角度上來研究和分析,缺乏理論的依據,因為之前沒有很深入的去研究MVC的底層運行機制與生命周期,所以這方面還需要得加強學習一下,如果你也有興趣,可以下載我修改好的源碼來分析一下,甚至可以下載MVC4的源碼來進行對比。
六、后續
感謝各位園友的支持,本文上了昨天博客園的首頁推薦,也有很多園友給出一些非常有用的建議,特此貼出,以饗園友!
@eflay
dotnetgeek回復:文章中,已經提到可以升級MVC4了,但是我今天又發現了另外一個問題,在MVC3中,Dictionary也有問題,而且,升級到MVC4之后,如果Key為int類型的話,會報錯。接下來我會新開一文說說這個問題。你可以做個例子試試看。
@雙調
dotnetgeek回復: 親測確實可以,不過,你不覺得如果由前端傳過來的數據沒有帶引號,就能令MVC內部報異常從而導致數據不正確,是一件很不妥的事情嗎?另外,我昨天也發現Dictionary也會有這樣的問題,並且前端傳過來的數據是帶上了雙引號的,MVC綁定也不能正確的識別,並且在MVC4里面,如果Dictionary的Key如果為int類型的話,JSON(obj)序列化輸出的時候,還會報錯。
ps:關於Dictionary的問題
關於Dictionary其實也有相關的問題,並且在MVC4中還有其他問題,這我應該會新開一文來解一下,這里做個提醒的是:
用Dictionary類型序列化之后的JSON格式字符串,提交給action,action是不認得的,經過在stackoverflow找到了解決方法,原來提交的時候,格式需要特殊處理一下,按照KeyValue鍵值對來的,而非序列化后的格式,各位注意了。
正確被識別Dictionary的JSON: { "ID":"9999","Traffic":"1","Dic":[{"Key":1,"Value":"xyz"},{"Key":2,"Value":42}] }
不能被識別Dictionary的JSON: { "ID":"9999","Traffic":"1","Dic":{"1":"xyz","2":"42"} } 但此格式又是被 JSON(obj) 序列化后的格式。