天天看點

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

模型綁定(Model Binding)是使用浏覽器發起Http請求時的資料建立.NET對象的過程。我們每一次定義帶參數的action方法時就已經依靠了模型綁定——這些參數對象是通過模型綁定建立的。這一章會介紹模型綁定的原理以及針對進階使用必要的定制模型綁定的技術。

了解模型綁定(Understanding Model Binding)

想象下我們建立了一個控制器如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

View Code

action方法定義在HomeController類裡面,VS預設建立的路由就是調用這裡的action方法。當我們請求一個如/Home/Person/23的URL,MVC架構會将請求的詳細資訊映射通過一種傳遞合适的值或對象作為參數的方式映射到action方法。action調用者負責在調用action之前擷取這些值,預設的action調用者ControllerActionInvoker依賴于Model Binders,它們是通過IModelBinder接口定義的,如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

在MVC程式裡面可以有多個model binders,每一個binder可以綁定一個或多個model類型。當action調用者需要調用一個action方法,它會尋找定義在方法裡面的參數并且找到對應負責每一個參數類型的model binder。在最開始的例子裡面,action調用者會發現我們的action方法具有一個int型的參數,是以它會定位到負責綁定int值的binder并調用自己的BindModel方法,如果沒有能夠處理int值的binder,那麼預設的model binder會被使用。

model binder是用來生成比對action方法的參數值,這通常意味着傳遞一些請求元素的資料(例如form或query string值),但是MVC架構不會對如何擷取這些值有任何限制。

使用預設的Model Binder(Using the Default Model Binder)

盡管一個應用程式有多個binders,大多數都是依賴于内置的binder類——DefaultModelBinder。這也是當action調用者找不到自定義的binder時使用的binder。預設情況下,這個model binder搜尋了4個路徑,如下所示:

Request.Form:HTML表單提供的值

RouteData.Values:使用應用程式路由擷取的值

Request.QueryString:包含在URL的請求字元串裡面的資料

Request.Files:作為請求部分被上傳的檔案

上面四個路徑是按順序搜尋的,例如在上面的例子中,action方法需要一個參數id,DefaultModelBinder會檢查action方法并尋找名為id的參數。它會按下面的順序來尋找:

1.  Request.Form["id"]

2.  RouteData.Values["id"]

3.  Request.QueryString["id"]

4.  Request.Files["id"]

隻要有一個值找到,搜尋就會停止。

綁定簡單類型(Binding to Simple Types)

當處理簡單的參數類型時,DefaultModelBinder會試圖使用System.ComponentModel.TypeDescriptor類将request資料(字元串型)轉換為對應action方法參數的類型。如果這個值不能轉換,那麼DefaultModelBinder将不能夠綁定到model。如果要避免這個問題,可以修改下參數,如:public ViewResult RegisterPerson(int? id) {...},這樣修改以後,如果不能比對,參數的值會為null。還可以提供一個預設值如:public ViewResult RegisterPerson(int id = 23) {...}

綁定複雜類型(Binding to Complex Types)

如果action方法參數是一個複雜類型(就是不能使用TypeConverter轉換的類型),那麼DefaultModelBinder會使用反射擷取公共的屬性并輪流綁定每一個屬性。使用前面的Person.cs來舉例,如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

預設的model binder會檢查這個類的屬性是否都是簡單類型,如果是,binder就會在請求裡面具有相同的名稱的資料項。對應例子來說就是FirstName屬性會引起binder尋找一個名為FirstName的資料項。如果這個類的屬性(如Address)仍然是個複雜類型,那麼對這個類型重複上面的處理過程。在尋找Line1屬性的值時,model binder會尋找HomeAddress.Line1的值。

指定自定義的字首(Specifying Custom Prefixes)

當預設的model binder尋找對應的資料項時,我們可以指定一個自定義的字首。這對于在HTML裡包含了額外的model對象時非常有用。舉例如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

