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