Andrey on .NET | Часть 23 – Связывание получаемых данных и Модели

Часть 23 – Связывание получаемых данных и Модели

Рассмотрим еще один вопрос, касающийся Модели. А именно, каким образом в её экземпляр попадают данные из запроса, полученного от формы на странице сайта. Кроме того, разберем решение одной интересной задачи, периодически встречающейся в реальных проектах.

1. Связывание через параметры запроса

Это самый простой вариант, но при этом необходимо быть с ним очень аккуратным. Принцип его прост: необходимо создать Действие с именами и типами параметров, копирующими все или часть свойств Модели. При этом в метод View() необходимо передать пустую ссылку вместо экземпляра Модели.

Для примера добавим в Контроллер Payments Действие AddFromParams:

// GET: /Payments/AddFromParams
public ActionResult AddFromParams(
    string userLogin,
    double amount,
    string info)
{
    return this.View("Add");
} 

В этом методе нет явного создания экземпляра Модели. Кроме того, даже не все её свойства перечислены в качестве параметров. Давайте запустим веб-приложение и введем запрос вида:

http://localhost:[порт]/Payments/AddFromParams?UserLogin=axel&Amount=500&Info=some%20text

В открывшемся Представлении Payments/Add в соответствующих полях можно увидеть значения из указанного выше запроса. Откуда они там появились? Дело в том, что при получении пустой ссылки вместо Модели, метод View() самостоятельно создаст её экземпляр. После чего заполнит её данными, если у у Действия присутствуют параметры, аналогичных именам Модели.

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

Посмотрим что необходимо сделать, чтобы улучшить данный вариант. Создадим Действие, которое будет добавлять запись в базу данных. Для начала создадим сам экземпляр Модели. Затем проверим его на наличие ошибок и выберем нужное действие в ответ:

// GET: /Payments/AddFromParams
public ActionResult AddFromParams(
    string userLogin,
    double amount,
    string info)
{
    var newPayment = new PaymentModel() {
        UserLogin = userLogin,
        Amount = amount,
        Info = info
    };

    this.TryValidateModel(newPayment);

    if (!this.ModelState.IsValid) {
        return this.View("Add", newPayment);
    }

    var paymentsRepository = new PaymentRepository();
    paymentsRepository.Add(newPayment);

    this.TempData["Message"] = Resources.Controllers.PaymentsRes.PaymentAddedMessage;

    return this.RedirectToAction("Index");
} 

Внимательно посмотрим на приведенный код. Легко заметить, что это аналог уже метода Add(), который использует другой вид связывания.

2. Связывание через объект

Более удобным способом является связывание параметров запроса и Модели через объект. По сути, он является развитием предыдущего варианта и не раз уже использовался в демонстрационном веб-приложении. Например, в Действии Add Контроллера Payments.

// POST: /Payments/Add
[HttpPost]
public ActionResult Add(PaymentModel newPayment)
{
    if (!this.ModelState.IsValid) {
        return this.View(newPayment);
    }

    var paymentsRepository = new PaymentRepository();
    paymentsRepository.Add(newPayment);

    this.TempData["Message"] = Resources.Controllers.PaymentsRes.PaymentAddedMessage;

    return this.RedirectToAction("Index");
}

Данный код аналогичен приведенному ранее варианту, но короче его за счет использования возможностей ядра ASP.NET MVC. Оно самостоятельно передаст параметры из запроса в экземпляр PaymentModel и вызовет метод для проверки значений Модели.

Коме того, данный способ позволяет связывать данные с коллекциями объектов. Например вот так может выглядеть Действие, которое позволяет принять и сохранить список платежей:

// POST: /Payments/AddMultiple
[HttpPost]
public ActionResult AddMultiple(PaymentModel[] newPayments)
{
    if (!this.ModelState.IsValid) {
        return this.View(newPayments);
    }

    var paymentsRepository = new PaymentRepository();

    foreach (var payment in newPayments) {
        paymentsRepository.Add(payment);
    }
    
    this.TempData["Message"] = Resources.Controllers.PaymentsRes.PaymentAddedMessage;

    return this.RedirectToAction("Index");
}