我們使用了EditorFor helper方法來對Person對象生成HTML,lambda表達式的輸入是一個model對象(用m代替),當使用這種方式以後,生成的HTML元素的屬性名會有一個字首,這個字首來源于我們在EditorFor裡面的變量名myPerson。運作以後可以看到頁面源代碼如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

public ActionResult Index(Person firstPerson,Person myPerson){...},第一個參數對象使用沒有字首的資料綁定,第二個參數尋找以參數名開頭的資料綁定。

如果我們不想用這種方式,可以使用Bind特性來指定,如下:

public ActionResult Register(Person firstPerson, [Bind(Prefix="myPerson")] Person secondPerson)

這樣就設定了Prefix屬性的值為myPerson,這意味着預設的model binder将使用myPerson作為資料項的字首,即使這裡第二個參數的名為secondPerson。

有選擇的綁定屬性(Selectively Binding Properties)

想象一下如果Person類的IsApproved屬性是非常敏感的資訊,我們能夠通過模版綁定來不呈現該屬性,但是一些惡意的使用者可以簡單的在一個URL裡附加?/IsAdmin=true後來送出表單。如果這種情況發生,model binder在綁定的過程會識别并使用這個資料的值。幸運的是,我們可以使用"Bind"特性來從綁定過程包含或排除model的屬性。具體的示例如下:

public ActionResult Register([Bind(Include="FirstName, LastName")] Person person) {...}//僅僅包含Person屬性裡面的FirstName和LastName屬性

public ActionResult Register([Bind(Exclude="IsApproved, Role")] Person person) {...}//排除了IsApproved屬性

上面這樣使用Bind僅僅是針對單個的action方法,如果想将這種政策應用到所有控制器的所有action方法,可以在model類本身使用該特性,如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

這樣就會在所有的用到給model的action方法生效。

注:如果Bind特性被應用到model類并且也在action方法的參數中使用,在沒有其他的應用程式特性排除它時會被包含在綁定過來裡。這意味着應用到model的類的政策不能通過應用一個較小限制政策到action方法參數來重寫。下面用示例說明:

首先添加一個Model Person如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

對Person類添加了Bind特性,排除了IsApproved屬性,然後添加Controller如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

最後添加兩個涉及的視圖PersonEdit和PersonDisplay,如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

運作程式如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

另外,我們在URL裡面添加?IsApproved=true試試看有什麼效果:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

接着繼續測試,剛才不是有說到關于政策重寫的問題嗎,這裡我們對【HttpPost】的Index action的參數添加一個Bing特性如下:

理論上這裡的是沒有辦法對Person上應用的政策進行重寫的,有圖為證:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

綁定到數組和集合(Binding to Arrays and Collections)

處理具有相通名字的多條資料項是預設的model binder的一個非常優雅的功能,示例說明如下:

建立兩個視圖Movies和MoviesDisplay,如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

添加對應的action,如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

model binder會尋找使用者送出的所有值并把它們通過List<string>集合傳遞到Movies action方法,binder是足夠的聰明的識别不同的參數類型,例如我們可以将List<string>改成IList<string>或是string[]。

綁定到自定義類型的集合(Binding to Collections of Custom Types)

上面的多個值的綁定技巧非常好用,但如果我們想應用到自定義的類型,就必須用一種合适的格式來生成HTML。添加MPerson視圖和MPersonDisplay視圖如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

添加Controller,如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

運作程式可以看到效果,要綁定這些資料,我們僅僅定義了一個action并接收一個視圖model類型的集合參數,如:

[HttpPost]

public ViewResult Register(List<Person> people) {...}

因為我們綁定到一個集合,預設的model binder會搜尋用一個索引做字首的Person類的屬性。當然,我們不必使用模版化的helper方法來生成HTML,可以顯示地在視圖裡面做,如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

隻要我們保證了索引值被恰當的建立,model binder會找到并綁定所有定義的資料元素。

使用非線性的索引綁定到集合(Binding to Collections with Nonsequential Indices)

除了上面使用數字序列的索引值外,還可以使用字元串來作為鍵值,這在當我們想要使用js在用戶端動态的添加或移除控件時非常有用,而且不用去維護索引的順序。采用這種方式需要定義一個hidden input元素name為指定key的index。如下:

