ASP.NET MVC

ASP.NET MVC モデル バインディングの特長と問題点

Jess Chadwick

ASP.NET MVC モデル バインディングを使用すると、コントローラー アクションのパラメーターを自動的に設定する抽象層の導入、通常のプロパティ マッピングの処理、および ASP.NET 要求データの操作に通常必要となる型変換コードによって、コントローラーのアクションを簡略化できます。モデル バインディングはシンプルに見えますが、実際は比較的複雑なフレームワークで、コントローラーのアクションに必要なオブジェクトを作成および設定する、多くの連動する要素で構成されています。

この記事では、モデル バインディング フレームワークの各層や、アプリケーションのニーズを満たすためにモデル バインディング ロジックを拡張できるさまざまな方法を紹介しながら、ASP.NET MVC モデル バインディング サブシステムの中核について詳しく説明します。その過程で、見過ごされがちなモデル バインディングの手法やモデル バインディングで最も一般的な間違いを防ぐ方法をいくつか紹介します。

モデル バインディングの基礎

モデル バインディングを理解するために、ASP.NET アプリケーションの要求値からオブジェクトを設定する際に通常使用する方法を見てみましょう (図 1 参照)。

図 1 要求から値を直接取得する

public ActionResult Create()
{
  var product = new Product() {
    AvailabilityDate = DateTime.Parse(Request["availabilityDate"]),
    CategoryId = Int32.Parse(Request["categoryId"]),
    Description = Request["description"],
    Kind = (ProductKind)Enum.Parse(typeof(ProductKind), 
                                   Request["kind"]),
    Name = Request["name"],
    UnitPrice = Decimal.Parse(Request["unitPrice"]),
    UnitsInStock = Int32.Parse(Request["unitsInStock"]),
 };
 // ...
}

次に、モデル バインディングを利用して同じ結果を生み出す図 2 のアクションと図 1 を比べてください。

図 2 プリミティブ値へのモデル バインディング

public ActionResult Create(
  DateTime availabilityDate, int categoryId,
    string description, ProductKind kind, string name,
    decimal unitPrice, int unitsInStock
  )
{
  var product = new Product() {
    AvailabilityDate = availabilityDate,
    CategoryId = categoryId,
    Description = description,
    Kind = kind,
    Name = name,
    UnitPrice = unitPrice,
    UnitsInStock = unitsInStock,
 };
 
 // ...
}

この 2 つの例は同じこと (Product インスタンスの設定) を行いますが、図 2 のコードでは、ASP.NET MVC を使用して要求の値を厳密に型指定された値に変換しています。モデル バインディングを使用すると、コントローラーのアクションによってビジネス価値を提供することだけに力を注ぎ、通常の要求のマッピングや解析に時間を浪費するのを避けることができます。

複合オブジェクトにバインドする

単純なプリミティブ型へのモデル バインディングではかなり大きな効果が得られますが、多くのコントローラー アクションで使用するパラメーターは数種類では済みません。さいわい、ASP.NET MVC では、プリミティブ型以外に複合型も処理することが可能です。

次のコードは、プリミティブ値と Product クラスに直接バインドしている部分を省略しいる Create アクションのもう 1 つの形式です。

public ActionResult Create(Product product)
{
  // ...
}

繰り返しになりますが、このコードは図 1図 2 のアクションと同じ結果になります。今回だけは、コードは一切必要ありません。ASP.NET MVC の複合モデル バインディングによって、新しい Product インスタンスを作成および設定するのに必要なすべての定型コードが省略されています。このコードは、モデル バインディングの真価を具体的に表したものです。

モデル バインディングを分解する

実際にモデル バインディングを紹介したところで、モデル バインディング フレームワークを形づくる要素に分けて見ていきましょう。

モデル バインディングは、要求からの値の収集とそれらの値のモデルへの設定という、まったく異なる 2 つの手順に分割されます。これらの手順は、それぞれ値プロバイダーとモデル バインダーによって実現します。

値プロバイダー

ASP.NET MVC には、クエリ文字列パラメーター、フォーム フィールド、経路データといった要求値の最も一般的なソースに対応する値プロバイダーの実装が含まれていて、実行時に、ValueProviderFactories クラスに登録された値プロバイダーを使用して、モデル バインダーが使用できる要求値を評価します。