При этом соответствующее Представление должно передать список платежей, даже если он будет состоять только из одного элемента.

3. Управление связыванием с объектом

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

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

Чтобы избежать этого, ядро ASP.NET MVC предоставляет атрибут [Bind]. Он позволяет определять параметры, которые будет участвовать в процессе связывания. Остальные попросту игнорируются. При этом атрибут поддерживает следующие параметры:

  • Include – "белый список" свойств, участвующих в связывании;
  • Exclude – "черный список" свойств, исключенных из связывания.

Если необходимо указать несколько свойств, то в качестве разделителя используется запятая. Сам атрибут указывается перед параметром Действия, который является экземпляром Модели.

Рассмотрим несколько примеров. Разрешим передачу в метод Add() только имени входа пользователя:

// GET: /Payments/AddFromParams
public ActionResult AddFromParams(
    [Bind(Include="UserLogin")] PaymentModel newPayment)
{
    .........
} 

Теперь все параметры, кроме указанного UserLogin, будут игнорироваться. В этом легко можно убедиться, если посмотреть на результат выполнения вот такого запроса:

http://localhost:[порт]/Payments/AddFromParams?UserLogin=axel&Amount=500&Info=some%20text

Аналогично можно указать и "черный список":

// GET: /Payments/AddFromParams
public ActionResult AddFromParams(
    [Bind(Exclude="Date, Source")] PaymentModel newPayment)
{
    .........
} 

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

Метод AddFromParams() был создан только для примера и не потребуется в дальнейшем. Можно удалить его из исходного кода демонстрационного веб-приложения.

Полный контроль связывания передаваемых значений и Модели

IModelBinder

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

/somepage.html?user=[name]&money=[amount]&additionalinfo=[payment info]

Как хорошо видно, ни один из параметров запроса не совпадает с именами свойств Модели. С одной стороны, можно реализовать Действие, которое будет заниматься сортировкой. Но это уже перенос бизнес-логики в код Контроллера. Как быть в этой ситуации?

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

namespace System.Web.Mvc
{
    public interface IModelBinder
    {
        object BindModel(
            ControllerContext controllerContext, 
            ModelBindingContext bindingContext);
    }
}

Метод BindModel() получает в качестве параметров два значения:

  • controllerContext – текущий контекст Контроллера;
  • bindingContext – контекст связывания, который позволяет получить информацию о самой Модели.

В качестве возвращаемого значения возвращается созданный экземпляр Модели. Таким образом, возможности данного варианта гораздо шире, чем у Действия для сбора параметров запроса.

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

Рассмотрим способы указания ASP.NET MVC использовать заданную реализацию IModelBinder:

Способ 1: Установка атрибута [ModelBinder] перед параметром метода

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

// POST: /Payments/GlobalBankOne
public ActionResult GlobalBankOne(
    [ModelBinder(typeof(GlobalBankOneBinder))] PaymentModel newPayment)
{
    ..........
}

Способ 2: Добавление типа в общий список

Такой способ задает реализацию связывания конкретного типа Модели во всех Действиях веб-приложения. Для этого в методе Application_Start() необходимо добавить такую строчку:

ModelBinders.Binders[typeof(PaymentModel)] = new GlobalBankOneBinder();

Способ 3: Указание атрибута [ModelBinder] перед объявлением класса

Данный вариант похож по сути на предыдущий. Но теперь указание добавляется в файл с кодом Модели. Таким образом, при её повтором использовании, например в другом проекте, он не будет утерян:

[ModelBinder(typeof(GlobalBankOneBinder))] 
public class PaymentModel
{
    .........
}

Пример реализации

Будем считать, что Модель PaymentModel позволяет указать источник платежа. Поэтому добавим элемент в перечень. В остальном оставим её без изменений.

namespace MVCDemo.Models
{
    using System;

    public class PaymentModel
    {
        public enum PaymentSource
        {
            Unknown = 0,
            Direct = 1,
            GlobalBankOne = 2
        }