我們用input元素的字首來比對index隐藏域的值,model binder會檢測到index并使用它在綁定過程中關聯資料的值。

綁定到一個Dictionary(Binding to a Dictionary)

預設的model binder是能夠綁定到一個Dictionary的,但是隻有當我們遵循一個非常具體的命名序列時才行。如下:

此時可以使用如下的action來擷取值

public ViewResult Register(IDictionary<string, Person> people) {...}

手動調用模型綁定(Manually Invoking Model Binding)

模型綁定的過程是在一個action方法定義了參數時自動執行的,但是可以直接控制這個過程。這給了我們對于model對象如何執行個體化,資料的值從哪裡擷取,以及資料強制轉換錯誤如何處理等更多明确的控制權。示例如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

UpdateModel方法擷取一個model對象作為參數并試圖使用标準綁定過程擷取model對象裡面公共屬性的值。手動調用model綁定的其中一個原因是為了支援DI。例如,如果我們使用了一個應用程式範圍的依賴解析器,那麼我們能夠添加DI到這裡的Person對象的建立,如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

正如我們闡釋的,這不是在綁定過程引入DI的唯一方式,後面還會介紹其他的方式。

将綁定限制到指定的資料源(Restricting Binding to a Specific Data Source)

當我們手動的調用綁定時,可以限制綁定到指定的資料源。預設情況下,bingder會尋找四個地方:表單資料,路由資料,querystring,以及上傳的文體。下面例子說明如何限制綁定到單個資料源——表單資料。修改action方法如下:

這裡的UpdateModel是重載的版本接收一個IValueProvider接口實作作為參數,進而指定了綁定過程的資料源。每一個預設的資料源都對應了一個對該接口的實作,如下:

1.Request.Form——>FormValueProvider

2.RouteData.Values——>RouteDataValueProvider

3.Request.QueryString——>QueryStringValueProvider

4.Request.Files——>HttpFileCollectionValueProvider

最常用的現在資料源的方式就是隻在尋找Form裡面的值,有一個非常靈巧的綁定技巧,以至于我們不用建立一個FormValueProvider的執行個體,如下:

FormCollection類實作了IValueProvider接口,并且如果我們定義的action方法接收一個該類型的參數,model binder會提供一個可以直接傳遞給UpdateModel方法的對象。

處理綁定錯誤(Dealing with Binding Errors)

使用者難免會送出一些不能綁定到相應的model屬性的值,如未驗證的日期或文本當成數值。下一章會介紹相關的綁定驗證的内容,這裡在使用UpdateModel方法時,我們必須準備捕獲處理相關的異常,并使用ModelState向使用者提示錯誤的資訊,如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

除了try...catch之外,還可以使用TryUpdateModel()方法,它的傳回值是bool值,如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

使用模型綁定接收檔案上傳(Using Model Binding to Receive File Uploads)

為了接收上傳的檔案,需要定義一個action方法并接收一個HttpPostedFileBase類型的參數。然後,model binder将會使用跟上傳的檔案一緻的資料填充這個參數。如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

這裡的關鍵是要設定enctype屬性的值為"multipart/form-data".如果不這樣做,浏覽器隻會發送檔案名而不是檔案本身(這是浏覽器的運作原理決定的).

自定義模型綁定系統(Customizing the Model Binding System)

前面介紹都是預設的模型綁定系統,我們同樣可以定制自己的模型綁定系統,下面會展示一些例子:

建立一個自定義的Value Provider

通過定義一個value provider,我們可以在模型綁定過程添加自己的資料源。value providers實作IValueProvider接口,如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

我們隻響應針對CurrentTime的請求,并當接收到這樣的請求時,傳回DateTime.Now屬性的值,對其他的請求,傳回null,表示不能提供資料。我們必須将資料作為ValueProviderResult類型傳回。為了注冊自定義的Value Provider,我們需要建立一個用來産生Provider執行個體的工廠,這個類從ValueProviderFactory派生,如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

