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


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

理解模型綁定(Understanding Model Binding)

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

View Code
using System; 
using System.Web.Mvc; 
using MvcApp.Models; 
 
namespace MvcApp.Controllers 
{  
    public class HomeController : Controller 
   { 
        public ViewResult Person(int id) 
       { 
            // 獲取一條person記錄
            Person myPerson = null; 
           //檢索數據的邏輯...
            return View(myPerson); 
        } 
    } 
}

action方法定義在HomeController類里面,VS默認創建的路由就是調用這里的action方法。當我們請求一個如/Home/Person/23的URL,MVC框架會將請求的詳細信息映射通過一種傳遞合適的值或對象作為參數的方式映射到action方法。action調用者負責在調用action之前獲取這些值,默認的action調用者ControllerActionInvoker依賴於Model Binders,它們是通過IModelBinder接口定義的,如下:

View Code
namespace System.Web.Mvc 
{  
    public interface IModelBinder 
    { 
        object BindModel(ControllerContext controllerContext,  
            ModelBindingContext bindingContext); 
    } 
} 

在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來舉例,如下:

View Code
public class Person 
{ 
    [HiddenInput(DisplayValue=false)] 
    public int PersonId { get; set; } 
 
    public string FirstName { get; set; } 
    public string LastName { get; set; } 
 
    [DataType(DataType.Date)] 
    public DateTime BirthDate { get; set; } 
    public Address HomeAddress { get; set; } 
    public bool IsApproved { get; set; } 
    public Role Role { get; set; } 
} 

public class Address 
{ 
    public string Line1 { get; set; } 
    public string Line2 { get; set; } 
    public string City { get; set; } 
    public string PostalCode { get; set; } 
    public string Country { get; set; } 
}

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

指定自定義的前綴(Specifying Custom Prefixes)

當默認的model binder尋找對應的數據項時,我們可以指定一個自定義的前綴。這對於在HTML里包含了額外的model對象時非常有用。舉例如下:

View Code
@using MvcApp.Models; 
@model MvcApp.Models.Person 
 
@{ 
    Person myPerson = new Person() { 
        FirstName = "Jane", LastName = "Doe" 
    }; 
} 
 
@using (Html.BeginForm()) { 
      
     @Html.EditorFor(m => myPerson) 
     @Html.EditorForModel() 
 
    <input type="submit" value="Submit" /> 
} 

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


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類本身使用該特性,如下:

View Code
    [Bind(Exclude = "IsApproved")]
    public class Person
    {

        [HiddenInput(DisplayValue = false)]
        public int PersonId { get; set; }

        public string FirstName { get; set; }
        public string LastName { get; set; }

        [DataType(DataType.Date)]
        public DateTime BirthDate { get; set; }
        public Address HomeAddress { get; set; }
        public bool IsApproved { get; set; }
        public Role Role { get; set; }

        public DateTime CurrentTime { get; set; }

    }

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

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

首先添加一個Model Person如下:

View Code
using System.Web.Mvc;
using System.ComponentModel.DataAnnotations;

namespace ModelBinding.Models
{
    [Bind(Exclude = "IsApproved")]
    public class Person
    {

        [HiddenInput(DisplayValue = false)]
        public int PersonId { get; set; }

        public string FirstName { get; set; }
        public string LastName { get; set; }

        [DataType(DataType.Date)]
        public DateTime BirthDate { get; set; }
        public Address HomeAddress { get; set; }
        public bool IsApproved { get; set; }
        public Role Role { get; set; }

        public DateTime CurrentTime { get; set; }

    }

    public class Address
    {
        public string Line1 { get; set; }
        public string Line2 { get; set; }
        public string City { get; set; }
        public string PostalCode { get; set; }
        public string Country { get; set; }
    }

    public enum Role
    {
        Admin,
        User,
        Guest
    }

}

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

View Code
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            Person myPerson = new Person
            {
                PersonId = 1,
                FirstName = "Joe",
                LastName = "Smith",
                BirthDate = DateTime.Parse("1988/12/01"),
                HomeAddress = new Address
                {
                    Line1 = "123 North Street",
                    Line2 = "West Bridge",
                    City = "London",
                    Country = "UK",
                    PostalCode = "WC2R 1SS"
                },
                IsApproved = true,
                Role = Role.User
            };
            return View("PersonEdit", myPerson);
        }

        [HttpPost]
        public ActionResult Index(Person person, Person myPerson)
        {
            return View("PersonDisplay", person);
        }
    }

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

