Главная страница   /   22.3. Использование модели связывания данных по умолчанию (ASP.NET MVC 4

ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

22.3. Использование модели связывания данных по умолчанию

Пользовательские механизмы связывания в приложении определяются редко, чаще всего используется встроенный класс механизма связывания, DefaultModelBinder. Он используется механизмом вызова действий, когда отсутствует пользовательский механизм связывания для данного типа. По умолчанию этот механизм связывания проверяет четыре места, которые показаны в таблице 22-1, чтобы найти данные, соответствующие имени параметра, для которого выполняется связывание.

Таблица 22-1: Порядок поиска данных для параметра классом DefaultModelBinder
Место Описание
Request.Form Значения, предоставленные пользователем в элементе HTML form.
RouteData.Values Значения, полученные через маршруты приложения.
Request.QueryString Данные строки запроса из URL.
Request.Files Файлы, загруженные как часть запроса (пример загрузки файлов показан в главе 11).

Места проверяются в указанном порядке. Так, в нашем примере DefaultModelBinder будет искать значение параметра id следующим образом:

  1. Request.Form["id"]
  2. RouteData.Values["id"]
  3. Request.QueryString["id"]
  4. Request.Files["id"]

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

Подсказка

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

Связывание простых типов

Для простых типов параметров DefaultModelBinder пытается преобразовать полученное из данных запроса строковое значение в тип параметра с помощью класса System.ComponentModel.TypeDescriptor. Если значение не может быть преобразовано, например, если мы предоставили значение apple для параметра, который требует значение int, то DefaultModelBinder не сможет связать данные.

Чтобы увидеть возникающую из-за этого проблему, запустите приложение и перейдите по ссылке /Home/Index/apple. На рисунке 22-2 показан ответ от сервера.

Рисунок 22-2: Ошибка при обработке свойства модели

Механизм связывания по умолчанию упорно пытается привести предоставленное нами значение apple к требуемому типу int, что вызывает ошибку, показанную на рисунке.

Упростить работу механизма связывания можно с помощью типов, допускающих значение null, которые обеспечивают запасную позицию. Вместо того чтобы требовать числовое значение, параметр int? позволяет механизму связывания установить аргументу метода действия значение null. В листинге 22-7 показано, как мы применили тип, допускающий значение null, к действию Index.

Листинг 22-7: Используем тип, допускающий значение null, для параметра метода действия
public ActionResult Index(int? id) 
{
	Person dataItem = personData.Where(p => p.PersonId == id).First();
	return View(dataItem);
}

Если вы запустите приложение и перейдите по ссылке /Home/Index/apple, то увидите, что мы решили только часть проблемы, как показано на рисунке 22-3.

Рисунок 22-3: Запрос для значения null

Мы изменили характер проблемы – механизм связывания может использовать null в качестве значения аргумента id метода Index, но код метода действия не обрабатывает значение null. Это можно было бы исправить, явно включив в метод код для обработки значений null, но мы установили для параметра значение по умолчанию, которое будет использоваться вместо null. В листинге 22-8 показано, как мы применили значение параметра по умолчанию в методе действия Index.

Листинг 22-8: Применяем значение параметра по умолчанию в методе действия Index
public ActionResult Index(int id = 1) 
{
	Person dataItem = personData.Where(p => p.PersonId == id).First();
	return View(dataItem);
}

Всякий раз, когда механизм связывания не сможет найти значение для параметра id, будет использоваться значение по умолчанию 1, что равнозначно выбору объекта Person со значением свойства PersonId 1, как показано на рисунке 22-4.

Рисунок 22-4: Результат использования значения параметра по умолчанию в методе действий

Подсказка

Имейте в виду, что мы решили проблему нечисловых значений для механизм связывания, но мы все еще можем получить значения int, для которых в контроллере Home нет действительных объектов Person. Например, механизм связывания может запросто преобразовать последний сегмент URL-адресов /Home/Index/-1 и /Home/Index/500 в значения int. Это предоставит методу действия действительное значение для вызова метода Index, но приведет к ошибке, потому что наш код не выполняет никаких дополнительных проверок. Рекомендуем вам обратить внимание на диапазон значений параметров, с которым может работать ваш метод действия, и создать соответствующие тесты.

Анализ с учетом настроек культуры

Класс DefaultModelBinder использует различные настройки культуры для преобразования типов из разных областей данных запроса. Значения, получаемые из URL (данные маршрутизации и строки запроса), преобразуются без учета настроек культуры (culture-insensitive parsing), но эти настройки принимаются во внимание для значений из данных форм.

Чаще всего это вызывает проблемы со значениями DateTime. Даты, которые обрабатываются без учета настроек культуры, должны быть в универсальном формате yyyy-mm-dd. Значения дат из форм должны быть в формате, указанном сервером. Это означает, что на сервере с настройками культуры UK формат даты будет dd-mm-yyyy, в то время как на сервере с настройками культуры US формат будет mm-dd-yyyy, хотя yyyy-mm-dd будет приемлемым в обоих случаях.

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

Связывание сложных типов

Когда параметр метода действия является сложным типом (т.е. типом, который не может быть преобразован с помощью класса TypeConverter), то DefaultModelBinder класс использует рефлексию (reflection), чтобы получить список общих свойств, а затем связывает их по очереди. Для демонстрации, как это работает, мы добавили два новых метода действий в контроллер Home, как показано в листинге 22-9.

Листинг 22-9: Добавляем новые методы действий в контроллер Home
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MvcModels.Models;

namespace MvcModels.Controllers
{
	public class HomeController : Controller
	{
		private Person[] personData =
		{
			new Person
			{
				PersonId = 1,
				FirstName = "Adam",
				LastName = "Freeman",
				Role = Role.Admin
			},
			new Person
			{
				PersonId = 2,
				FirstName = "Steven",
				LastName = "Sanderson",
				Role = Role.Admin
			},
			new Person
			{
				PersonId = 3,
				FirstName = "Jacqui",
				LastName = "Griffyth",
				Role = Role.User
			},
			new Person
			{
				PersonId = 4,
				FirstName = "John",
				LastName = "Smith",
				Role = Role.User
			},
			new Person
			{
				PersonId = 5,
				FirstName = "Anne",
				LastName = "Jones",
				Role = Role.Guest
			}
		};

		public ActionResult Index(int id = 1)
		{
			Person dataItem = personData.Where(p => p.PersonId == id).First();
			return View(dataItem);
		}

		public ActionResult CreatePerson()
		{
			return View(new Person());
		}

		[HttpPost]
		public ActionResult CreatePerson(Person model)
		{
			return View("Index", model);
		}
	}
}

Перегруженная версия метода CreatePerson без параметров создает новый объект Person и передает его в метод представления, в результате чего визуализируется представление Views/Home/CreatePerson.cshtml, которое показано в листинге 22-10.

Листинг 22-10: Представление CreatePerson.cshtml
@model MvcModels.Models.Person
@{
	ViewBag.Title = "CreatePerson";
}
<h2>Create Person</h2>
@using (Html.BeginForm())
{
	<div>@Html.LabelFor(m => m.PersonId)@Html.EditorFor(m => m.PersonId)</div>
	<div>@Html.LabelFor(m => m.FirstName)@Html.EditorFor(m => m.FirstName)</div>
	<div>@Html.LabelFor(m => m.LastName)@Html.EditorFor(m => m.LastName)</div>
	<div>@Html.LabelFor(m => m.Role)@Html.EditorFor(m => m.Role)</div>
	<button type="submit">Submit</button>
}

Это представление визуализирует простой набор элементов label и editor для свойств объекта Person, с которыми мы работаем, а также элемент form, который отправляет данные элементов editor к методу действия CreatePerson с атрибутом HttpPost. Он использует представление Views/Home/Index.cshtml для отображения данных, переданных в форме. Чтобы увидеть, как работают новые методы действий, запустите приложение и перейдите по ссылке /Home/CreatePerson, как показано на рисунке 22-5.

Рисунок 22-5: Используем методы действий CreatePerson

Отправляя форму к методу CreatePerson, мы создаем различные ситуации для связывания данных. Механизм связывания по умолчанию обнаруживает, что наш метод действия требует объект Person и обрабатывает все его свойства по очереди. Для каждого свойства простого типа механизм связывания пытается найти значение запроса, как и в предыдущем примере. Так, например, обнаружив свойство PersonID, он будет искать значение данных PersonID, которые найдет в данных формы в запросе.

Если для свойства требуется сложный тип, то для нового типа процесс повторяется - будут получены общедоступные свойства и механизм связывания попытается найти значения для каждого из них. Разница в том, что имена свойств являются вложенными. Например, свойство HomeAddress класса Person принадлежит типу Address, который показан в листинге 22-11.

Листинг 22-11: Вложенный класс модели
public class Address
{
	public string Line1 { get; set; }
	public string Line2 { get; set; }
	public string City { get; set; }
	public string PostalCode { get; set; }
	public string Country { get; set; }
}

Чтобы найти значение для свойства Line1, механизм связывания будет искать значение для HomeAddress.Line1, то есть, он объединит имя свойства, указанное в объекте модели, с именем свойства, указанном в типе свойства.

Создаем легко связываемый HTML

Использование строк с префиксами означает, что мы должны создавать представления, которые будут их визуализировать - хотя вспомогательные методы значительно упрощают эту задачу. В листинге 22-12 показано, как мы обновили файл представления CreatePerson.cshtml, чтобы использовать несколько свойств из типа Address.

Листинг 22-12: Обновляем представление CreatePerson.cshtml, чтобы использовать свойства из типа Address
@model MvcModels.Models.Person
@{
	ViewBag.Title = "CreatePerson";
}
<h2>Create Person</h2>
@using (Html.BeginForm())
{
	<div>@Html.LabelFor(m => m.PersonId)@Html.EditorFor(m => m.PersonId)</div>
	<div>@Html.LabelFor(m => m.FirstName)@Html.EditorFor(m => m.FirstName)</div>
	<div>@Html.LabelFor(m => m.LastName)@Html.EditorFor(m => m.LastName)</div>
	<div>@Html.LabelFor(m => m.Role)@Html.EditorFor(m => m.Role)</div>
	<div>
		@Html.LabelFor(m => m.HomeAddress.City)
		@Html.EditorFor(m => m.HomeAddress.City)
	</div>
	<div>
		@Html.LabelFor(m => m.HomeAddress.Country)
		@Html.EditorFor(m => m.HomeAddress.Country)
	</div>
	<button type="submit">Submit</button>
}

Мы использовали строго типизированный вспомогательный метод EditorFor и указали свойства, которые мы хотим отредактировать, из свойства HomeAddress. Вспомогательный метод автоматически задает атрибуты name элементов input в соответствии с форматом, который используется механизмом связывания по умолчанию, например:

<input class="text-box single-line" id="HomeAddress_Country" name="HomeAddress.Country"
	type="text" value="" />

Благодаря этой функции нам не нужно самим проверять, сможет ли механизм связывания создать объект Address для свойства HomeAddress. Чтобы увидеть, как это работает, мы отредактируем представление /Views/Home/Index.cshtml, в котором будем отображать свойства HomeAddress, полученные из формы, как показано в листинге 22-13.

Листинг 22-13: Отображаем свойства HomeAddress.City и HomeAddress.Country
@model MvcModels.Models.Person
@{
ViewBag.Title = "Index";
}
<h2>Person</h2>
<div><label>ID:</label>@Html.DisplayFor(m => m.PersonId)</div>
<div><label>First Name:</label>@Html.DisplayFor(m => m.FirstName)</div>
<div><label>Last Name:</label>@Html.DisplayFor(m => m.LastName)</div>
<div><label>Role:</label>@Html.DisplayFor(m => m.Role)</div>
<div><label>City:</label>@Html.DisplayFor(m => m.HomeAddress.City)</div>
<div><label>Country:</label>@Html.DisplayFor(m => m.HomeAddress.Country)</div>

Запустите приложение и перейдите по ссылке /Home/CreatePerson, введите значения для свойств City и Country и убедитесь, что при отправке формы они связываются с объектом модели, как показано на рисунке 22-6.

Рисунок 22-6: Связывание свойств в сложных объектах

Определяем пользовательские префиксы

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

Чтобы продемонстрировать эту ситуацию, мы создали новый класс под названием AddressSummary.cs в папке Models. Содержимое этого файла показано в листинге 22-14.

Листинг 22-14: Класс AddressSummary.cs
namespace MvcModels.Models
{
	public class AddressSummary
	{
		public string City { get; set; }
		public string Country { get; set; }
	}
}

Мы добавили новый метод действия в контроллер Home, который использует класс AddressSummary; он показан в листинге 22-15.

Листинг 22-15: Добавляем новый метод действия в контроллер Home
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MvcModels.Models;

namespace MvcModels.Controllers
{
	public class HomeController : Controller
	{
		// ...other statements omitted from listing for brevity...
		public ActionResult DisplaySummary(AddressSummary summary)
		{
			return View(summary);
		}
	}
}

Наш новый метод называется DisplaySummary. Он принимает параметр AddressSummary, который передает в метод View. Представление для этого метода называется DisplaySummary.cshtml. Мы создали его в папке /Views/Home. Содержимое этого представления показано в листинге 22-16.

Листинг 22-16: Представление DisplaySummary.cshtml
@model MvcModels.Models.AddressSummary
@{
	ViewBag.Title = "DisplaySummary";
}
<h2>Address Summary</h2>
<div><label>City:</label>@Html.DisplayFor(m => m.City)</div>
<div><label>Country:</label>@Html.DisplayFor(m => m.Country)</div>

Это очень простое представление, которое просто отображает значения двух свойств, определенных в классе AddressSummary. Чтобы продемонстрировать проблему с префиксами при связывании с другими типами моделей, мы изменим вызов к вспомогательному методу файле /Views/Home/CreatePerson.cshtml, чтобы форма была отправлена к новому методу действия DisplaySummary, как показано в листинге 22-17.

Листинг 22-17: Изменяем метод для отправки формы в представлении CreatePerson.cshtml
@model MvcModels.Models.Person
@{
	ViewBag.Title = "CreatePerson";
}
<h2>Create Person</h2>
@using (Html.BeginForm("DisplaySummary", "Home"))
{
	<div>@Html.LabelFor(m => m.PersonId)@Html.EditorFor(m => m.PersonId)</div>
	<div>@Html.LabelFor(m => m.FirstName)@Html.EditorFor(m => m.FirstName)</div>
	<div>@Html.LabelFor(m => m.LastName)@Html.EditorFor(m => m.LastName)</div>
	<div>@Html.LabelFor(m => m.Role)@Html.EditorFor(m => m.Role)</div>
	<div>
		@Html.LabelFor(m => m.HomeAddress.City)
		@Html.EditorFor(m => m.HomeAddress.City)
	</div>
	<div>
		@Html.LabelFor(m => m.HomeAddress.Country)
		@Html.EditorFor(m => m.HomeAddress.Country)
	</div>
	<button type="submit">Submit</button>
}

Чтобы увидеть проблему, запустите приложение и перейдите по ссылке /Home/CreatePerson. При отправке формы значения, введенные для свойств City и Country, не отображаются в HTML представления DisplaySummary.

Проблема в том, что атрибуты name в форме имеют префикс HomeAddress, а это не то, что ищет механизм связывания, когда пытается связать тип AddressSummary. Чтобы исправить это, можно применить к параметру метода действия атрибут Bind, который сообщит механизму связывания, какой префикс он должен искать, как показано в листинге 22-18.

Листинг 22-18: Применяем атрибут Bind к параметру метода действия
public ActionResult DisplaySummary([Bind(Prefix = "HomeAddress")] AddressSummary summary)
{
	return View(summary);
}

Синтаксис запутанный, но результат его оправдывает. Заполняя свойства объекта AddressSummary, механизм связывания будет искать в запросе данные значений HomeAddress.City и HomeAddress.Country. В этом примере мы отображаем элементы editor для свойств объекта Person, но после отправки формы создали экземпляр класса AddressSummary с помощью механизма связывания, как показано на рисунке 22-7. Этот способ может показаться слишком объемным для решения такой простой проблемы, но связывание с другим объектом используется очень часто, и, скорее всего, вы будете использовать эту технику в реальных проектах.

Рисунок 22-7: Связывание свойств с другим типом объекта

Выборочное связывание свойств

Представьте, что в свойство Country класса AddressSummary является особенно чувствительным, и мы не хотим, чтобы пользователи могли указывать для него значения. Чтобы не отображать данное свойство для пользователей или даже запретить включать его в HTML для отправки в браузер, можно использовать атрибуты, что мы рассмотрели в главе 20, или просто не добавлять для этого свойства элементы editor в представлении.

Тем не менее, злоумышленник может просто отредактировать данные формы, которые отправляются на сервер при отправке формы, и выбрать значение для свойства Country, которое ему подходит. На самом деле, мы хотим сообщить механизму связывания, чтобы он не связывал значение для свойства Country из запроса, для чего нужно применить к параметру метода действия атрибут Bind. В листинге 22-19 показано, как с помощью атрибута мы запретили пользователю предоставлять значение для свойства Country в методе действия DisplaySummary контроллера Home.

Листинг 22-19: Запрещаем связывание данных для свойства
public ActionResult DisplaySummary(
	[Bind(Prefix = "HomeAddress", Exclude = "Country")]AddressSummary summary)
{
	return View(summary);
}

Свойство Exclude атрибута Bind позволяет исключить свойства из процесса связывания данных. Чтобы увидеть эффект, перейдите по ссылке /Home/CreatePerson, введите данные и отправьте форму. Вы увидите, что для свойства Country ничего не отображается. (В качестве альтернативы, вы можете использовать свойство Include, чтобы указать только те свойства, которые должны быть связаны для этой модели; все остальные свойства будут игнорироваться).

Когда к параметру метода действия применяется атрибут Bind, он влияет только на те экземпляры класса, которые связываются для данного метода действия; все остальные методы действий будут пытаться выполнить связывание для всех свойств, определенных типом параметра. Если вы хотите получить более широкий результат, примените атрибут Bind к самому классу модели, как показано в листинге 22-20. Здесь мы применили метод Bind к классу AddressSummary, чтобы включить в процесс связывания только свойство City.

Листинг 22-20: Применяем атрибут Bind к классу модели
using System.Web.Mvc;

namespace MvcModels.Models
{
	[Bind(Include = "City")]
	public class AddressSummary
	{
		public string City { get; set; }
		public string Country { get; set; }
	}
}

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

Связывание с массивами и коллекциями

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

Связывание с массивами

В механизме связывания по умолчанию очень хорошо реализована поддержка параметров методов действий, которые представляют собой массивы. Чтобы ее продемонстрировать, мы добавили в контроллер Home новый метод под названием Names, который показан в листинге 22-21.

Листинг 22-21: Добавляем метод Names в контроллер Home
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MvcModels.Models;

namespace MvcModels.Controllers
{
	public class HomeController : Controller
	{
		// ...other statements omitted from listing for brevity
		public ActionResult Names(string[] names)
		{
			names = names ?? new string[0];
			return View(names);
		}
	}
}

Метод действия Names принимает в качестве параметра массив строк под названием names. Механизм связывания будет искать любые данные с именем names и создаст массив, который будет содержать эти значения.

Подсказка

Обратите внимание, что в этом примере мы проверяем, не содержит ли параметр null в данном методе действия. В качестве значение по умолчанию для параметров можно использовать только константы или литералы.

В листинге 22-22 показано представление /Views/Home/Names.cshtml, с помощью которого продемонстрируем связывание массивов.

Листинг 22-22: Содержание файла Names.cshtml
@model string[]
@{
	ViewBag.Title = "Names";
}
<h2>Names</h2>
@if (Model.Length == 0)
{
	using (Html.BeginForm())
	{
		for (int i = 0; i < 3; i++)
		{
			<div><label>@(i + 1):</label>@Html.TextBox("names")</div>
		}
	<button type="submit">Submit</button>
	}
}
else
{
	foreach (string str in Model)
	{
		<p>@str</p>
	}
	@Html.ActionLink("Back", "Names");
}

Это представление отображает различный контент в зависимости от количества элементов в модели представления. Если элементы отсутствуют, то мы выводим форму с тремя одинаковыми элементами ввода:

<form action="/Home/Names" method="post">
	<div><label>1:</label><input id="names" name="names" type="text" value="" /></div>
	<div><label>2:</label><input id="names" name="names" type="text" value="" /></div>
	<div><label>3:</label><input id="names" name="names" type="text" value="" /></div>
	<button type="submit">Submit</button>
</form>

Когда мы отправим форму, механизм связывания по умолчанию увидит, что для нашего метода действия требуется массив строк, и будет искать данные с тем же именем, что и параметр. В нашем примере это означает, что содержание всех наших элементов input будет собрано вместе. На рисунке 22-8 показано, как работают метод действия и представление.

Рисунок 22-8: Связывание данных для массивов

Связывание с коллекциями

Мы можем выполнять связывание не только с массивами, но также и с классами коллекций .NET. В листинге 22-23 показано, как мы изменили тип параметра метода действия Names на строго типизированный список.

Листинг 22-23: Используем строго типизированную коллекцию в качестве параметра метода действия
public ActionResult Names(IList<string> names)
{
	names = names ?? new List<string>();
	return View(names);
}

Обратите внимание, что мы использовали интерфейс IList. Нам не нужно даже указывать конкретную реализацию класса (хотя при желании это можно сделать). В листинге 22-24 показано, как мы изменили файл Names.cshtml, чтобы использовать новый тип модели.

Листинг 22-24: Используем коллекцию в качестве типа модели в представлении Names.cshtml
@model IList<string>
@{
	ViewBag.Title = "Names";
}
<h2>Names</h2>
@if (Model.Count == 0)
{
	using (Html.BeginForm())
	{
		for (int i = 0; i < 3; i++)
		{
			<div><label>@(i + 1):</label>@Html.TextBox("names")</div>
		}
		<button type="submit">Submit</button>
	}
}
else
{
	foreach (string str in Model)
	{
		<p>@str</p>
	}
	@Html.ActionLink("Back", "Names");
}

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

Связывание с коллекциями пользовательских типов моделей

Мы также можем связать отдельные свойства данных с массивом пользовательских типов, таким как наш класс модели AddressSummary. В листинге 22-25 показано, что мы добавили новый метод действия под названием Address, который принимает в качестве параметров строго типизированную коллекцию, которая полагается на пользовательский класс модели.

Листинг 22-25: Определяем метод действия с коллекцией пользовательских типов модели
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MvcModels.Models;

namespace MvcModels.Controllers
{
	public class HomeController : Controller
	{
		// ...other statements omitted from listing for brevity...
		public ActionResult Address(IList<AddressSummary> addresses)
		{
			addresses = addresses ?? new List<AddressSummary>();
			return View(addresses);
		}
	}
}

Для этого метода действия мы создали представление /Views/Home/Address.cshtml, которое показано в листинге 22-26.

Листинг 22-26: Содержание файла Address.cshtml
@using MvcModels.Models
@model IList<AddressSummary>
@{
	ViewBag.Title = "Address";
}
<h2>Addresses</h2>
@if (Model.Count() == 0)
{
	using (Html.BeginForm())
	{
		for (int i = 0; i < 3; i++)
		{
			<fieldset>
				<legend>Address @(i + 1)</legend>
				<div><label>City:</label>@Html.Editor("[" + i + "].City")</div>
				<div><label>Country:</label>@Html.Editor("[" + i + "].Country")</div>
			</fieldset>
		}
		<button type="submit">Submit</button>
	}
}
else
{
	foreach (AddressSummary str in Model)
	{
		<p>@str.City, @str.Country</p>
	}
	@Html.ActionLink("Back", "Address");
}

Если в коллекции модели нет элементов, это представление визуализирует элемент form. Форма состоит из пар элементов input, атрибуты name которых начинаются с индекса массива:

<fieldset>
	<legend>Address 1</legend>
	<div>
		<label>City:</label>
		<input class="text-box single-line" name="[0].City" type="text" value="" />
	</div>
	<div>
		<label>Country:</label>
		<input class="text-box single-line" name="[0].Country" type="text" value="" />
	</div>
</fieldset>
<fieldset>
	<legend>Address 2</legend>
	<div>
		<label>City:</label>
		<input class="text-box single-line" name="[1].City" type="text" value="" />
	</div>
	<div>
		<label>Country:</label>
		<input class="text-box single-line" name="[1].Country" type="text" value="" />
	</div>
</fieldset>

После того, как форма отправлена, механизм связывания по умолчанию обнаружит, что ему нужно создать коллекцию объектов AddressSummary, и будет использовать префиксы индексов массива из атрибутов name, чтобы получить значения для свойств объекта. Свойства с префиксом [0] используются для первого объекта AddressSumary, с префиксом [1] - для второго объекта, и так далее.

Наше представление Address.cshtml определяет элементы input для трех таких объектов с индексами и отображает их, когда в коллекции модели есть элементы. Прежде чем мы сможем это продемонстрировать, мы должны удалить атрибут Bind из класса модели AddressSummary, как показано в листинге 22-27, в противном случае механизм связывания проигнорирует свойство Country.

Листинг 22-27: Удаляем атрибут Bind из класса модели AddressSummary
using System.Web.Mvc;

namespace MvcModels.Models
{
	// This attribute has been commented out
	//[Bind(Include="City")]
	public class AddressSummary
	{
		public string City { get; set; }
		public string Country { get; set; }
	}
}

Чтобы увидеть, как процесс связывания данных работает для пользовательских коллекций объектов, запустите приложение и перейдите по ссылке /Home/Address. Введите несколько городов и стран, а затем нажмите кнопку Submit для отправки формы на сервер. Механизм связывания найдет и обработает значения данных с индексами, и будет использовать их для создания коллекции объектов AddressSummary, которая затем будет передана обратно в представление и выведена на экран, как показано на рисунке 22-9.

Рисунок 22-9: Связывание коллекций пользовательских объектов