ASP.NET MVC

Особенности и слабые стороны связывания моделей в ASP.NET MVC

Джесс Чэдвик

Продукты и технологии:

ASP.NET MVC

В статье рассматриваются:

• основы связывания моделей;

• связывание со сложными объектами;

• исследование частей инфраструктуры;

• рекурсивное связывание моделей;

• ограничения связывания моделей;

• использование собственных атрибутов.

Связывание моделей в ASP.NET MVC упрощает операции контроллера за счет введения уровня абстракции, который автоматически заполняет параметры операций контроллера, берет на себя рутинное сопоставление свойств и преобразование типов, т. е. все то, что часто требуется при работе с данными запроса ASP.NET. Хотя связывание моделей кажется простым, на самом деле это сравнительно сложная инфраструктура, состоящая из нескольких частей, которые взаимодействуют друг с другом при создании и заполнении объектов, нужных для ваших операций контроллера.

В этой статье мы погрузимся глубоко в сердцевину подсистемы связывания моделей в ASP.NET MVC, рассмотрим каждый уровень инфраструктуры связывания и различные способы расширения логики связывания в соответствии с требованиями вашего приложения. Попутно вы увидите несколько методик связывания, которые часто упускают из виду, а также узнаете, как избежать некоторых наиболее распространенных ошибок при связывании моделей.

Основы связывания моделей

Чтобы понять, что такое связывание модели, сначала взгляните на типичный способ заполнения объекта значениями из запроса в приложении ASP.NET (рис. 1). А потом сравните действия на рис. 1 с операциями на рис. 2, где для достижения той же цели используется связывание модели.

Хотя в обоих примерах результат одинаков (заполненный экземпляр Product), код на рис. 2 полагается на ASP.NET MVC в преобразовании значений из запроса в строго типизированные значения. При использовании связывания модели в разработке операций контроллера основное внимание можно уделять прикладной логике и не тратить время на рутинные действия, связанные с сопоставлением и разбором запроса.

Рис. 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. Связывание модели с элементарными значениями

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,
 };
 
 // ...
}

Связывание со сложными объектами

Связывание модели даже с простыми, элементарными типами может оказать весьма заметное влияние, а ведь многие операции контроллера используют больше пары параметров. К счастью, ASP.NET MVC обрабатывает сложные типы так же хорошо, как элементарные.

В следующем коде используется еще одна передача данных операции Create с пропуском элементарных значений и прямой привязкой к классу Product:

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

И вновь этот код дает тот же результат, что и действия, показанные на рис. 1 и 2, только на этот раз никакого кода нет вообще: модель связывания в ASP.NET MVC позволила исключить весь стереотипный код, необходимый для создания и заполнения нового экземпляра Product. Этот код демонстрирует настоящую мощь связывания модели.

Разбираем связывание моделей на части

Теперь, когда вы видели связывание модели в действии, самое время разобрать ее инфраструктуру на части.

Процесс связывания осуществляется в два этапа: получение значений из запроса и заполнение моделей этими значениями. Эти этапы выполняются соответственно провайдерами значений (value providers) и механизмами связывания моделей (model binders).

Провайдеры значений

ASP.NET MVC включает реализации провайдеров значений, которые охватывают наиболее распространенные источники значений запросов, такие как параметры querystring, поля форм и данные маршрута (route data). В период выполнения ASP.NET MVC использует провайдеры значений, зарегистрированные в классе ValueProviderFactories, для оценки значений запроса, которыми смогут пользоваться механизмы связывания моделей.

По умолчанию набор провайдеров значений оценивает значения из различных источников в следующем порядке.

  1. Ранее связанные параметры операции, если она является дочерней.
  2. Поля формы (Request.Form).
  3. Значения свойств в теле JSON Request (Request.InputStream), но только когда запрос является AJAX-запросом.
  4. Данные маршрута (RouteData.Values).
  5. Параметры querystring (Request.QueryString).
  6. Переданные файлы (Request.Files).

Создавать собственные провайдеры значений довольно легко, но будьте осторожны, делая это.

Набор провайдеров значений вроде объекта Request на самом деле является словарем — уровнем абстракции пар «ключ-значение», которые механизмы связывания модели могут использовать, не зная, откуда исходят данные. Однако инфраструктура провайдеров значений выводит эту абстракцию на уровень выше, чем словарь Request, давая вам полный контроль над тем, как и где эта инфраструктура получает свои данные. Вы даже можете создавать собственные провайдеры значений.

Собственные провайдеры значений

Минимальное требование при создании собственного провайдера значений достаточно простое: создать новый класс, реализующий интерфейс System.Web.Mvc.ValueProviderFactory.

Например, на рис. 3 показан собственный провайдер значений, который извлекает значения из файлов cookie пользователя.

Рис. 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, делает доступным большинство данных в HttpRequest (кроме, возможно, файлов cookie) и, в целом, предоставляет достаточно данных во многих сценариях использования.

Чтобы решить, стоит ли создавать новый провайдер значений в вашей конкретной ситуации, задайте себе следующий вопрос: содержит ли набор информации, предоставляемый существующими провайдерами значений, все необходимые данные (пусть и не в подходящем формате)?

Если ответ — нет, тогда добавление собственного провайдера значений, по-видимому, будет правильным для заполнения пробелов. Однако при положительном ответе, как чаще и бывает, подумайте о возможности восполнения недостающих частей подстройкой связывания модели для доступа к данным, предоставляемым провайдерами значений. В оставшейся части статье будет показано именно это.

Основной компонент инфраструктуры связывания модели в ASP.NET MVC, отвечающий за создание и заполнение моделей с использованием значений от провайдеров, называют механизмом связывания моделей (model binder).

Исходный механизм связывания моделей

Инфраструктура ASP.NET MVC включает реализацию исходного механизма связывания моделей с именем DefaultModelBinder, которая предназначена для эффективного связывания большинства типов моделей. Достигается это использованием сравнительно простой и рекурсивной логики для каждого свойства целевой модели.

  1. Выполняется анализ провайдеров значений, чтобы определить, как было распознано свойство — как простой или сложный тип. Для этого проверяется, зарегистрировано ли имя свойства как префикс. Префикс — это не более чем имя поля формы HTML в «нотации с точкой», сообщающее, является ли значение свойством сложного объекта. Шаблон префикса выглядит так: [ParentProperty].[Property]. Например, поле формы с именем UnitPrice.Amount содержит значение для поля Amount свойства UnitPrice.
  2. Для имени этого свойства из зарегистрированных провайдеров извлекается ValueProviderResult.
  3. Если значение является простым типом, предпринимается попытка преобразовать его в целевой тип. Исходная логика преобразования использует TypeConverter свойства для конвертации исходного значения типа string в целевой тип.
  4. В ином случае свойство является сложным типом, и выполняется рекурсивное связывание.

Рекурсивное связывание моделей

Рекурсивное связывание моделей, по сути, начинает весь процесс связывания заново, но при этом для имени целевого свойства используется новый префикс. При таком подходе DefaultModelBinder способен проходить целые графы сложных объектов и заполнять значения даже глубоко вложенных свойств.

Чтобы увидеть рекурсивное связывание в действии, измените Product.UnitPrice с простого типа decimal на пользовательский тип Currency. Оба класса показаны на рис. 4.

Рис. 4. Класс Product со сложным свойством Unitprice

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; }
}

После этого обновления механизм связывания будет искать значения с именами UnitPrice.Amount и UnitPrice.Code, чтобы заполнить сложное свойство Product.UnitPrice.

Рекурсивная логика связывания DefaultModelBinder может заполнять даже самые сложные графы объектов. До сих пор вы видели сложный объект, который находится всего на один уровень глубже в иерархии объектов, и такой вариант DefaultModelBinder обработал с легкостью. Чтобы почувствовать настоящую мощь рекурсивного связывания, добавьте в Product новое свойство с именем Child того же типа — Product:

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

Затем добавьте новое поле на форму и, применяя нотацию с точкой для обозначения каждого уровня, создайте столько уровней, сколько вам нужно. Например:

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

Это поле формы приведет к созданию шести уровней 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!"
            }
          }
        }
      }
    }
  }
}

Хотя в этом надуманном примере задается значение всего одного свойства, он отлично демонстрирует, как функциональность рекурсивного связывания в DefaultModelBinder изначально позволяет поддерживать некоторые очень сложные графы объектов. В случае рекурсивного связывания, если вы можете создать имя поле формы, представляющее значение для заполнения, месторасположение этого значения в иерархии объектов не имеет значения — механизм связывания найдет его и выполнит привязку.

Где связывание моделей якобы не работает

Это верно, что некоторые модели DefaultModelBinder просто не сумеет связать. Однако есть довольно много ситуаций, в которых кажется, что исходная логика связывания модели может не сработать, но на деле отлично работает, если вы правильно используете ее.

Ниже рассматривается несколько наиболее распространенных сценариев, с которыми, как часто полагают разработчики, DefaultModelBinder не справляется. Однако они могут быть реализованы с применением DefaultModelBinder безо всяких дополнительных средств.

Сложные наборы Готовые провайдеры значений в ASP.NET MVC интерпретируют все имена полей запроса так, будто они являются значениями передачи формы (form post). Возьмем, к примеру, набор элементарных значений в передаче формы, где каждое значение требует свой уникальный индекс (пробелы добавлены для большей читаемости):

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

JSON-запросы должны соответствовать синтаксису именования передачи формы.

Тот же подход применим и к наборам сложных объектов. Чтобы убедиться в этом, обновите класс Product для поддержки нескольких валют (currencies), изменив тип свойства UnitPrice на набор объектов Currency:

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-массива для этих данных выглядел бы так:

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

Однако исходные провайдеры значений и механизмы связывания моделей требуют, чтобы данные были представлены в виде передачи формы JSON (JSON form post):

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

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

По-видимому, ситуация с набором сложных объектов — одна из наиболее проблематичных, с которой сталкиваются разработчики, потому что требуемый синтаксис далеко не очевиден. Однако, как только вы изучите сравнительно простой синтаксис для передачи сложных наборов, вам станет гораздо легче справляться с этими ситуациями.

Обобщенные собственные механизмы связывания моделей Хотя DefaultModelBinder — достаточно мощное средство, способное справиться почти со всем, что вы ему подсовываете, иногда он не в состоянии делать именно то, что вам нужно. Попадая в такие ситуации, многие разработчики хватаются за модель расширения инфраструктуры связывания и создают собственный механизм связывания модели.

Например, хотя Microsoft .NET Framework обеспечивает великолепную поддержку объектно-ориентированных принципов, в DefaultModelBinder нет никакой поддержки связывания абстрактных базовых классов и интерфейсов. Чтобы увидеть воочию этот недостаток, переработайте класс Product так, чтобы он наследовал от интерфейса с именем IProduct, который состоит из свойств только для чтения. Аналогично обновите операцию контроллера Create, чтобы она принимала новый интерфейс IProduct вместо конкретной реализации Product, как показано на рис. 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)
{
  // ...
}

Обновленная операция Create (рис. 6), хоть и написана как совершенно правильный C#-код, заставляет DefaultModelBinder сгенерировать исключение «Cannot create an instance of an interface» («Создать экземпляр интерфейса нельзя»). Вполне понятно, что механизм связывания модели генерирует это исключение, учитывая, что у DefaultModelBinder нет возможности узнать, какой конкретный тип IProduct он должен создать.

Самый простой способ решить эту проблему — создать собственный механизм связывания модели, который реализует интерфейс IModelBinder interface. На рис. 7 показан ProductModelBinder — собственный механизм связывания модели, которому известно, как создать и связать экземпляр интерфейса IProduct.

Рис. 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 и замещая лишь ту логику, которая определяет тип целевой модели, вы можете не только справиться с конкретным сценарием 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__». Например, этот ключ можно было бы определять как часть маршрута (route) (в данных маршрута), указывать как параметр querystring или даже представлять как некое поле в данных передачи формы.

Для поддержки связывания модели с интерфейсом механизм связывания должен сначала преобразовать интерфейс в конкретный тип.

Далее AbstractModelBinder использует имя конкретного типа для генерации нового набора метаданных, описывающих детали конкретного класса. Эти метаданные используются AbstractModelBinder для замены существующего свойства ModelMetadata, которое описывает начальный абстрактный тип модели, в конечном счете заставляя механизм связывания забыть, что он когда-то работал с неконкретным типом.

После того как AbstractModelBinder заменяет метаданные модели всей информацией, необходимой для связывания с подходящей моделью, он просто возвращает управление логике базового DefaultModelBinder для выполнения остальной работы.

AbstractModelBinder — отличный пример, показывающий, как можно расширить исходную логику связывания с помощью собственной логики — без изобретения колеса, напрямую наследуя от интерфейса IModelBinder.

Выбор механизма связывания модели

Создание собственных механизмов связывания моделей — это лишь первый шаг. Чтобы применить логику собственного механизма в своем приложении, вы должны зарегистрировать его. В большинстве руководств показываются два способа регистрации собственных механизмов связывания моделей..

Глобальный набор ModelBinders Обычно рекомендуемый способ переопределения механизма связывания модели для специфических типов — регистрация сопоставления «тип-механизм» в словаре ModelBinders.Binders.

В следующем фрагменте кода инфраструктуре указывается использовать AbstractModelBinder для связывания моделей Currency:

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

Замещение исходного механизма связывания модели В качестве альтернативы, чтобы заменить глобальный исходный обработчик, вы можете присвоить механизм связывания модели свойству ModelBinders.Binders.DefaultBinder. Например:

ModelBinders.Binders.DefaultBinder = new AbstractModelBinder();

Хотя эти два подхода отлично работают во многих сценариях, в ASP.NET MVC есть еще два способа, позволяющих зарегистрировать механизм связывания модели для какого-либо типа: атрибуты и провайдеры.

Дополнение моделей собственными атрибутами

Помимо добавления сопоставления типов в словарь ModelBinders, инфраструктура ASP.NET MVC предлагает абстрактный атрибут System.Web.Mvc.CustomModelBinderAttribute, позволяющий динамически создавать механизм связывания модели для каждого класса или свойства, к которому применен этот атрибут. На рис. 9 показана реализация CustomModelBinderAttribute, создающая AbstractModelBinder.

Рис. 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.

Если вы уделите время изучению связывания моделей в ASP.NET MVC и тому, как правильно им пользоваться, то сможете получить серьезный выигрыш — даже в простейшем приложении.

Применение атрибутов — отличный способ добиться более декларативного подхода к конфигурированию механизмов связывания моделей и в то же время не усложнять глобальный набор таких механизмов. Кроме того, возможность применения атрибутов собственных механизмов связывания моделей как к целым классам, так и к индивидуальным свойствам дает вам средства более тонкого управления процессов связывания моделей.

Спросите у самих механизмов связывания!

Провайдеры механизмов связывания моделей (model binder providers) дают возможность выполнять произвольный код в реальном времени, чтобы определить лучший механизм связывания для данного типа. Как таковые, они являются отличным компромиссом между явной регистрацией механизмов связывания моделей, статической регистрацией на основе атрибутов и набором исходных механизмов связывания моделей для всех типов.

В следующем коде показано, как создать IModelBinderProvider, предоставляющий AbstractModelBinder для всех интерфейсов и абстрактных типов:

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. Например, зарегистрируйте AbstractModelBinder так:

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

И вот так легко вы добавили в свое приложение поддержку связывания моделей для неконкретных типов.

Этот подход позволяет обеспечить гораздо более динамичный выбор механизмов связывания моделей, снимая бремя определения подходящего механизма с инфраструктуры и перекладывая его на сами механизмы связывания.

Ключевые точки расширения

Как и любой другой метод, связывание моделей в ASP.NET MVC позволяет операциям контроллера принимать сложные объектные типы как параметры. Связывание моделей также способствует лучшему разделению обязанностей за счет отделения логики заполнения объектов от логики, использующей эти заполненные объекты.

Я показал вам некоторые ключевые точки расширения в инфраструктуре связывания моделей, которые помогут вам использовать их на полную катушку. Если вы уделите время изучению связывания моделей в ASP.NET MVC и тому, как правильно им пользоваться, то сможете получить серьезный выигрыш — даже в простейшем приложении.


Джесс Чэдвик (Jess Chadwick) — независимый консультант по программному обеспечению со специализацией в области веб-технологий. Имеет более чем десятилетний опыт разработок — от программирования встраиваемых устройств в начинающих компаниях до создания веб-ферм в компаниях из списка «Fortune 500». Обладатель званий ASPInsider, Microsoft MVP в области ASP.NET; также пишет книги и статьи в журналах. Активно участвует в жизни сообщества разработчиков, регулярно выступает на собраниях групп пользователей и конференциях, а также возглавляет группу пользователей «NJDOTNET Central New Jersey .NET».

Выражаю благодарность за рецензирование статьи эксперту: Филу Хааку (Phil Haack).