View Code
//PersonEdit.cshtml
@using ModelBinding.Models;
@model ModelBinding.Models.Person
<style type="text/css">
    .check-box
    {
        margin: 0.5em 0 0 0;
    }
</style>
@{
    Person myPerson = new Person()
    {
        FirstName = "xuefei",
        LastName = "zhang"
    };
}
@using (Html.BeginForm())
{ 
    @Html.EditorFor(m => myPerson)
    @Html.EditorForModel()
    <input type="submit" value="Submit" />
}

//PersonDisplay.cshtml
@model ModelBinding.Models.Person
<div class="column">
    @Html.DisplayForModel()
</div>
<div class="column">
    @Html.DisplayFor(m => m.HomeAddress)
</div>

運行程序如下:

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



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

[HttpPost]
public ActionResult Index([Bind(Include = "IsApproved")]Person person, Person myPerson)
{
    return View("PersonDisplay", person);
}

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

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

處理具有相通名字的多條數據項是默認的model binder的一個非常優雅的功能,示例說明如下:
創建兩個視圖Movies和MoviesDisplay,如下:

View Code
@*Movies*@
@{
    ViewBag.Title = "Movies";
}
輸入三部你最喜愛的影片名:
@using (Html.BeginForm())
{ 
    @Html.TextBox("movies")
    @Html.TextBox("movies")
    @Html.TextBox("movies")
    <input type="submit" />
}

@*MoviesDisplay*@
@model List<string>
@{
    ViewBag.Title = "MoviesDisplay";
}
你最喜愛的電影:
@foreach (string movie in Model)
{
    <p>@movie</p>
}

添加對應的action,如下:

View Code
public ViewResult Movies()
{
     return View();
}

[HttpPost]
public ViewResult Movies(List<string> movies)
{
     return View("MoviesDisplay", movies);
}

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

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

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

View Code
@*MPerson.cshtml*@
@model List<ModelBinding.Models.Person>
@{
    ViewBag.Title = "MPerson";
}
@using (Html.BeginForm())
{ 
    <h4>
        First Person</h4>
    <input type="hidden" name="[0].key" value="firstPerson" />
    @:First Name:@Html.TextBox("[0].value.FirstName")
    @:Last Name:@Html.TextBox("[0].value.LastName")

    <h4>
        Second Person</h4>
    <input type="hidden" name="[1].key" value="secondPerson" />
    @:First Name:@Html.TextBox("[1].value.FirstName")
    @:Last Name:@Html.TextBox("[1].value.LastName")

    <input type="submit" />
}

@*MPersonDisplay*@
@using ModelBinding.Models;
@model IDictionary<string, ModelBinding.Models.Person>
@foreach (string key in Model.Keys)
{
    @Html.DisplayFor(m => m[key]);
}

添加Controller,如下:

View Code
        public ViewResult MPerson()
        {
            List<Person> people = new List<Person> { 
            new Person{FirstName="xuefei",LastName="zhang"},
            new Person{FirstName="si",LastName="Li"}
            };
            return View(people);
        }

        [HttpPost]
        public ViewResult MPerson(IDictionary<string, Person> people)
        {
            return View("MPersonDisplay", people);
        }

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

[HttpPost]
public ViewResult Register(List<Person> people) {...}
因為我們綁定到一個集合,默認的model binder會搜索用一個索引做前綴的Person類的屬性。當然,我們不必使用模版化的helper方法來生成HTML,可以顯示地在視圖里面做,如下:

View Code
<h4>First Person</h4> 
First Name: @Html.TextBox("[0].FirstName") 
Last Name: @Html.TextBox("[0].LastName") 
 
<h4>Second Person</h4> 
First Name: @Html.TextBox("[1].FirstName") 
Last Name: @Html.TextBox("[1].LastName") 

只要我們保證了索引值被恰當的創建,model binder會找到並綁定所有定義的數據元素。

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

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

<h4>First Person</h4> 
<input type="hidden" name="index" value="firstPerson"/> 
First Name: @Html.TextBox("[firstPerson].FirstName") 
Last Name: @Html.TextBox("[firstPerson].LastName") 
 
<h4>Second Person</h4> 
<input type="hidden" name="index" value="secondPerson"/> 
First Name: @Html.TextBox("[secondPerson].FirstName") 
Last Name: @Html.TextBox("[secondPerson].LastName") 

我們用input元素的前綴來匹配index隱藏域的值,model binder會檢測到index並使用它在綁定過程中關聯數據的值。

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

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

<h4>First Person</h4> 
<input type="hidden" name="[0].key" value="firstPerson"/> 
First Name: @Html.TextBox("[0].value.FirstName") 
Last Name: @Html.TextBox("[0].value.LastName") 
 
