ASP.NET MVC привязка модели (Model binding)

ASP.NET MVC привязка модели (Model binding)

В основе коммуникации веб приложений лежит 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.

  1. Полученный запрос, механизм маршрутизации направляет в определенный контроллер и действие.
  2. Проводится анализ сигнатуры действия контроллера и сопоставляются параметры. Те параметры, что будут совпадать по имени, будут сопоставлены в обход привязки модели. Из оставшихся данных будет составлена модель, которую ожидает действие контролера.
  3. Создается объект необходимой модели. Модель обязательно должна иметь конструктор по-умолчанию без параметров и без опциональных параметров.
  4. Заполняются все поля модели, которые имеют первый уровень вложенности.
  5. Для оставшихся полей производится создание моделей второго уровня и заполнение, начиная с третьего пункта алгоритма.

Выборочная привязка модели и ограничение привязки модели

В некоторых случаях нам нет необходимости заполнять полностью все поля модели. Для исключения их из списка заполняемых, можно использовать атрибут 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);
    }
}

Выполнив запрос по адресу, получаем модель из хранилища:

GET запрос

Пользовательский механизм привязки модели готов. В данном примере упрощены некоторые механизмы, которые можно было задействовать : 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. Исходный код

d2funlife | Даниил Павлов 2015-2020
Powered by ASP.NET Core 2.2, Entity Framework Core 2.2. Web + UI