擴展 ASP.NET MVC 模型擴展 – ASP.NET MVC 4 系列

       大部分人不能將核心運行時(System.Web 中的類)和 ASP.NET Web Forms 應用程序平臺(System.Web.UI 中的類)區分開來。數組

       ASP.NET 開發團隊在簡單的核心運行時抽象之上建立了複雜的 Web Form 抽象和 ASP.NET MVC。正由於 ASP.NET MVC 框架創建在公共抽象之上,因此 ASP.NET MVC 框架能實現的任何功能,任何人也均可以實現。ASP.NET MVC 框架自己也由若干層抽象組成,從而使得開發人員可以選擇他們須要的 MVC 片斷,替換或修改他們不須要的片斷。對於後續的每個版本,ASP.NET MVC 團隊都開放了更多的框架內部定製點服務器

       ASP.NET MVC 4 中的模型系統包括幾個可擴展部分,其中包括使用元數據描述模型、驗證模型以及影響從請求中構造模型的能力app

 

模型擴展 - 把請求數據轉化爲模型

       將請求數據(表單數據、查詢字符串數據、路由信息)轉換爲模型的過程稱爲模型綁定。模型綁定分爲兩個階段:框架

  • 使用值提供器理解數據的來源
  • 使用這些值 建立/更新 模型對象(經過使用 模型綁定器)

       真實模型綁定過程當中使用的值都來自值提供器。值提供器的做用僅僅是訪問可以在模型綁定過程當中正確使用的信息。ASP.NET MVC 框架自帶的若干值提供器能夠提供如下數據源中的數據ide

  1. 子操做(RenderAction)的顯式值
  2. 表單值
  3. 來自 XMLHttpRequest 的 JSON 數據
  4. 路由值
  5. 查詢字符串值
  6. 上傳的文件

       值提供器來自值提供器工廠,而且系統按照值提供器的註冊順序來從中搜尋數據(上面是默認順序)。開發人員能夠編寫本身的值提供器工廠和值提供器,而且還能夠把它們插入到包含在 ValueProviderFactories.Factories 中的工廠列表中。當模型綁按期間須要使用額外的數據源時,開發人員一般會選擇編寫本身的值提供器工廠和值提供器。函數

       除了 ASP.NET MVC 自己包含的值提供器工廠之外,開發團隊也在 ASP.NET MVC Futures 中包含了另外一些值提供器工廠和值提供器ui

  1. Cookie 值提供器
  2. 服務器變量值提供器
  3. Session 值提供器
  4. TempData 值提供器

 

       模型擴展的另外一部分是模型綁定器。它們從值提供器系統中獲取值,並利用獲取的值建立新模型或者填充已有模型。ASP.NET MVC 中的默認模型綁定器(DefaultModelBinder)是一段很是強大的代碼,它能夠對傳統類、集合類、列表、數組、字典進行模型綁定。但默認模型綁定器不支持不可變對象對象初始值經過構造函數設置,以後不能改變)。this

       例如,因爲 CLR 中 Point 類是不可變的,所以咱們必須使用它的值構造一個新實例:spa

public class PointModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var valueProvider = bindingContext.ValueProvider;
        int x = (int)valueProvider.GetValue("X").ConvertTo(typeof(int));
        int y = (int)valueProvider.GetValue("Y").ConvertTo(typeof(int));
        return new Point(x, y);
    }
}

       當建立一個新的模型綁定器時,須要告知 MVC 框架存在一個新的模型綁定器(能夠在 ModelBinders.Binders 的全局列表中註冊新的模型綁定器)以及什麼時候使用它(可用 [ModelBinder] 特性來裝飾綁定類)。code

       模型綁定器每每容易被忽略的一個責任是:驗證它們要綁定的值。上面的示例代碼未包含任何驗證邏輯,所以看上去很是簡單。下面是一個更完整的模型綁定器版本:

public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
    // We first attempt to find values based on the model name, and if we can't find
    // anything for the model name, we'll fall back to the empty prefix as appropriate.
 
    if (!String.IsNullOrEmpty(bindingContext.ModelName) &&
        !bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
    {
        if (!bindingContext.FallbackToEmptyPrefix)
            return null;
 
        bindingContext = new ModelBindingContext
        {
            ModelMetadata = bindingContext.ModelMetadata,
            ModelState = bindingContext.ModelState,
            PropertyFilter = bindingContext.PropertyFilter,
            ValueProvider = bindingContext.ValueProvider
        };
    }
 
    // We have to create a new model, because Point is immutable. When the type
    // isn't immutable, we can always take in the existing object, when one exists,
    // and update it instead. The existing object, if one exists, is available
    // via bindingContext.Model. Instead, we'll put a temporary (empty) object into
    // the binding context so that validation can succeed while we validate all
    // the parameter values.
 
    bindingContext.ModelMetadata.Model = new Point();
 
    return new Point(
        Get<int>(controllerContext, bindingContext, "X"),
        Get<int>(controllerContext, bindingContext, "Y")
    );
}

       上述代碼新增了 2 項內容:

  1. if 代碼塊試圖在回落到空前綴以前找到帶有名稱前綴的值。當系統開始模型綁定時,模型參數名稱(本例中是 pt,public ActionResult Index(Point pt))被設置爲 bindingContext.ModelName 中的值,而後再查看值提供器,以肯定是否包含 pt 爲前綴的子值,例如 pt.X,pt.Y。假如擁有一個名爲 pt 的參數,那麼使用的值的名稱應該是 pt.X 和 pt.Y 而不是隻有 X,或只有 Y。然而,若是找不到以 pt 開頭的值,就須要使用名稱中只有 X 和 Y 的值。
  2. 在 ModelMetadata.Model 中設置了一個 Point 對象的空實例。之因此這樣作,是由於大部分驗證系統包括 DataAnnotations,都指望看到一個容器對象的實例,即使裏面不存在任何實際的值。因爲 Get 方法調用驗證,所以咱們須要提供給驗證系統一個某種類型的容器對象,即使知道它不是最終容器。

 

       下面是 Get 方法的完整函數:

private TModel Get<TModel>(ControllerContext controllerContext,
                            ModelBindingContext bindingContext,
                            string name)
{
    // Get the fully qualified name, because model state needs to use that, and not just
    // the simple property name.
    string fullName = name;
    if (!String.IsNullOrWhiteSpace(bindingContext.ModelName))
        fullName = bindingContext.ModelName + "." + name;
 
    // Get the value from the value provider
    ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(fullName);
 
    // Add the attempted value to model state, so that we can round-trip their
    // value even when it's incorrect and incapable of being held inside the
    // model itself (i.e., the user types "abc" for an int).
    ModelState modelState = new ModelState { Value = valueProviderResult };
    bindingContext.ModelState.Add(fullName, modelState);
 
    // Get the ModelMetadata that represents this property, as we use several of its
    // values, and it's necessary for validation
    ModelMetadata metadata = bindingContext.PropertyMetadata[name];
 
    // Convert the attempted value to null automatically
    string attemptedValue = valueProviderResult.AttemptedValue;
    if (metadata.ConvertEmptyStringToNull && String.IsNullOrWhiteSpace(attemptedValue))
        attemptedValue = null;
 
    TModel model;
    bool invalidValue = false;
 
    try
    {
        // Attempt to convert the value to the correct type
        model = (TModel)valueProviderResult.ConvertTo(typeof(TModel));
        metadata.Model = model;
    }
    catch (Exception)
    {
        // Conversion failed, so give back the default value for the type
        // and set the attempted value into model metadata
        model = default(TModel);
        metadata.Model = attemptedValue;
        invalidValue = true;
    }
 
    // Run the validators for the given property
    IEnumerable<ModelValidator> validators = ModelValidatorProviders.Providers.GetValidators(metadata, controllerContext);
    foreach (var validator in validators)
        foreach (var validatorResult in validator.Validate(bindingContext.Model))
            modelState.Errors.Add(validatorResult.Message);
 
    // Only add the "invalid value" message if there were no other errors, because things like
    // required validation should trump conversion failures, and null/empty values will often
    // fail both required validation and type-conversion validation
    if (invalidValue && modelState.Errors.Count == 0)
        modelState.Errors.Add(
            String.Format(
                "The value '{0}' is not a valid value for {1}.",
                attemptedValue,
                metadata.GetDisplayName()
            )
        );
 
    return model;
}
相關文章
相關標籤/搜索