<h4>Second Person</h4> 
<input type="hidden" name="[1].key" value="secondPerson"/> 
First Name: @Html.TextBox("[1].value.FirstName") 
Last Name: @Html.TextBox("[1].value.LastName") 

此時可以使用如下的action來獲取值
[HttpPost]
public ViewResult Register(IDictionary<string, Person> people) {...}

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

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

View Code
//Controller里添加action
[HttpPost] 
public ActionResult RegisterMember() { 
 
    Person myPerson = new Person(); 
    UpdateModel(myPerson); 
    return View(myPerson); 
} 

//添加Register視圖
@using ModelBinding.Models;
@model ModelBinding.Models.Person
<style type="text/css">
    .check-box
    {
        margin: 0.5em 0 0 0;
    }
</style>
@using (Html.BeginForm("RegisterMember", "Home"))
{
    @Html.EditorForModel()
    <input type="submit" value="Submit" /> 
}

UpdateModel方法獲取一個model對象作為參數並試圖使用標准綁定過程獲取model對象里面公共屬性的值。手動調用model綁定的其中一個原因是為了支持DI。例如,如果我們使用了一個應用程序范圍的依賴解析器,那么我們能夠添加DI到這里的Person對象的創建,如下:

View Code
[HttpPost] 
public ActionResult RegisterMember() { 
 
    Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person)); 
    UpdateModel(myPerson); 
    return View(myPerson); 
} 

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

將綁定限制到指定的數據源(Restricting Binding to a Specific Data Source)

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

[HttpPost]
public ActionResult RegisterMember()
{
    //Person myPerson = new Person();
    //UpdateModel(myPerson);
    Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
    UpdateModel(myPerson, new FormValueProvider(ControllerContext));
    return View(myPerson);
}

這里的UpdateModel是重載的版本接收一個IValueProvider接口實現作為參數,從而指定了綁定過程的數據源。每一個默認的數據源都對應了一個對該接口的實現,如下:
1.Request.Form——>FormValueProvider
2.RouteData.Values——>RouteDataValueProvider
3.Request.QueryString——>QueryStringValueProvider
4.Request.Files——>HttpFileCollectionValueProvider

最常用的現在數據源的方式就是只在尋找Form里面的值,有一個非常靈巧的綁定技巧,以至於我們不用創建一個FormValueProvider的實例,如下:

[HttpPost]
public ActionResult RegisterMember(FormCollection formData)
{
     Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
     UpdateModel(myPerson, formData);
     return View(myPerson);
}

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

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

用戶難免會提交一些不能綁定到相應的model屬性的值,如未驗證的日期或文本當成數值。下一章會介紹相關的綁定驗證的內容,這里在使用UpdateModel方法時,我們必須准備捕獲處理相關的異常,並使用ModelState向用戶提示錯誤的信息,如下:

View Code
        [HttpPost]
        public ActionResult RegisterMember(FormCollection formData)
        {
            Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
            try
            {
                UpdateModel(myPerson, formData);
            }
            catch (InvalidOperationException ex)
            {
                //這里根據ModelState提供UI反饋
                throw ex;
            }
            return View("PersonDisplay", myPerson);
        }

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

View Code
        [HttpPost]
        public ActionResult RegisterMember(FormCollection formData)
        {
            Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
            //try
            //{
            //    UpdateModel(myPerson, formData);
            //}
            //catch (InvalidOperationException ex)
            //{
            //    //這里根據ModelState提供UI反饋
            //    throw ex;
            //}
            if (TryUpdateModel(myPerson, formData))
            {
                //...
            }
            else
            {
                //這里根據ModelState提供UI反饋
            }
            return View("PersonDisplay", myPerson);
        }

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

為了接收上傳的文件,需要定義一個action方法並接收一個HttpPostedFileBase類型的參數。然后,model binder將會使用跟上傳的文件一致的數據填充這個參數。如下:

這里的關鍵是要設定enctype屬性的值為"multipart/form-data".如果不這樣做,瀏覽器只會發送文件名而不是文件本身(這是瀏覽器的運行原理決定的).

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

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

創建一個自定義的Value Provider

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

View Code
using System.Web.Mvc;
using System.Globalization;

namespace ModelBinding.Infrastructure
{
    public class CurrentTimeValueProvider : IValueProvider
    {

        public bool ContainsPrefix(string prefix)
        {
            return string.Compare("CurrentTime", prefix, true) == 0;
        }

