Настройка системы привязки моделей

96

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

Создание специального поставщика значений

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

namespace System.Web.Mvc
{
    public interface IValueProvider
    {
        bool ContainsPrefix(string prefix);
        ValueProviderResult GetValue(string key);
    }
}

Метод ContainsPrefix() вызывается связывателем модели с целью определения может ли поставщик значений обеспечить данные для указанного префикса. Метод GetValue() возвращает значение для указанного ключа данных либо null, если поставщик не имеет подходящих данных.

В пример проекта добавлена папка Infrastructure, а в ней создан новый файл класса по имени CountryValueProvider.cs, который будет использоваться для предоставления значений, предназначенных свойству Country. Содержимое этого файла приведено в примере ниже:

using System.Globalization;
using System.Web.Mvc;

namespace MvcModels.Infrastructure
{
    public class CountryValueProvider : IValueProvider
    {
        public bool ContainsPrefix(string prefix)
        {
            return prefix.ToLower().IndexOf("country") > -1;
        }

        public ValueProviderResult GetValue(string key)
        {
            if (ContainsPrefix(key))
            {
                return new ValueProviderResult("Россия", "Россия",
                    CultureInfo.InvariantCulture);
            }
            else
            {
                return null;
            }
        }
    }
}

Этот поставщик значений отвечает только на запросы значений для свойства Country и всегда возвращает значение "Россия". Для всех прочих запросов возвращается null, указывая на невозможность предоставления данных.

Значение данных должно возвращаться в виде экземпляра класса ValueProviderResult. Конструктор этого класса принимает три параметра. Первый из них - это элемент данных, который необходимо ассоциировать с запрошенным ключом. Второй параметр представляет версию значения данных, безопасную для отображения как часть HTML-страницы. В третьем параметре передается информация о культуре, относящейся к значению; здесь указано InvariantCulture.

Чтобы зарегистрировать поставщик значений для приложения, понадобится построить класс фабрики, который будет создавать экземпляры поставщика, когда они окажутся затребованными инфраструктурой ASP.NET MVC Framework. Класс фабрики должен быть унаследован от абстрактного класса ValueProviderFactory. В примере ниже показано содержимое файла класса CustomValueProviderFactory.cs, добавленного в папку Infrastructure:

using System.Web.Mvc;

namespace MvcModels.Infrastructure
{
    public class CustomValueProviderFactory : ValueProviderFactory
    {
        public override IValueProvider GetValueProvider(
            ControllerContext controllerContext)
        {
            return new CountryValueProvider();
        }
    }
}

Метод GetValueProvider() вызывается, когда связывателю модели необходимо получить значения для процесса привязки. В этой реализации просто создается и возвращается экземпляр класса CountryValueProvider, но данные, передаваемые через параметр ControllerContext, можно использовать для реагирования на различные виды запросов путем создания разных поставщиков значений.

Класс фабрики должен быть зарегистрирован для приложения, что делается в методе Application_Start() файла Global.asax, как показано в примере ниже:

using System.Web.Mvc;
using System.Web.Routing;
using MvcModels.Infrastructure;

namespace MvcModels
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            ValueProviderFactories.Factories.Insert(
                index: 0,
                item: new CustomValueProviderFactory());
        }
    }
}

Класс фабрики регистрируется путем добавления экземпляра в статическую коллекцию ValueProviderFactories.Factories. Связыватель модели просматривает поставщики значений последовательно, а это значит, что специальная фабрика с помощью метода Insert() должна быть помещена в первую позицию коллекции поставщиков значений, поддерживаемую приложением, если требуется, чтобы она получила преимущество перед встроенными поставщиками.

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

ValueProviderFactories.Factories.Add(new CustomValueProviderFactory());

В примере ниже специальный поставщик значений должен применяться перед всеми остальными поставщиками, поэтому использовался метод Insert(). Прежде чем можно будет протестировать этот поставщик значений, понадобится модифицировать метод действия Address(), чтобы связыватель модели не просматривал данные формы на предмет значений для свойств модели. В примере ниже из вызова метода TryUpdateModel() было удалено ограничение, установленное на источнике значений.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Web.Mvc;
using MvcModels.Models;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        // ...

        public ActionResult Address()
        {
            List<AdressSummary> addresses = new List<AdressSummary>();
            UpdateModel(addresses);
            return View(addresses);
        }
	}
}

Чтобы понаблюдать за работой специального поставщика значений, необходимо запустить приложение и перейти на URL вида /Home/Address. Введите данные для городов и стран, после чего щелкните на кнопке "Отправить". Вы увидите, что для генерации значений, предназначенных свойству Country, в каждом объекте AddressSummary, который был создан связывателем модели, использовался специальный поставщик значений, что объясняется его преимуществом перед встроенными поставщиками:

Результат работы специального поставщика значений

Создание специального связывателя модели

Поведение стандартного связывателя модели можно переопределить, создав специальный связыватель модели для специфического типа. Специальные связыватели моделей реализуют интерфейс IModelBinder, который был показан ранее. Чтобы продемонстрировать создание специального связывателя, в папку Infrastructure добавляется файл класса AddressSummaryBinder.cs с содержимым, приведенным в примере ниже:

using System.Web.Mvc;
using MvcModels.Models;

namespace MvcModels.Infrastructure
{
    public class AddressSummaryBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext,
                ModelBindingContext bindingContext)
        {
            AdressSummary model = (AdressSummary)bindingContext.Model
                ?? new AdressSummary();
            model.City = GetValue(bindingContext, "City");
            model.Country = GetValue(bindingContext, "Country");
            return model;
        }

        private string GetValue(ModelBindingContext context, string name)
        {
            name = (context.ModelName == "" ? "" : context.ModelName + ".") + name;

            ValueProviderResult result = context.ValueProvider.GetValue(name);
            if (result == null || result.AttemptedValue == "")
            {
                return "<Не указано>";
            }
            else
            {
                return (string)result.AttemptedValue;
            }
        }
    }
}

Инфраструктура ASP.NET MVC Framework вызывает метод BindModel(), когда ей требуется экземпляр типа модели, которую поддерживает связыватель. Вскоре будет показано, как зарегистрировать связыватель модели, но класс AddressSummaryBinder применяется только для создания экземпляров класса AddressSummary, что намного упрощает код (конечно, можно создавать специальные связыватели, которые поддерживают множество типов, но предпочтительнее иметь для каждого типа свой связыватель).

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

В качестве параметров метод BindModel() принимает объект ControllerContext, который можно использовать для получения деталей о текущем запросе, и объект ModelBindingContext, который предоставляет детали об искомом объекте модели, а также доступ к остальным средствам привязки моделей в приложении MVC. Наиболее полезные свойства класса ModelBindingContext описаны в таблице:

Наиболее полезные свойства класса ModelBindingContext
Свойство Описание
Model

Возвращает объект модели, переданный методу UpdateModel(), если привязка была вызвана вручную

ModelName

Возвращает имя привязываемой модели

ModelType

Возвращает тип привязываемой модели

ValueProvider

Возвращает реализацию IValueProvider, которая может использоваться для получения значений данных из запроса

Специальный связыватель модели довольно прост. Когда вызывается метод BindModel(), производится проверка, установлено ли свойство Model объекта ModelBindingContext. Если оно установлено, то это объект, для которого будет генерироваться значение данных, а если нет, то создается новый экземпляр класса AddressSummary. Значения для свойств City и Country получаются путем вызова метода GetValue(), после чего возвращается заполненный объект AddressSummary.

В методе GetValue() с помощью реализации IValueProvider, извлеченной из свойства ModelBindingContext.ValueProvider, получаются значения для свойств объекта модели.

Свойство ModelName сообщает, имеется ли префикс, который необходимо добавить к имени искомого свойства. Как вы помните, метод действия пытается создать коллекцию объектов AddressSummary, а это означает, что индивидуальные элементы <input> будут иметь значения атрибутов name, снабженные префиксами [0] и [1]. В запросе будет производиться поиск значений [0].City, [0].Country и т.д. В качестве финального шага предоставляется стандартное значение "<Не указано>", если не удается найти значение для свойства или свойство является пустой строкой (которая отправляется серверу, когда пользователь не вводил значение в элементе <input> формы).

Регистрация специального связывателя модели

Специальный связыватель модели должен быть зарегистрирован, чтобы приложению MVC было известно, какие типы он может поддерживать. Это делается в методе Application_Start() файла Global. asax:

using System.Web.Mvc;
using System.Web.Routing;
using MvcModels.Infrastructure;

namespace MvcModels
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            
            /*ValueProviderFactories.Factories.Insert(
                index: 0,
                item: new CustomValueProviderFactory());*/

            ModelBinders.Binders.Add(typeof(MvcModels.Models.AdressSummary),
                new AddressSummaryBinder());
        }
    }
}

Связыватель регистрируется с помощью метода ModelBinders.Binders.Add(), которому передается тип, поддерживаемый связывателем, и экземпляр класса связывателя. Обратите внимание на то, что оператор, который регистрирует специальный поставщик значений, был удален. Чтобы протестировать специальный связыватель модели, запустите приложение, перейдите на URL вида /Home/Address и заполните только некоторые элементы формы. При отправке формы специальный связыватель модели будет использовать строку "<Не указано>" для всех свойств, для которых не было введено значение:

Результат работы специального связывателя модели

Специальный связыватель модели можно также зарегистрировать путем декорирования класса модели атрибутом ModelBinder, что устраняет необходимость использования файла Global.asax. В примере ниже показано, как указать AddressSummaryBinder в качестве связывателя для класса AddressSummary:

using System.Web.Mvc;
using MvcModels.Infrastructure;

namespace MvcModels.Models
{
    [ModelBinder(typeof(AddressSummaryBinder))]
    public class AdressSummary
    {
        public string City { get; set; }
        public string Country { get; set; }
    }
}
Пройди тесты
Лучший чат для C# программистов