Главная страница   /   10.1. Создание пользовательского механизма связывания данных модели (ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

Джеффри Палермо

10.1. Создание пользовательского механизма связывания данных модели

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

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

// До
public ViewResult Edit(Guid id)
{
	var profile = _profileRepository.GetById(id);
	return View(new ProfileEditModel(profile));
}
// После
public ViewResult Edit(Profile id)
{
	return View(new ProfileEditModel(id));
}

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

Чтобы ее реализовать, мы должны предоставить и пользовательский провайдер механизма связывания, и пользовательский механизм связывания. Провайдеры используются MVC Framework для определения того, какой из механизмов связывания нужно использовать для связывания данных модели. Чтобы предоставить механизм связывания для данного типа, провайдеру достаточно вернуть экземпляр механизма связывания. Если провайдер не может предоставить механизм связывания для данного типа, он возвращает null.

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

public interface IModelBinderProvider
{
	IModelBinder GetBinder(Type modelType);
}

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

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

Листинг 10-1: Наш пользовательский провайдер связывания данных
public class EntityModelBinderProvider : IModelBinderProvider
{
	public IModelBinder GetBinder(Type modelType)
	{
		if (!typeof(Entity).IsAssignableFrom(modelType))
			return null;
		return new EntityModelBinder();
	}
}

Наш новый пользовательский провайдер механизма связывания реализует интерфейс IModelBinderProvider, который содержит только один метод, GetBinder. Сначала мы проверяем параметр modelType, чтобы определить, наследуется ли тип модели от нашего базового типа Entity. Если нет, провайдер механизма связывания возвращает null, указывая, что данный провайдер механизма связывания не может предоставить механизм связывания для данного типа. Если модель наследует от базового типа Entity, мы возвращаем новый экземпляр EntityModelBinder. Рисунок 10-1 иллюстрирует отношения между этими интерфейсами и классами.

Рисунок 10-1: Диаграмма классов EntityModelBinderProvider и EntityModelBinder

Теперь, когда у нас есть новый провайдер механизма связывания, который может устанавливать соответствия для более чем одного типа, давайте обратим внимание на наш новый механизм связывания для загрузки постоянных объектов. Этот новый механизм связывания будет реализацией интерфейса IModelBinder. Ему необходимо будет выполнить ряд действий, чтобы вернуть корректный сохраненный объект:

  • Получить значение запроса из связующего контекста
  • Обработать отсутствующие значения запроса
  • Создать правильное хранилище
  • Использовать хранилище для загрузки объекта и его возврата

Мы не будем подробно рассматривать третий пункт (создание хранилища), так как этот пример предполагает наличие контейнера Inversion of Control (IoC) (обсуждается далее в главе 18).

Весь механизм связывания должен реализовывать интерфейс IModelBinder.

Листинг 10-2: EntityModelBinder
public class EntityModelBinder : IModelBinder
{
	public object BindModel(
		ControllerContext controllerContext,
		ModelBindingContext bindingContext)
	{
		ValueProviderResult value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
		if (value == null)
			return null;
		if (string.IsNullOrEmpty(value.AttemptedValue))
			return null;

		int entityId;
		if (!int.TryParse(value.AttemptedValue, out entityId))
			return null;

		Type repositoryType = typeof(IRepository<>).MakeGenericType(bindingContext.ModelType);
		var repository = (IRepository)ServiceLocator.Resolve(repositoryType);
		Entity entity = repository.GetById(entityId);

		return entity;
	}
}

Строка 7: Получает значение запроса

Строки 8-11: Возвращается, если не указано значение

Строки 14-15: Конвертирует значение в int

Строки 17-19: Получает репозиторий

В листинге 10-2 мы реализовываем интерфейс механизма связывания IModelBinder. Во-первых, мы должны реализовать метод BindModel по инструкции, данной ранее. Мы извлекаем значение из запроса ModelBindingContext, переданного в метод BindModel. Свойство ValueProvider можно использовать для извлечения экземпляров ValueProviderResult, которые представляют данные из формы, данные о маршруте и строку запроса. Если нет ValueProviderResult с тем же именем, что и наш параметр действия, мы не будем пытаться получить объект из хранилища. Хотя идентификатор объекта является целым числом, искомое значение является строкой, поэтому мы создаем новый тип int из имеющегося значения для ValueProviderResult.

Как только мы разобрали (распарсили ) integer из запроса, мы можем создать соответствующие хранилище из контейнера IoC. Поскольку у нас есть конкретные хранилища для каждого вида объектов, мы не будем знать конкретный тип репозитория во время компиляции. Но все наши хранилища реализуют общий интерфейс, как показано здесь.

public interface IRepository<TEntity> where TEntity : Entity
{
	TEntity Get(int id);
}

Мы хотим, чтобы контейнер IoC создал правильное хранилище для типа объекта, к которому мы применяем связывание. Это значит, что нам нужно создать правильный объект Type для IRepository, который мы создаем. Это можно сделать, используя метод Type.MakeGenericType для создания закрытого родового типа (дженерик-типа) из открытого родового типа IRepository<>.

Открытые и закрытые родовые типы (generics)

Открытый родовой тип - это родовой тип, который не имеет параметров типа. IList<> и IDictionary<> - открытые родовые типы. Закрытый родовой тип представляет собой родовой тип с параметрами типа, например, IList <int> и IDictionary <string, User>.

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

Когда свойство ModelBindingContext.ModelType ссылается на закрытый родовой тип для IRepository, мы можем с помощью контейнера IoC создать экземпляр хранилища, который будем далее использовать.

Наконец мы вызываем метод Get хранилища и возвращаем извлеченный объект из BindModel. Так как мы не можем вызвать родовой метод во время выполнения без использования рефлексии, мы используем другой не-дженерик интерфейс IRepository, который возвращает только такие объекты, как Entity, как показано здесь.

public interface IRepository
{
	Entity Get(int id);
}

Все хранилища в нашей системе наследуются от общего базового класса хранилища, который реализует и родовые, и не родовые IRepository. Так как не везде могут содержаться ссылки на родовой интерфейс (как мы уже видели в связывании данных модели), дополнительный не родовой интерфейс IRepository поддерживает эти сценарии.

У нас есть и EntityModelBinderProvider и EntityModelBinder, который связывается с объектами из значения запроса, но нам еще осталось настроить ASP.NET MVC, чтобы использовать новый провайдер механизма связывания. Для этого мы добавляем его в список доступных провайдеров механизмов связывания в свойство ModelBinderProviders.Binder в Application_Start, как показано здесь.

protected void Application_Start()
{
	ModelBinderProviders.BinderProviders.Add(new EntityModelBinderProvider());
}

На данный момент у нас есть только один пользовательский провайдер механизма связывания. На практике у нас могут быть специализированные механизмы связывания для определенных объектов, классов объектов (например, классы перечисления) и так далее. Во время выполнения ASP.NET MVC оценивает каждый провайдер механизма связывания по порядку, и провайдер механизма связывания по умолчанию выполняется последним. Создавая механизмы связывания для объектов, мы можем создать действия контроллера, которые принимают объекты в качестве параметров, а не просто целые числа, как показано здесь.

public ViewResult Edit(Profile id)
{
	return View(new ProfileEditModel(id));
}

Когда у нас есть EntityModelBinder, мы не повторяем код в действии контроллера. Теперь нам проще создать экран Edit, показанный на рисунке 10-2, без постоянного поиска данных в хранилище. Этот повторяющийся код доступа к данным сделал бы неясным назначение действия контроллера, так как он не имеет непосредственного отношения к тому, что он делает.

Рисунок 10-2: Для экрана Edit теперь не нужно загружать профиль вручную

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

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