        public ValueProviderResult GetValue(string key)
        {
            return ContainsPrefix(key) ?
                new ValueProviderResult(DateTime.Now, null, CultureInfo.InvariantCulture)
                : null;
        }
    }

    public class CurrentTimeValueProviderFactory : ValueProviderFactory
    {

        public override IValueProvider GetValueProvider(ControllerContext controllerContext)
        {
            return new CurrentTimeValueProvider();
        }
    }
}

我們只響應針對CurrentTime的請求,並當接收到這樣的請求時,返回DateTime.Now屬性的值,對其他的請求,返回null,表示不能提供數據。我們必須將數據作為ValueProviderResult類型返回。為了注冊自定義的Value Provider,我們需要創建一個用來產生Provider實例的工廠,這個類從ValueProviderFactory派生,如下:

View Code
protected void Application_Start()
{
     AreaRegistration.RegisterAllAreas();

     ValueProviderFactories.Factories.Add(0, new CurrentTimeValueProviderFactory());
     RegisterGlobalFilters(GlobalFilters.Filters);
     RegisterRoutes(RouteTable.Routes);
}

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

public ActionResult Clock(DateTime currentTime)
{
      return Content("The time is " + currentTime.ToLongTimeString());
}

 

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

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

View Code
using System.Web.Mvc;

namespace ModelBinding.Infrastructure
{
    public class DIModelBinder : DefaultModelBinder
    {
        protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
        {
            return DependencyResolver.Current.GetService(modelType) ?? base.CreateModel(controllerContext, bindingContext, modelType);
        }
    }
}

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

View Code
protected void Application_Start() { 
    AreaRegistration.RegisterAllAreas(); 
 
    ModelBinders.Binders.DefaultBinder = new DIModelBinder(); 
 
    RegisterGlobalFilters(GlobalFilters.Filters); 
    RegisterRoutes(RouteTable.Routes); 
} 

 

創建一個自定義的Model Binder

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

View Code
using System.Web.Mvc;
using ModelBinding.Models;

namespace ModelBinding.Infrastructure
{
    public class PersonModelBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            //判斷,如果存在一個model則更新,否則創建
            Person model = (Person)bindingContext.Model ?? (Person)DependencyResolver.Current.GetService(typeof(Person));

            //檢查下這個Value Provider是否具有必須的前綴
            bool hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName);
            string searchPrefix = (hasPrefix) ? bindingContext.ModelName + "." : "";

            //填充model對象的字段
            model.PersonId = int.Parse(GetValue(bindingContext, searchPrefix, "PersonId"));
            model.FirstName = GetValue(bindingContext, searchPrefix, "FirstName");
            model.LastName = GetValue(bindingContext, searchPrefix, "LastName");
            model.BirthDate = DateTime.Parse(GetValue(bindingContext, searchPrefix, "BirthDate"));
            model.IsApproved = GetCheckedValue(bindingContext, searchPrefix, "IsApproved");
            model.Role = (Role)Enum.Parse(typeof(Role), GetValue(bindingContext, searchPrefix, "Role"));
            return model;
        }

        private string GetValue(ModelBindingContext context, string prefix, string key)
        {
            ValueProviderResult vpr = context.ValueProvider.GetValue(prefix + key);
            return vpr == null ? null : vpr.AttemptedValue;
        }

        private bool GetCheckedValue(ModelBindingContext context, string prefix, string key)
        {
            bool result = false;
            ValueProviderResult vpr = context.ValueProvider.GetValue(prefix + key);
            if (vpr != null)
            {
                result = (bool)vpr.ConvertTo(typeof(bool));
            }
            return result;
        }
    }
}

下面我一步步來解析這段代碼,首先我們獲取將要綁定的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,如下:

View Code
using System.Web.Mvc;
using ModelBinding.Models;

namespace ModelBinding.Infrastructure
{
    public class CustomModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(Type modelType)
        {
            return modelType == typeof(Person) ? new PersonModelBinder() : null;
        }
    }
}

這種方式更加靈活,特別是在我們有多個自定義的binders或多個providers維護時。接着注冊剛創建的provider:
ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());

使用ModelBinder屬性(Using the ModelBinder Attribute)

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

View Code
[ModelBinder(typeof(PersonModelBinder))] 
public class Person 
{ 
    [HiddenInput(DisplayValue=false)] 
    public int PersonId { get; set; } 
 
    public string FirstName { get; set; } 
    public string LastName { get; set; } 
 
    [DataType(DataType.Date)] 
    public DateTime BirthDate { get; set; } 
    public Address HomeAddress { get; set; } 
    public bool IsApproved { get; set; } 
    public Role Role { get; set; } 
} 

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM