В основе коммуникации веб приложений лежит HTTP протокол передачи сообщений, который представляет собой строковые значения. Но в ASP.NET MVC приложениях было бы крайне неудобно получать значения запроса из строковых пар "ключ"-"значение", поэтому создали механизм связывания строковых пар запроса и строго типизированных объектов - model binding. Благодаря этому механизму мы можем перейти от подхода, когда нужно забирать значения вручную из объекта запроса, к подходу, когда получаем готовый типизированный объект и можем с ним работать. На примере все более наглядно показано:
public class Movie
{
public int Id { get; private set; }
public string Title { get; set; }
public string Director { get; set; }
public double Rating { get; set; }
public int Year { get; set; }
}
Получение модели фильма без использования model binding:
[HttpPost]
public ActionResult Update()
{
var movie = new Movie
{
Title = Request.Form["Title"],
Director = Request.Form["Director"],
Rating = double.Parse(Request.Form["Rating"]),
Year = int.Parse(Request.Form["Year"])
};
return this.View();
}
Получение модели фильма с ипользованием model binding:
[HttpPost]
public ActionResult Update(Movie movie)
{
//some actions with movie
return this.View();
}
В примере, где получаем модель из объекта запроса возможны так же ошибки, к примеру : если опечататься в названии ключа данных, то ошибку уже получим в момент выполнения кода, при построении проекта ошибки не будет видно. Так же связывание модели актуально и для более простых типов.
Алгоритм связывания данных и модели
Алгоритм приведен для пользовательского типа данных, в нашем случае это - Movie.
- Полученный запрос, механизм маршрутизации направляет в определенный контроллер и действие.
- Проводится анализ сигнатуры действия контроллера и сопоставляются параметры. Те параметры, что будут совпадать по имени, будут сопоставлены в обход привязки модели. Из оставшихся данных будет составлена модель, которую ожидает действие контролера.
- Создается объект необходимой модели. Модель обязательно должна иметь конструктор по-умолчанию без параметров и без опциональных параметров.
- Заполняются все поля модели, которые имеют первый уровень вложенности.
- Для оставшихся полей производится создание моделей второго уровня и заполнение, начиная с третьего пункта алгоритма.
Выборочная привязка модели и ограничение привязки модели
В некоторых случаях нам нет необходимости заполнять полностью все поля модели. Для исключения их из списка заполняемых, можно использовать атрибут Bind для параметров действия. Пример указания, что в модели Movie необходимо заполнить только Title и Year :
[HttpPost]
public ActionResult Change([Bind(Include = "Title, Year")]Movie movie)
{
//some actions
return this.View();
}
Пример указания, что в модели должны быть заполнены все поля исключая Director :
[HttpPost]
public ActionResult SecondChange([Bind(Exclude = "Director")]Movie movie)
{
//some actions
return this.View();
}
Атрибут Bind можно так же указать на сам класс модели и указать необходимые ограничения. Данная техника может пригодится в случае, если у вас в модели есть поля флаги, которые могу управлять какими-то действиям (запись в базу публикация на сайте рассылка уведомлений). Исключение из заполнения на уровне контроллера защищает от подстановки значения злоумышленником и выполнения дополнительных действий. Но это лишь один из возможных примеров.
Альтернативным вариантом решения данной задачи является описания интерфейса модели, а точнее ее "видимой" части и получение значения модели используя метод контроллера TryUpdateModel UpdateModel :
public interface IEditMovie
{
string Title { get; set; }
int Year { get; set; }
}
Ограничение заполнения модели, используя интерфейсы модели:
[HttpPost]
public ActionResult Edit()
{
var movie = new Movie();
this.TryUpdateModel<IEditMovie>(movie);
return this.View();
}
В данном случае необходимо описывать дополнительные интерфейсы для модели и реализовывать их, что не всегда является положительным. Так же можно использовать ViewModel для представления, которая сможет ограничить поля модели. В этом случае в качестве параметра в действие контроллера будет передаваться тип MovieViewModel, который будет содержать только необходимые поля (Title, Director). И еще одним из вариантов может быть использования атрибута ReadOnly для поля в модели.
Реализация собственной привязки модели
Разобравшись на высоком уровне привязки данных к модели, необходимо посмотреть "под капот" данному процессу и разобраться как мы можем создавать собственные привязчики данных (model binder).
На низком уровне связывания модели и данных, сам процесс связки происходит в активаторе ControllerActionInvoker, где и происходит связка модели и данных. Для осуществления связки необходим привязчик модели, который реализует интерфейс IModelBinder. Каждый тип может иметь свой привязчик модели, активатор будет искать для каждого типа параметра действия свой привязчик и выполнять метод BindModel из интерфейса привязчика. Если же привязчика для модели не найдено, то используется привязчик который идет "из коробки" - DefaultModelBinder. Привязчиков модели предоставляют провайдеры механизма связывания, которые реализуют интерфейс IModelBinderProvider.
Рассмотрим один из возможных вариантов применения своей (custom) привязки модели. Очень часто существует такая задача, когда в действие контролера в качестве параметра передают Id модели, которую необходимо отредактировать / отобразить / удалить.
Получение модели без использования custom model binding:
[HttpGet]
public ActionResult Edit(int movieId)
{
var movie = this.movieManger.GetMovie(movieId);
return this.View(movie);
}
В данном примере мы получаем идентификатор модели фильма, который необходимо получить из базы данных и отобразить пользователю для дальнейшего редактирования. С использованием своей привязки модели мы сможем реализовать следующий вариант взаимодействия:
[HttpGet]
public ActionResult Edit(Movie movie)
{
return this.View(movie);
}
Для реализации пользовательского механизма связывания данных модели нам необходим пользовательский провайдер привязчика и пользовательский привязчик модели. Пользовательский провайдер:
public class MovieModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
if (typeof(Movie) != modelType)
return null;
return new MovieModelBinder();
}
}
Провайдер проверяет совпадение типов и возвращает собственную реализацию механизма связывания. Пользовательский привязчик получив параметр идентификационного номера сущности должен получить сущность фильма и отдать ее в качестве параметра действия контроллеру. Для получения условного хранилища данных (аля база данных) необходимо добавить менеджер, который и будет возвращать фильм по номеру.
public class MovieManager : IMovieManger
{
private readonly IList<Movie> movieTable;
public MovieManager()
{
this.movieTable = new List<Movie>;
{
new Movie{Id = 1, Title = "Форсаж 7", Director = "Джеймс Ван", Year = 2015, Rating = 7.414},
new Movie{Id = 2, Title = "Мстители: Эра Альтрона", Director = "Джосс Уидон", Year = 2015, Rating = 8.254},
new Movie{Id = 3, Title = "Звёздные войны: Пробуждение силы", Director = "Джей Джей Абрамс", Year = 2015, Rating = 0},
new Movie{Id = 4, Title = "Kingsman: Секретная служба", Director = "Мэттью Вон", Year = 2014, Rating = 8.0}
};
}
public Movie GetMovie(int id)
{
return this.movieTable.FirstOrDefault(model =>; model.Id == id);
}
}
Интерфейс менеджера содержит только метод получения фильма. И статическую коллекцию фильмов в виде источника данных.
public class MovieModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value == null)
return null;
if (string.IsNullOrEmpty(value.AttemptedValue))
return null;
int movieId;
if (!int.TryParse(value.AttemptedValue, out movieId))
return null;
var movieManager = new MovieManager();
var movie = movieManager.GetMovie(movieId);
return movie;
}
}
Привязчик модели получает значение запроса, если значение отсутствует, то происходит возврат пустой модели. Получив значение запроса, значение конвертируется в переменную идентификатора по которому получаем модель из менеджера. Для полной функциональности данного примера необходимо зарегистрировать провайдер привязчика при старте приложения в Global.asax.cs:
public class MvcApplication : HttpApplication
{
protected void Application_Start()
{
ModelBinderProviders.BinderProviders.Add(new MovieModelBinderProvider());
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}
Выполнив запрос по адресу, получаем модель из хранилища:
Пользовательский механизм привязки модели готов. В данном примере упрощены некоторые механизмы, которые можно было задействовать : IoC, работа с базой данных. Данные механизмы можно реализовать самостоятельно и тогда функциональность привязчика модели будет полноценной.
Привязка коллекции данных
Используя DefaultModelBinder можно создать привязку к коллекции данных:
public ActionResult UpdateInts(ICollection<int> numbers)
{
return this.View(numbers);
}
Форма, которая предоставляет коллекцию чисел:
<form method="post" action="/Numbers/Edit">
<input type="text" name="number" value="1" />
<input type="text" name="number" value="4" />
<input type="text" name="number" value="2" />
<input type="text" name="number" value="8" />
<input type="submit" />
</form>
Для привязки коллекции данных, используя пользовательские типы существует следующий подход:
<form method="post" action="/Movies/Update">
<input type="text" name="[0].Title" value="Форсаж 7" />
<input type="text" name="[0].Director" value="Джеймс Ван" />
<input type="text" name="[0].Year" value="2015" />
<input type="text" name="[1].Title" value="Мстители: Эра Альтрона" />
<input type="text" name="[1].Director" value="Джосс Уидон" />
<input type="text" name="[1].Year" value="2015" />
<input type="text" name="[2].Title" value="Звёздные войны: Пробуждение силы" />
<input type="text" name="[2].Director" value="Джей Джей Абрамс" />
<input type="text" name="[2].Year" value="2015" />
<input type="submit" />
</form>
Контроллер, использующий привязку коллекции данных:
public ActionResult Update(ICollection<Movie> movies) {
return this.View(movies);
}
Одна важная особенность - индексы моделей в HTML разметке должны идти непрерывной последовательностью с инкрементом 1. Исходный код