既定では、値プロバイダーのコレクションによって、さまざまなソースの値が次の順序で評価されます。

  1. 以前にバインドしたアクション パラメーター (アクションが子アクションの場合)
  2. フォーム フィールド (Request.Form)
  3. JSON 要求本文中にあるプロパティの値 (Request.InputStream) (ただし、要求が AJAX 要求の場合のみ)
  4. 経路データ (RouteData.Values)
  5. クエリ文字列パラメーター (Request.QueryString)
  6. ポストされたファイル (Request.Files)

Request オブジェクトのような値プロバイダーのコレクションは、実際見せ掛けのディクショナリ (データがあった場所を把握しなくてもモデル バインダーで使用できるキーと値のペアの抽象層) にすぎません。しかし、値プロバイダー フレームワークでは、この抽象概念が Request ディクショナリよりもさらに一歩進んだものになり、モデル バインディング フレームワークがデータを取得する方法や場所を完全に制御できます。また、独自のカスタム値プロバイダーを作成することも可能です。

カスタム値プロバイダー

カスタム値プロバイダーを作成する最小要件はきわめて単純で、System.Web.Mvc.ValueProviderFactory インターフェースを実装する新しいクラスを作成します。

例として、ユーザーの Cookie から値を取得するカスタム値プロバイダーを、図 3 に示します。

図 3 Cookie 値を調査するカスタム値プロバイダー ファクトリ

public class CookieValueProviderFactory : ValueProviderFactory
{
  public override IValueProvider GetValueProvider
  (
    ControllerContext controllerContext
  )
  {
    var cookies = controllerContext.HttpContext.Request.Cookies;
 
    var cookieValues = new NameValueCollection();
    foreach (var key in cookies.AllKeys)
    {
      cookieValues.Add(key, cookies[key].Value);
    }
 
    return new NameValueCollectionValueProvider(
      cookieValues, CultureInfo.CurrentCulture);
  }
}

CookieValueProviderFactory がとてもシンプルなことがわかります。新しい値プロバイダーを最初から構築する代わりに CookieValueProviderFactory を使用すれば、単純にユーザーの Cookie を取得し、NameValueCollectionValueProvider を利用してその値をモデル バインディング フレームワークに公開できます。

カスタム値プロバイダーを作成したら、ValueProviderFactories.Factories コレクションを使って値プロバイダーの一覧に追加する必要があります。

var factory = new CookieValueProviderFactory();
ValueProviderFactories.Factories.Add(factory);

カスタム値プロバイダーの作成はとても簡単ですが、作成するかどうかは検討が必要です。ASP.NET MVC に最初から付属している値プロバイダーのセットでは、(場合によっては Cookie の例外を持つ) HttpRequest で使用できるデータのほとんどが公開され、通常は、ほとんどのシナリオを満たすのに十分なデータが提供されます。

特定のシナリオに対して新しい値プロバイダーを作成することが正しいかどうか判断するには、既存の値プロバイダーから提供される一連の情報に、必要なデータが (場合によっては適切な形式でなくても) すべて含まれているかどうかを確認します。

すべて含まれていなければ、カスタム値プロバイダーを追加することは、その欠点を補うのに正しい方法であると言えるでしょう。しかし、(多くの場合がそうであるように) すべて含まれている場合は、モデル バインディング動作をカスタマイズして欠けている部分を補い、値プロバイダーから提供されるデータにアクセスする方法を考えることをお勧めします。こからは、これを実現する方法を説明します。

値プロバイダーを使用してモデルを作成および設定する、ASP.NET MVC モデル バインディング フレームワークの主な構成要素をモデル バインダーと呼びます。

既定のモデル バインダー