        .........
    }
}

Для эмуляции запросов в корне сайта создадим HTML файл GlobalBankOne.html с формой для ввода данных. Условимся, что обрабатывать запросы будет Действие GlobalBankOne Контроллера Payments.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>IModelBinder Demo</title>
</head>
<body>
    <form action="/Payments/GlobalBankOne" method="get">
    User:
    <input id="user" name="user" type="text" /><br />
    Amount (RUR):
    <input id="money" name="money" type="text" /><br />
    Payment info
    <input id="additionalinfo" name="additionalinfo" type="text" /><br />
    <input id="Submit" type="submit" value="submit" />
    </form>
</body>
</html>

Теперь в папке Models создадим вложенную папку Binders. В ней разместим класс GlobalBankOneBinder:

namespace MVCDemo.Models.Binders
{
    using System;
    using System.Web.Mvc;
    using MVCDemo.Resources.Models;
    using MVCDemo.Resources.Shared;

    public class GlobalBankOneBinder : IModelBinder
    {
        #region IModelBinder Members

        public object BindModel(
            ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var payment = new PaymentModel();

            payment.UserLogin = controllerContext.HttpContext.Request["user"];
            var userRepository = new UserRepository();
            if (!userRepository.IsLoginExists(payment.UserLogin)) {
                bindingContext.ModelState.AddModelError("UserLogin", ErrorsRes.InvalidUserLogin);
            }            

            payment.Date = DateTime.Now;

            try {
                payment.Amount
                    = Convert.ToDouble(controllerContext.HttpContext.Request["money"]);
            }
            catch {
                bindingContext.ModelState.AddModelError(
                    "Amount", PaymentRes.IncorrectAmountValue);
            }

            payment.Source = PaymentModel.PaymentSource.GlobalBankOne;

            payment.Info = controllerContext.HttpContext.Request["additionalinfo"];

            return payment;
        }

        #endregion
    }
}

Обратите внимание на следующие моменты:

  • Пример достаточно простой и в нем не задействуются метаданные.
  • Метод BindModel() не обязательно должен создавать экземпляры только одного конкретного класса. Он получает информацию о типе, к которому надо привести исходный запрос, через параметр bindingContext. Это позволяет создать одну реализацию, которая может создавать экземпляры разных классов Модели в зависимости от текущей задачи и бизнес-логики приложения.
  • В приведенном коде реализована своя проверка данных. Для уведомления об ошибках используется объект ModelState.
  • Контекст Контроллера позволяет получить любую информацию о запросе. Именно с его помощью в данном случае осуществляется чтение переданных данных.
  • Не обязательно все данные брать только из запроса. В данном случае дата и источник платежа заполняются исходя из потребностей и задач бизнес-логики.

Теперь создадим Действие в Контроллере. Оно не будет отличаться от метода Add() по перечню параметров. Однако, обратите внимание на указание атрибута [ModelBinder]. Он и определяет используемую для связывания реализацию IModelBinder:

namespace MVCDemo.Controllers
{
    using System;
    using System.Web.Mvc;
    using MVCDemo.Models;
    using MVCDemo.Models.Binders;

    public class PaymentsController : Controller
    {
        .........

        // GET: /Payments/GlobalBankOne
        public ActionResult GlobalBankOne(
            [ModelBinder(typeof(GlobalBankOneBinder))] PaymentModel newPayment)
        {
            if (!this.ModelState.IsValid) {
                return this.View("Add", newPayment);
            }

            var paymentsRepository = new PaymentRepository();
            paymentsRepository.Add(newPayment);

            this.TempData["Message"] = Resources.Controllers.PaymentsRes.PaymentAddedMessage;

            return this.RedirectToAction("Index");
        }
    }
}

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

В очередной раз можно запустить веб-приложение и убедиться в работоспособности реализованного способа связи с Моделью.


