ASPを理解する.NET MVCでのモデルバインド

15044 ワード

モデルバインドの本質


任意のコントローラメソッドの実行は、action invokerコンポーネント(以下、invokerで置き換える)によって制御される.各アクションメソッドのパラメータについて、このinvokerコンポーネントはModel Binder Object(モデルバインドオブジェクト)を取得します.Model Binderの役割には、Actionメソッドパラメータの可能な値(HTTPリクエストコンテキストから)を探すことが含まれます.各パラメータは異なるModel Binderにバインドできます.しかし、ほとんどの場合、デフォルトのモデルバインド-DefaultModelBinderを使用しています(カスタムModel Binderを使用する場合は、明示的に設定していません).
各Model Binderは、アクションメソッドのパラメータに値を設定するために独自のアルゴリズムを使用します.既定のモデルバインドオブジェクトでは、反射メカニズムが多数使用されます.具体的には、各アクションメソッドパラメータについて、Model Binderはパラメータ名に基づいてパラメータに一致する値を求めることを試みる.たとえば、Actionメソッドのパラメータ名がTextの場合、ModelBinderはコンテキストで同じ名前の名前の名前の値ペア(Entry)を探すように要求します.見つかった場合、Model BinderはEntryの値をActionメソッドパラメータタイプに変換し続けます.タイプ変換に成功すると、変換後の値がそのActionメソッドパラメータに割り当てられます.そうしないと、例外が放出されます.
注意:最初の変換タイプが正常に見つからないか、リクエストコンテキストで一致するパラメータが見つからない(このパラメータのタイプは空ではないタイプ)場合、すぐに例外が放出されます.つまり、宣言されたすべてのパラメータがModel Binderによって正常に解析された場合にのみ、Actionメソッドが呼び出されます.さらにModel Binderで発生した異常はAction法では捕獲できないことに注意し、global.asaxにグローバルエラープロセッサ(global error handler)を設定して、これらの例外の処理をキャプチャします.また、メソッドパラメータがnullに値を付けられない場合にのみ異常が放出されることに注意してください.次のような場合には、
public ActionResult TestAction(string name, Int32 age) {
   // ...
   return View();
}

要求コンテキストにnameという名前のEntryが含まれていない場合、nameパラメータ値はModelBinderによってnullに設定されます.ただしageパラメータは異なります.Int 32タイプは基本値タイプなのでnull値は付与できません.ageパラメータを送信しないことを許可する必要がある場合は、コードを簡単に変更して、次のようにするか、ageパラメータにデフォルト値を指定します.
public ActionResult TestAction(string name, Nullable<Int32> age) {
   // ...
   return View();
}

HTTPリクエストコンテキストからパラメータ値を検索するには、次の手順に従います。


要求データソース
説明
Request.Form
フォームからコミットされたパラメータ
RouteData.Values
ルーティングパラメータ
Request.QueryString
クエリー・パラメータ、類似http://abc.com?name=jxq,ここでのname=jxqはクエリパラメータである.
Request.Files
要求に従ってアップロードされたファイル
アップロードするパラメータが複数ある場合は、メソッドパラメータのリストが長すぎないように、リクエストパラメータごとにアクションメソッドパラメータを作成しないほうがいいです.
デフォルトのモデル・バインダーでは、パラメータ・リストをパラメータ・タイプにカプセル化し、デフォルトのモデル・バインダーでは、上記と同じ名前(属性名と要求パラメータ名が一致する)マッチング・アルゴリズムに従ってパラメータ・オブジェクトの属性を設定できます.



手動でモデルバインドを呼び出す
デフォルトでは、モデルバインドは自動的に呼び出されますが、手動でモデルバインドを行うこともできます.たとえば、次のコードの例を示します.
[HttpPost]
public ActionResult RegisterMember() {
   Person myPerson = new Person();
   UpdateModel(myPerson);
   return View(myPerson);
}

上のUpdateModel(myPerson)は、手動モデルバインドです.
モデルバインドを手動で行う最大の利点は、モデルバインドプロセスをより柔軟にすることです.たとえば、タイプバインドをフォームコミットパラメータのみに制限すると、次のようにできます.
[HttpPost]
public ActionResult RegisterMember() {
   Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
   UpdateModel(myPerson, new FormValueProvider(ControllerContext));
 
   return View(myPerson);
}

FormValueProviderはIValueProviderインタフェースを実現し、その他のいくつかのパラメータに対応するIValueProviderは以下のように実現した.
要求データソース
IValueProvider実装
Request.Form
FormValueProvider
RouteData.Values
RouteDataValueProvider
Request.QueryString
QueryStringValueProvider
Request.Files
HttpFileCollectionValueProvider
上記の方法では、タイプバインドのデータソースを制限できるほか、IValueProviderとしてFormCollectionを直接利用することもできます.以下のようにします.
[HttpPost]
public ActionResult RegisterMember(FormCollection formData) {
   Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
   UpdateModel(myPerson, formData);

   return View(myPerson);
}

モデルバインドプロセスを手動で行うと、例外が発生する可能性があります.2つの方法で処理できます.1つ目の方法は、例外を直接キャプチャすることです.2つ目の方法はTryUpdateModel法を利用することである.1つ目の方法は次のとおりです.
[HttpPost]
public ActionResult RegisterMember(FormCollection formData) {
   Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
   try
   {
      UpdateModel(myPerson, formData);
   }
   catch(InvalidOperationException e)
   {
      //     
   }

   return View(myPerson);
}