ASP.NET MVC フレームワークには、ほとんどの型のモデルを効果的にバインドするように設計された、DefaultModelBinder というモデル バインダーの既定の実装が含まれています。DefaultModelBinder では、以下のように対象モデルの各プロパティに比較的単純で再帰的なロジックを使用することで、その目的を実現します。

  1. 値プロバイダーを調べて、プロパティ名がプレフィックスとして登録されているかどうかを確かめることで、そのプロパティが単純型と複合型のどちらの型として検出されたかを確認する。プレフィックスは、値が複合オブジェクトのプロパティかどうかを示すのに使用する、"ドット表記" の HTML フォーム フィールド名です。このプレフィックスのパターンは、[親プロパティ].[プロパティ] になります。たとえば、UnitPrice.Amount という名前のフォーム フィールドには、UnitPrice プロパティの Amount フィールドの値が含まれます。
  2. プロパティ名を取得するため、登録済みの値プロバイダーから ValueProviderResult を受け取る。
  3. 値が単純型の場合は、ターゲット型への変換を試みる。既定の変換ロジックでは、プロパティの TypeConverter を利用して、文字列型のソース値がターゲット型に変換されます。
  4. それ以外の場合は、プロパティが複合型であるため、再帰的にバインドを行う。

再帰モデル バインディング

再帰モデル バインディングでは、モデル バインディング処理全体が事実上何度も開始されますが、新しいプレフィックスにはターゲット プロパティの名前が使用されます。この方法を使用すると、DefaultModelBinder で複合オブジェクト グラフ全体をスキャンし、深く入れ子になったプロパティ値を設定できます。

再帰バインディングを実際に確認するため、Product.UnitPrice を単純型の Decimal からカスタム型の Currency に変更します。両方のクラスを図 4 に示します。

図 4 Unitprice 複合プロパティを持つ Product クラス

public class Product
{
  public DateTime AvailabilityDate { get; set; }
  public int CategoryId { get; set; }
  public string Description { get; set; }
  public ProductKind Kind { get; set; }
  public string Name { get; set; }
  public Currency UnitPrice { get; set; }
  public int UnitsInStock { get; set; }
}
 
public class Currency
{
  public float Amount { get; set; }
  public string Code { get; set; }
}

この更新を適切に行うと、モデル バインダーでは、複合プロパティの Product.UnitPrice を設定する UnitPrice.Amount という値と UnitPrice.Code という値が検索されます。

DefaultModelBinder の再帰バインディング ロジックでは、最も複雑なオブジェクト グラフでも事実上設定することができます。ここまでは、DefaultModelBinder で簡単に処理できる、オブジェクト階層の 1 階層だけ深い位置に存在する複合オブジェクトについて説明してきました。再帰モデル バインディングの真価を示すために、次のように、Product クラスに同じ型の Child という新しいプロパティを追加します。

public class Product {
  public Product Child { get; set; }
  // ...
}

次に、フォームに新しいフィールドを追加して、(各レベルを示すドット表記を当てはめながら) 好きなだけ階層を作成します。たとえば、次のような階層です。

<input type="text" name="Child.Child.Child.Child.Child.Child.Name"/>

このフォーム フィールドは、結果として 6 階層を持つ Product クラスになります。各階層では、DefaultModelBinder によって律儀に新しい Product インスタンスが作成され、すぐに値のバインドが開始されます。バインドがすべて完了するころには、図 5 のコードのようなオブジェクト グラフができあがります。

図 5 再帰モデル バインディングから作成したオブジェクト グラフ

new Product {
  Child = new Product { 
    Child = new Product {
      Child = new Product {
        Child = new Product {
          Child = new Product {
            Child = new Product {
              Name = "MADNESS!"
            }
          }
        }
      }
    }
  }
}

この例はプロパティ値を 1 つしか設定していない不自然なものですが、DefaultModelBinder の再帰モデル バインディング機能によって非常に複雑なオブジェクト グラフを初めからサポートできるしくみを示すデモとしては優れたものです。設定する値を示すフォーム フィールド名を作成できる場合、再帰モデル バインディングを使用すれば、その値がオブジェクト階層のどの位置に存在するかにかかわらず、モデル バインダーでその値を検索してバインドできます。

モデル バインディングが失敗するように見える場合

DefaultModelBinder でどうしてもバインドできないモデルがあるというのは事実です。しかし、既定のモデル バインディング ロジックが機能しないように見えて、実際には適切に使用することで正常に機能するシナリオもかなりあります。

以下に、開発者が多くの場合に DefaultModelBinder で処理できないと考える最も一般的シナリオと、それらのシナリオを DefaultModelBinder だけを使って実装できる方法をいくつか紹介します。

複合コレクション: ASP.NET MVC 組み込みの値プロバイダーは、すべての要求フィールド名をフォーム ポスト値と同様に扱います。たとえば、それぞれの値に独自かつ一意のインデックスが必要な、フォーム ポストのプリミティブ値のコレクションを考えてみましょう (読みやすいように空白文字を追加しています)。

MyCollection[0]=one &
MyCollection[1]=two &
MyCollection[2]=three

複合オブジェクトのコレクションにも同じ手法を利用できます。このことを示すために、UnitPrice プロパティを Currency オブジェクトのコレクションに変更して複数の通貨をサポートするよう、Product クラスを更新します。

public class Product : IProduct
{
  public IEnumerable<Currency> UnitPrice { get; set; }
 
  // ...
}

この変更により、更新した UnitPrice プロパティを設定するのに次の要求パラメーターが必要です。

UnitPrice[0].Code=USD &
UnitPrice[0].Amount=100.00 &

UnitPrice[1].Code=EUR &
UnitPrice[1].Amount=73.64

複合オブジェクトのコレクションをバインドするのに必要な要求パラメーターの名前付け構文に注目してください。領域で一意の各項目を識別するのに使用するインデクサーがあることと、各インスタンスの各プロパティに、そのインスタンスへの完全な参照がインデックス付きで必ず含まれていることがわかります。要求が GET か POST かにかかわらず、モデル バインダーは、プロパティ名がフォーム ポストの名前付け構文にならっていると想定することに注意してください。

少々直観に反することですが、JSON 要求にも同じ要件があります。つまり、JSON 要求もフォーム ポストの名前付け構文に従っている必要があります。たとえば、前の UnitPrice コレクションの JSON ペイロードを考えてみましょう。このデータに対する純粋な JSON の配列構文は、次のように表します。

[ 
  { "Code": "USD", "Amount": 100.00 },
  { "Code": "EUR", "Amount": 73.64 }
]

しかし、既定の値プロバイダーやモデル バインダーでは、データを JSON のフォーム ポストとして表す必要があります。

{
  "UnitPrice[0].Code": "USD",
  "UnitPrice[0].Amount": 100.00,

  "UnitPrice[1].Code": "EUR",
  "UnitPrice[1].Amount": 73.64
}

複合オブジェクトのコレクションを使用するシナリオは、構文がすべての開発者にとってわかりやすいとは言えないため、おそらく開発者が直面する中で最も一般的に問題のあるシナリオの 1 つでしょう。しかし、複合コレクションをポストする比較的単純な構文を習得すれば、これらのシナリオははるかに対処しやすくなります。

汎用カスタム モデル バインダー: DefaultModelBinder は、ほとんどのシナリオを処理できるほど強力なものですが、望むとおりに動作しない場合があります。そのようなシナリオがあるとき、多くの開発者は、モデル バインディング フレームワークの拡張機能を利用するチャンスに飛び付いて、独自のカスタム モデル バインダーを構築します。

たとえば、Microsoft .NET Framework では、オブジェクト指向の原則に関してすばらしいサポートが提供されますが、DefaultModelBinder では、抽象基本クラスやインターフェイスへのバインドに関して何もサポートされていません。この問題点について説明するために、Product クラスを、読み取り専用のプロパティで構成されるインターフェイス (IProduct) から派生するようリファクタリングします。同様に、Create コントローラー アクションを、Product の具体的な実装ではなく新しい IProduct インターフェイスを受け取るように更新します (図 6 参照)。

図 6 インターフェイスへのバインド

public interface IProduct
{
  DateTime AvailabilityDate { get; }
  int CategoryId { get; }
  string Description { get; }
  ProductKind Kind { get; }
  string Name { get; }
  decimal UnitPrice { get; }
  int UnitsInStock { get; }
}
 
public ActionResult Create(IProduct product)
{
  // ...
}