Исходный код проекта (C#, Visual Studio 2010): MVCDemo-Part23.zip (517 Kb)

Комментарии (15) -

Виктор 01.04.2011 20:17:10

Круто, даже прочитав Сандерсона, стока не узнал(но узнал много другого Smile) Спасибо.

Не подскажете
Вот я я допустим создал 3 проекта - WebApplication, сборку с интерфесами (в ней IRepository, IUnitOfWork, IEntity, ITask, IAccount и так далее), и сборку с реализацией этих интерфейсов на базе Linq2SQL. И допустим есть в WebApplication контроллер TaskController и метод у него Create. И нужно в этом контроллере нужно создать новый объект Task. Делать new Task() нельзя, так как WebApplication не ссылается на сборку с реализацией и соответсвенно про класс Task не в курсе. Поэтому же нельзя делать Task параметром контролера. Если делать параметром ITask - то как ASP.NET MVC Framework создаст объект, реализующий этот интерфейс? на базе какого класса? Правильно ли в этом лсучае поручать репозиторию создавать новый Task, а потом наполнять его пришедшими значениями? Или есть более правильный способ?

Во первых, сценарий сильно походит на описание Dependency Injection. Smile

Во вторых, почему Контроллер обязательно должен создавать экземпляр конкретного класса Модели через new? Он может обратиться к Фабрике или Фабричному методу и получить объект с заданным интерейсом (см. Creational patterns).

Такой вопрос, навеянный темой этой главы. Есть ли в mvc3 нечто подобное struts в jsp – то есть на страницу из контороллера забрасывается объект-entity, а уже на странице некий движок разбрасывает значения из полей объекта по текстовым полям на странице. И наоборот при обновлении данных в бд движок собирает значения из полей на странице и переносит их в поля объекта. Очень хочется иметь такой готовый механизм. Джависты уже лет 10 имеют подобные вещи.

А данный движок сам лезет в БД? В этом случае это будет противоречить идеологии MVC (не библиотеки ASP.NET MVC, а шаблона) и не может быть сделано в её рамках.

Если же просто про работу с объектами, то View так и работают - из полей формы собирается объект, который передается в Контроллер и затем в Модель. Равно как и наоборот - из Модели берется объект и выводится в форму.

Каким образом связываются названия полей формы и названия полей модели? А если мне на форме надо заполнить 2 объекта модели которые потом сохраняются в 2х записях таблицы бд? У меня есть подобного рода проекты в которых не катит подход в частности используемый в динамик дата.

Ну собственно способы связи и были в статье выше. И опять же можно написать свой ModelBinder, если стандартный не подходит. (я просто не видел тот механизм в Java поэтому не могу дать аналогию или сказать что её нет тут)

Встречал проекты в которых к связке MVC добавляется ViewModel, т.е. ��омимо моделей данных используют модели представлений, что избавляет от постоянного контроля Include и Exclude для свойств модели данных.
Andrey что Вы думайте по этому поводу, упростит ли использование ViewModels сопровождение больших проектов.

Будут ли еще главы учебника?
И если да то можете ли, кратко, описать их содержание.

Еще раз спасибо.

@ ch1seL: мне кажется что добавление ViewModel уже превращает MVC в MVVC или в их смесь. Кроме того, если необходим более жесткий контроль за значениями, то тут и проверки данных и свой ModelBinders и т. д. Т.е. Include и Exclude хорошо подходят когда надо получить или заблокировать несколько свойств модели в конкретном Действии.

Учебник это громко сказано. Но дальше будет, содержания нет, т.к. все в работе. Сроки зависят от занятости по рабочим проектам, т.е. пишу в свободное время. Могу только сказать, что в начале будет обновление написанного по Модели с поправкой на вышедший обновление MVC 3. В частности отказ от эмулятора БД и замена его на настоящую.

Спасибо! Отличный материал Smile

Спасибо, полезный обзор, главное, все просто и доступно написано!

Пожалуйста.

Андрей! Что-то всё затихло с продолжением этих статей. Какие перспективы? Уж очень интересная тематика рассмотрена. Думаю что многие со мной согласятся.

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

Pingbacks and trackbacks (1)+

Добавить комментарий