通過向ValueProviderFactories.Factories集合裡面添加一個執行個體來注冊我們自己的工廠,model binder 會按順序尋找value provider,如果想讓我們的value provider優先,可以插入序号0,就像上面的代碼中寫的。如果想放在最後可以直接這樣添加:ValueProviderFactories.Factories.Add(new CurrentTimeValueProviderFactory()); 可以測下我們自己的Value Provider,添加一個Action方法如下:

建立一個依賴感覺的Model Binder(Creating a Dependency-Aware Model Binder)

前面有介紹過使用手動模型綁定引入依賴注入到綁定過程,但是還有一種更加優雅的方式,就是通過從DefaultModelBinder派生來建立一個DI敏感的binder并且重寫CreateModel方法,如下所示:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

接着需要注冊該binder,如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

建立一個自定義的Model Binder

我們能夠通過建立一個針對具體類型的自定義model binder來重寫預設的binder行為,如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

下面我一步步來解析這段代碼,首先我們擷取将要綁定的model對象如下:

Person model = (Person)bindingContext.Model ?? (Person)DependencyResolver.Current.GetService(typeof(Person));

當model binding過程被手動調用時,我們傳遞一個model對象到UpdateModel方法;該對象通過BindingContext類的Model屬性是可用的,一個好的model binder會檢查一個model 對象是否是可用的并且隻有當它是可以的時候才會被用于綁定過程,否則我們就需要負責建立一個model對象,并使用應用程式範圍級别的依賴解析器(第10章有介紹)

接着看我們是否需要使用一個字首請求來自value provider的資料:

bool hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName);

string searchPrefix = (hasPrefix) ? bindingContext.ModelName + "." : "";

BindingContext.ModelName屬性傳回綁定的model的名稱,如果我們在視圖裡呈現這個model對象,生成的HTML不會有字首,但是ModelName都要傳回Action方法的參數名,是以我們檢查value provider的值字首是否存在。我通過BindingContext.ValueProvider屬性通路value providers,這給了我們一個統一的方式來通路所有可用的value providers,并且請求按順序傳遞給它們。如果value data裡面存在字首則使用。

接着我們使用value providers擷取Person對象的屬性值,如下:

model.FirstName = GetValue(bindingContext, searchPrefix, "FirstName");

我們定義了一個GetValue的方法從統一的value provider擷取ValueProviderResult對象并且通過AttemptedValue屬性提取一個字元串值。

在前面有提到過當呈現一個CheckBox時,HTML helper方法建立一個hidden input元素來保證我們能夠擷取一個沒有選中的值,這會稍微對Model綁定有一些影響,因為value provider将會把兩個值作為字元串數組提供給我們。

為了解決這個問題,我們使用ValueProviderResult.ConvertTo方法來協調并給出正确的值:

result = (bool)vpr.ConvertTo(typeof(bool));

接着注冊model binder: ModelBinders.Binders.Add(typeof(Person), new PersonModelBinder());

建立Model Binder提供程式(Creating Model Binder Providers)

一種注冊自定義的model binders替代的方式就是通過實作IModelBinderProvider接口來建立一個model binder provider,如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

這種方式更加靈活,特别是在我們有多個自定義的binders或多個providers維護時。接着注冊剛建立的provider:

ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());

使用ModelBinder屬性(Using the ModelBinder Attribute)

還有最後一種注冊自定義model binder的方式就是使用ModelBinder特性到model類,如下:

《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】
《Pro ASP.NET MVC 3 Framework》學習筆記之三十【模型綁定】

ModelBinder特性具有的單個參數讓我們指定綁定對象的類型,在這個三種方式中,我們傾向于實作IModelBinderProvider接口來處理負責的需求,當然這三種方式最終實作的效果都一樣,是以選擇哪一個都可以。

好了,今天的筆記就到這裡,下一次是關于模型驗證(Model Validation)的内容,因為最近比較忙,是以随筆的時間間隔比較大了,我盡量抓緊時間寫吧,:-)

繼續閱讀