2つ目の方法は次のとおりです.
[HttpPost]
public ActionResult RegisterMember(FormCollection formData) {
   Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));

   if(TryUpdateModel(myPerson, formData))
   {
      //     
   }
   else {
      //     
   }

   return View(myPerson);
}

第1節では、デフォルトのモデルバインディングを使用する場合、globalでモデルバインディングプロセスから放出される異常をActionメソッドでキャプチャすることはできないと述べた.ascxでエラープロセッサを構成して処理をキャプチャします.それ以外にもModelStateを通じてIsValidは、デフォルトのモデルバインドが成功したかどうかを判断します.
 
カスタムモデルバインドシステム
IValueProviderインプリメンテーションをカスタマイズできます.IValueProviderインタフェースは次のように定義されています.
namespace System.Web.Mvc {
   using System;
   public interface IValueProvider {
      bool ContainsPrefix(string prefix);
      ValueProviderResult GetValue(string key);
   }
}

IValueProviderの実装をカスタマイズします.
using System;
using System.Globalization;
using System.Web.Mvc;

namespace CustomeModelBinderDemo.Controllers.ValueProvider
{
    public class MyValueProvider: IValueProvider
    {
        public bool ContainsPrefix(string prefix)
        {
            return System.String.Compare("curTime", prefix, StringComparison.OrdinalIgnoreCase) == 0;
        }

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

上記のGetValue(string key)メソッドは、Actionメソッドのパラメータ名に従ってHTTP要求コンテキストから一致する値を取得してValueProviderResultオブジェクトに格納し、ValueProviderResultタイプには、カプセル化された値を指定タイプに変換するConvertTo(Type type)メソッドが含まれます.これは、最初のセクションで説明したタイプ変換とよく一致します(XValueProviderオブジェクトがモデルバインド中のタイプ変換を担当していることも示しています.モデルバインドがXValueProviderオブジェクトをタイプ変換に呼び出すためです).
次に、ValueProviderファクトリクラスを定義します.
public class CurrentTimeValueProviderFactory : ValueProviderFactory {
   public override IValueProvider GetValueProvider(ControllerContext controllerContext) {
      return new CurrentTimeValueProvider();
   }
}

そして私たちはGlobalにいます.asaxのApplication_Startメソッドでこのファクトリクラスを登録します.
protected void Application_Start() {
   AreaRegistration.RegisterAllAreas();
   ValueProviderFactories.Factories.Insert(0, new CurrentTimeValueProviderFactory());   //   ValueProvider    
   RegisterGlobalFilters(GlobalFilters.Filters);
   RegisterRoutes(RouteTable.Routes);
}

最後にActionでは次のように使用します.
public ActionResult Clock(DateTime curTime) {
   return Content("The time is " + curTime.ToLongTimeString());
}

モデルバインディングオブジェクトをカスタマイズするには、2つの方法があります.1つ目は、DefaultModelBinderクラスを継承し、CreateModelメソッドを書き換え、Application_StartメソッドでModelBindersを設定する.Binders.DefaultBinderはこのモデル・バインダーです(現在のデフォルト・モデル・バインダーが私たち自身で定義されていることを示しています):
public class DIModelBinder : DefaultModelBinder {
   protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) {
      return DependencyResolver.Current.GetService(modelType) ?? base.CreateModel(controllerContext, bindingContext, modelType);
   }
}

そしてglobal.asaxのApplication_Startメソッドでは、デフォルトのモデルバインディングをDIModelBinderに設定します.
protected void Application_Start() {
   // ...
   ModelBinders.Binders.DefaultBinder = new DIModelBinder();
   // ...
}

2つ目の方法は、IModelBinderインタフェースを継承し、そのインタフェース方法を実装します.
public class PersonModelBinder : IModelBinder {
   public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
      //         model  ,        (         bindingContext.Model    null)
       Person model = (Person) bindingContext.Model ?? (Person)DependencyResolver.Current.GetService(typeof(Person));

      // find out if the value provider has the required prefix
      bool hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName);   // 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;
   }
}

そして同様にモデルバインダーを登録する必要があります.グローバルに登録するには、Application_Startメソッドに次のコードを追加します.
protected void Application_Start() {
   // ...
   ModelBinders.Binders.Add(typeof(Person), new PersonModelBinder());
   // ...
}

次のように、Attributeを使用してアクションパラメータのモデルバインディングを設定することもできます.
public ActionResult Index(
        [ModelBinder(typeof(DateTimeModelBinder))] DateTime theDate)

または、モデルバインダとタイプのバインドは、次のように行います.
[ModelBinder(typeof(PersonModelBinder))]
public class Person {
   [HiddenInput(DisplayValue=false)]
   public int PersonId { get; set; }

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

最後に、ModelBinderProviderをカスタマイズする方法を見てみましょう.主に複数のモデルバインディングがある場合にどのバインディングを使用するかを選択するために使用されます.IModelProviderインタフェースを実装する必要があります.
using System;
using System.Web.Mvc;
using MvcApp.Models;
namespace MvcApp.Infrastructure {
   public class CustomModelBinderProvider : IModelBinderProvider {
      public IModelBinder GetBinder(Type modelType) {
         return modelType == typeof(Person) ? new PersonModelBinder() : null;
      } 
   }
}

そしてまたいつものApplicationでStartメソッドに登録するには、次の手順に従います.
ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());

 
参考資料:
https://www.simple-talk.com/dotnet/asp.net/the-three-models-of-asp.net-mvc-apps/
.Chapter 17: Model Binding