図 6 のように更新した Create アクション (C# コードとして問題がない限り) によって、DefaultModelBinder では「インターフェイスのインスタンスを作成できません」という例外がスローされます。DefaultModelBinder では、作成する IProduct の具象型が把握できないことを考えれば、モデル バインダーでこの例外がスローされることは難なく理解できることです。

この問題を最も簡単に解決するには、IModelBinder インターフェイスを実装するカスタム モデル バインダーを作成します。ProductModelBinder (IProduct インターフェイスのインスタンスを作成してバインドする方法が組み込まれたカスタム モデル バインダー) を、図 7 に示します。

図 7 ProductModelBinder (密接に結合されたカスタム モデル バインダー)

public class ProductModelBinder : IModelBinder
{
  public object BindModel
    (
      ControllerContext controllerContext,
      ModelBindingContext bindingContext
    )
  {
    var product = new Product() {
      Description = GetValue(bindingContext, "Description"),
      Name = GetValue(bindingContext, "Name"),
  }; 
 
    string availabilityDateValue = 
      GetValue(bindingContext, "AvailabilityDate");

    if(availabilityDateValue != null)
    {
      DateTime date;
      if (DateTime.TryParse(availabilityDateValue, out date))
      product.AvailabilityDate = date;
    }
 
    string categoryIdValue = 
      GetValue(bindingContext, "CategoryId");

    if (categoryIdValue != null)
    {
      int categoryId;
      if (Int32.TryParse(categoryIdValue, out categoryId))
      product.CategoryId = categoryId;
    }
 
    // Repeat custom binding code for every property
    // ...
 
    return product;
  }
 
  private string GetValue(
    ModelBindingContext bindingContext, string key)
  {
    var result = bindingContext.ValueProvider.GetValue(key);
    return (result == null) ? null : result.AttemptedValue;
  }
}

IModelBinder インターフェイスを直接実装するカスタム モデル バインダーを作成する際に問題なのは、多くの場合、ロジックをほんの少し変更するだけで、DefaultModelBinder の大部分を複製することです。また、これらのカスタム バインダーでは、特定のモデル クラスに重点が置かれ、フレームワークとビジネス層間の結合が密接になり、モデルの他の型をサポートするための再利用が制限されることもよくあります。

カスタム モデル バインダーの作成においてこれらすべての問題を回避するには、DefaultModelBinder から派生すること、およびニーズに合う特定の動作のオーバーライドを検討することをお勧めします。この方法を使用することで、多くの場合、両方の長所を活かすことができます。

抽象モデル バインダー: DefaultModelBinder でインターフェイスにモデル バインディングを適用する際に発生する唯一の問題は、DefaultModelBinder ではモデルの具象型を判断できないことです。(非具象型に対するコントローラー アクションを開発して各要求の具象型を動的に判断する機能を付けるといった) 高い目標を考えてみてください。

DefaultModelBinder から派生させてターゲット モデルの型を判断するロジックだけをオーバーライドすることで、特定の IProduct シナリオに対処できるだけでなく、他のほとんどのインターフェイス階層にも対処できる汎用モデル バインダーを実際に作成することもできます。汎用モデルの抽象モデル バインダーの例を、図 8 に示します。

図 8 汎用的な抽象モデル バインダー

public class AbstractModelBinder : DefaultModelBinder
{
  private readonly string _typeNameKey;

  public AbstractModelBinder(string typeNameKey = null)
  {
    _typeNameKey = typeNameKey ?? "__type__";
  }

  public override object BindModel
  (
    ControllerContext controllerContext,
    ModelBindingContext bindingContext
  )
  {
    var providerResult =
    bindingContext.ValueProvider.GetValue(_typeNameKey);

    if (providerResult != null)
    {
      var modelTypeName = providerResult.AttemptedValue;

      var modelType =
        BuildManager.GetReferencedAssemblies()
          .Cast<Assembly>()
          .SelectMany(x => x.GetExportedTypes())
          .Where(type => !type.IsInterface)
          .Where(type => !type.IsAbstract)
          .Where(bindingContext.ModelType.IsAssignableFrom)
          .FirstOrDefault(type =>
            string.Equals(type.Name, modelTypeName,
              StringComparison.OrdinalIgnoreCase));

      if (modelType != null)
      {
        var metaData =
        ModelMetadataProviders.Current
        .GetMetadataForType(null, modelType);

        bindingContext.ModelMetadata = metaData;
      }
    }

    // Fall back to default model binding behavior
    return base.BindModel(controllerContext, bindingContext);
  }
}

インターフェイスへのモデル バインディングをサポートするには、まず、モデル バインダーではインターフェイスを具象型に変換する必要があります。これは、AbstractModelBinder で、要求の値プロバイダーから "__type__" キーを要求することで実現します。この種のデータに値プロバイダーを利用すると、"__type__" が定義されている場所である限り柔軟に処理できます。たとえば、キーは、(経路データの) 経路の一部として定義されていたり、クエリ文字列パラメーターとして指定されていたり、またはフォーム ポスト データのフィールドとして表されている可能性があります。

次に、AbstractModelBinder で具象型の名前を使用して、具象クラスの詳細を説明する新しいメタデータのセットを生成します。AbstractModelBinder は、最初の抽象モデルの型を示す既存の ModelMetadata プロパティからこの新しいメタデータに置き換えます。これにより、モデル バインダーでは、最初に非具象型に対してバインドされたことが事実上認識されなくなります。

AbstractModelBinder は、モデル メタデータを、適切なモデルにバインドするのに必要なすべての情報に置き換えたら、残りの処理を行う基本の DefaultModelBinder ロジックに制御を返します。

AbstractModelBinder は、IModelBinder インターフェイスから直接派生させることで、無駄な労力を使わずに独自のカスタム ロジックで既定のバインド ロジックを拡張できる方法を示す優れた例です。

モデル バインダーの設定

カスタム モデル バインダーの作成は、最初のステップにすぎません。カスタム モデルバインディング ロジックをアプリケーションに適用するには、カスタム モデル バインダーを登録することも必要です。ほとんどのチュートリアルで、カスタム モデル バインダーを登録する方法を 2 つ示しています。

グローバルな ModelBinders コレクション: 一般に、特定の型に対するモデル バインダーをオーバーライドするには、バインドする型のマッピングを ModelBinders.Binders ディクショナリに登録することをお勧めします。

次のコード スニペットは、AbstractModelBinder を使用して Currency モデルをバインドするフレームワークを示しています。

ModelBinders.Binders.Add(typeof(Currency), new AbstractModelBinder());

既定のモデル バインダーのオーバーライド: 別の方法として、モデル バインダーを ModelBinders.Binders.DefaultBinder プロパティに指定することで、既定のグローバル ハンドラーを置き換えることもできます。以下に例を示します。

ModelBinders.Binders.DefaultBinder = new AbstractModelBinder();

これら 2 つの方法は多くのシナリオで機能しますが、ASP.NET MVC では、属性とプロバイダーという、これとは別の 2 つの手段によって型に対するモデル バインダーを登録することも可能です。

モデルにカスタム属性を付ける

ASP.NET MVC フレームワークには、ModelBinders ディクショナリへの型マッピングの追加に加えて、抽象属性の System.Web.Mvc.CustomModelBinderAttribute (属性を適用する各クラスやプロパティのモデル バインダーを動的に作成できる属性) も用意されています。AbstractModelBinder を作成する CustomModelBinderAttribute 実装を図 9 に示します。

図 9 CustomModelBinderAttribute 実装

[AttributeUsage(
  AttributeTargets.Class | AttributeTargets.Enum |
  AttributeTargets.Interface | AttributeTargets.Parameter |
  AttributeTargets.Struct | AttributeTargets.Property,
  AllowMultiple = false, Inherited = false
)]
public class AbstractModelBinderAttribute : CustomModelBinderAttribute
{
  public override IModelBinder GetBinder()
  {
    return new AbstractModelBinder();
  }
}

これで、以下のように、AbstractModelBinderAttribute をあらゆるモデル クラスやプロパティに適用できます。

public class Product
{
  [AbstractModelBinder]
  public IEnumerable<CurrencyRequest> UnitPrice { get; set; }
  // ...
}

これにより、モデル バインダーでは、Product.UnitPrice に適切なバインダーを指定しようとする際に、AbstractModelBinderAttribute を検出し、AbstractModelBinder を使用して Product.UnitPrice をバインドするようになります。

カスタム モデル バインダーの属性を活用することは、グローバル モデル バインダーのコレクションを簡潔に保ったままモデル バインダーを構成するためのより宣言的な手法を実現できる、優れた方法です。また、モデル バインダーのカスタム属性をクラス全体と個別のプロパティの両方に適用できるということは、モデル バインディング処理において細かい制御が可能であることを意味します。

バインダーに尋ねる

モデル バインダー プロバイダーには、リアルタイムで任意のコードを実行し、特定の型に対して可能な限り最良のモデル バインダーを特定する機能が用意されています。このため、モデル バインダー プロバイダーによって、個々のモデルの型に対するモデル バインダーの明示的な登録、属性ベースの静的な登録、およびすべての型に対して定められた既定のモデル バインダーにおいてすばらしい中間点が提供されます。

以下のコードに、すべてのインターフェイスと抽象型の AbstractModelBinder を提供する IModelBinderProvider を作成する方法を示します。

public class AbstractModelBinderProvider : IModelBinderProvider
{
  public IModelBinder GetBinder(Type modelType)
  {
    if (modelType.IsAbstract || modelType.IsInterface)
      return new AbstractModelBinder();
 
    return null;
  }
}

AbstractModelBinder が特定のモデルの型に適用されるかどうかを決定するロジックは、「型が非具象型かどうか」という比較的単純なものです。非具象型の場合は、AbstractModelBinder がその型に対して適切なモデル バインダーなので、このモデル バインダーのインスタンスを作成して返します。型が具象型の場合は、AbstractModelBinder は適切ではありません。したがって、モデル バインダーがその型に合っていないことを示す null 値を返します。

.GetBinder ロジックを実装する際に留意すべきことは、このロジックが、モデル バインディングの候補になるプロパティすべてに対して実行されることです。したがって、ロジックを軽量にしておかなければ、アプリケーションでパフォーマンスの問題が発生しやすくなります。

モデル バインダー プロバイダーを使用するには、ModelBinderProviders.BinderProviders コレクションで保持されているプロバイダーの一覧に .GetBinder ロジックを追加します。たとえば、次のように AbstractModelBinder を登録します。

var provider = new AbstractModelBinderProvider();
ModelBinderProviders.BinderProviders.Add(provider);

これで簡単に、非具象型に対するモデル バインディングのサポートがアプリケーション全体に追加されました。

モデル バインディングを使用すると、フレームワークから適切なモデル バインダーを特定する負担を取り除き、その処理を最適な場所 (モデル バインダー自体) に配置することで、モデル バインディングの設定がはるかに動的になります。

重要な拡張ポイント

他のメソッドと同様に、ASP.NET MVC モデル バインディングを使用すると、コントローラー アクションでは、複合オブジェクトの型をパラメーターとして受け取ることができます。また、モデル バインディングでは、作成したオブジェクトを使用するロジックからオブジェクトを作成するロジックを切り離すことで、懸案事項の分離強化も促されます。

この記事では、モデル バインディング フレームワークを最大限に活用できるよう、重要な拡張ポイントをいくつか説明しました。ASP.NET MVC モデル バインディングとモデル バインディングの適切な使用方法を、時間をかけて理解すれば、最も単純なアプリケーションでも大きな影響を与えることが可能になります。

Jess Chadwick は、Web テクノロジを専門とする独立系ソフトウェア コンサルタントで、Fortune 500 社の企業において、スタートアップの組み込みデバイスからエンタープライズ規模の Web ファームまで及ぶ 10 年以上の開発経験があります。彼は、ASP の事情通で、ASP.NET の Microsoft MVP であり、書籍や雑誌の著者でもあります。開発コミュニティに積極的に参加し、.NET ユーザー グループの NJDOTNET Central New Jersey を率いるとともに、ユーザー グループやカンファレンスで定期的に講演しています。

この記事のレビューに協力してくれた技術スタッフの Phil Haack に心より感謝いたします。