Привязка модели

183

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

Пример проекта

Для целей этой и последующих статей в Visual Studio создан новый проект MVC по имени MvcModels с использованием шаблона Empty (Пустой) и отметкой флажка MVC в разделе Add folders and core references for (Добавить папки и основные ссылки для).

Мы будем работать с тем же самым классом модели, что и в предшествующих статьях, поэтому в папке Models создан новый файл класса по имени User.cs с содержимым, приведенным в примере ниже:

using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace MvcModels.Models
{
    public partial class User
    {
        public int UserId { get; set; }

        [DisplayName("Имя")]
        public string FirstName { get; set; }

        [DisplayName("Фамилия")]
        public string LastName { get; set; }

        [DisplayName("Дата рождения")]
        [DataType(DataType.Date)]
        public DateTime BirthDate { get; set; }

        [DisplayName("Адрес")]
        public Address HomeAddress { get; set; }

        [DisplayName("Подтвердил регистрацию?")]
        public bool IsApproved { get; set; }

        [DisplayName("Роль")]
        public Role Role { get; set; }
    }

    public class Address
    {
        [DisplayName("Адрес 1")]
        public string Line1 { get; set; }

        [DisplayName("Адрес 2")]
        public string Line2 { get; set; }

        [DisplayName("Город")]
        public string City { get; set; }

        [DisplayName("Почтовый индекс")]
        public string PostalCode { get; set; }

        [DisplayName("Страна")]
        public string Country { get; set; }
    }

    public enum Role
    {
        Admin,
        User,
        Guest
    }
}

Также определен контроллер Home, код которого показан в примере ниже. В этом контроллере определена коллекция объектов User и действие Index, которое позволяет выбирать одиночный объект User по значению свойства UserId:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Web.Mvc;
using MvcModels.Models;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        private List<User> UserCollection = new List<User> {
            new User {UserId = 1, FirstName = "Иван", 
                LastName = "Иванов", Role = Role.Admin},
            new User {UserId = 2, FirstName = "Петр", 
                LastName = "Петров", Role = Role.User},
            new User {UserId = 3, FirstName = "Сидор", 
                LastName = "Сидоров", Role = Role.User},
            new User {UserId = 4, FirstName = "Вася", 
                LastName = "Васильев", Role = Role.Guest}
        };

        public ActionResult Index(int id)
        {
            User user = UserCollection.Single(u => u.UserId == id);
            return View(user);
        }
    }
}

Для поддержки метода действия Index() создан файл представления /Views/Index.cshtml, содержимое которого приведено в примере ниже. В нем с помощью шаблонизированного вспомогательного метода отображаются значения некоторых свойств модели представления User:

@model MvcModels.Models.User
@{
    ViewBag.Title = "Index";
}

<h2>Данные пользователя</h2>
<div><label>ID:</label>@Html.DisplayFor(m => m.UserId)</div>
<div><label>Имя:</label>@Html.DisplayFor(m => m.FirstName)</div>
<div><label>Фамилия:</label>@Html.DisplayFor(m => m.LastName)</div>
<div><label>Роль:</label>@Html.DisplayFor(m => m.Role)</div>

Наконец, создается папка Views/Shared, в которую добавляется компоновка по имени _Layout.cshtml:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title</title>
    <style>
        label {
            display: inline-block;
            width: 100px;
            font-weight: bold;
            margin: 5px;
        }

        form label {
            float: left;
        }

        input.text-box {
            float: left;
            margin: 5px;
        }

        button[type=submit] {
            margin-top: 5px;
            float: left;
            clear: left;
        }

        form div {
            clear: both;
        }
    </style>
</head>
<body>
    <div>
        @RenderBody()
    </div>
</body>
</html>

Понятие привязки моделей

Привязка моделей представляет собой элегантный мост между HTTP-запросом и методами C#, определяющими действия. Большинство приложений ASP.NET MVC Framework в той или иной степени полагаются на привязку моделей, в том числе простой пример приложения, созданный в предыдущем разделе. Чтобы увидеть привязку моделей в работе, запустите приложение и перейдите на /Home/Index/1. Результат показан на рисунке ниже:

Простая демонстрация привязки моделей

Указанный URL содержит значение свойства UserId объекта User, который необходимо отобразить, например:

/Home/Index/1

Инфраструктура ASP.NET MVC Framework транслирует эту часть URL и применяет ее в качестве аргумента при вызове метода Index() класса контроллера Home с целью обслуживания запроса:

public ActionResult Index(int id)

Процесс, с помощью которого сегмент URL был преобразован в аргумент int метода, является примером привязки моделей. В последующих разделах будет описан процесс, инициируемый этой простой демонстрацией, и затем объяснены более сложные функциональные возможности привязки моделей.

Процесс, приводящий к привязке моделей, начинается сразу после получения запроса и его обработки механизмом маршрутизации. В этом примере приложения конфигурация маршрутизации не изменялась, поэтому для обработки запроса использовался стандартный маршрут, который среда Visual Studio добавляет в файл /App_Start/RouteConfig.cs. В качестве напоминания, этот стандартный маршрут приведен ниже:

using System.Web.Mvc;
using System.Web.Routing;

namespace MvcModels
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}

Вопросы определения и работы маршрутов подробно рассматривались ранее, поэтому здесь они повторяться не будут. Для процесса привязки моделей важной частью является необязательная переменная сегмента id. При переходе на URL вида /Home/Index/1 последний сегмент, который указывает интересующий объект User, присваивается переменной маршрутизации id.

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

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

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

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

В примере выше, приведенном в этом разделе, активатор действий будет проверять метод Index() и обнаружит, что он принимает один параметр int. Затем активатор действий найдет связыватель, отвечающий за значения int, и вызовет его метод BindModel().

Связыватель модели отвечает за предоставление значения int, которое может использоваться для вызова метода Index(). Это обычно означает трансформацию некоторого элемента данных запроса (такого как значения формы или строки запроса), но инфраструктура ASP.NET MVC Framework никак не ограничивает способ получения данных.

Позже будут предоставлены примеры специальных связывателей. Кроме того, будут продемонстрированы определенные возможности класса ModelBindingContext, экземпляр которого передается методу IModelBinder.BindModel().

Использование стандартного связывателя модели

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

Request.Form

Значения, предоставленные пользователем в HTML-элементах <form>

RouteData.Values

Значения, полученные с использованием маршрутов приложения

Request.QueryString

Данные, включенные в строку запроса URL

Request.Files

Файлы, загруженные как часть запроса

Указанные в таблице местоположения просматриваются по порядку. В рассматриваемом простом примере DefaultModelBinder ищет значение для параметра id в следующих местоположениях:

  1. Request.Form["id"]

  2. RouteData.Values["id"]

  3. Request.QueryString["id"]

  4. Request.Files["id"]

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

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

Привязка простых типов

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

Чтобы ознакомиться с проблемой, которая при этом возникает, необходимо запустить пример приложения и перейти на URL вида /Home/Index/строка. Ответ, полученный от сервера, показан на рисунке ниже:

Ошибка при обработке свойства модели

Стандартный связыватель модели довольно упорен - он выясняет, что требуется значение int, и пытается преобразовать предоставленное значение, т.е. строку в тип int, в результате чего возникает ошибка, приведенная на рисунке.

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

public class HomeController : Controller
{
     // ...

     public ActionResult Index(int? id)
     {
          // ...
     }
}

Запустив приложение и перейдя на /Home/Index/строка, легко заметить, что решена только часть проблемы:

Запрос значения null

Связыватель модели способен применять null в качестве значения для аргумента id метода Index, но код внутри этого метода действия не проверяет значения на предмет равенства null. Это можно было бы исправить путем явной проверки на предмет null, но допускается также установить для параметра стандартное значение, которое будет использоваться вместо null. В примере ниже показано, как применить стандартное значение параметра к методу действия Index():

// ...
public ActionResult Index(int id = 1)
{
    User user = UserCollection.Single(u => u.UserId == id);
    return View(user);
}
// ...

Всякий раз, когда связывателю модели не удается найти значение для параметра id, используется стандартное значение 1, которое приводит к выбору объекта User со значением 1 в свойстве UserId:

Результат применения стандартного значения параметра в методе действия

Привязка сложных типов

Если параметр метода действия относится к сложному типу (т.е. к любому типу, который не может быть преобразован с помощью класса TypeConverter), то класс DefaultModelBinder использует рефлексию для получения набора открытых свойств с последующей привязкой каждого из них по очереди. Чтобы продемонстрировать это в работе, в контроллер Home добавлены два новых метода действий, как показано в примере ниже:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Web.Mvc;
using MvcModels.Models;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        // ...

        public ActionResult CreateUser()
        {
            return View(new User());
        }

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

Перегруженная версия CreateUser(), не принимающая параметров, создает новый объект User и передает его методу View(), который визуализирует представление /Views/Home/CreateUser.cshtml, приведенное в примере ниже:

@model MvcModels.Models.User
@{
    ViewBag.Title = "CreateUser";
}

<h2>Добавить пользователя</h2>
@using (Html.BeginForm())
{
    <div>@Html.LabelFor(m => m.UserId)@Html.EditorFor(m => m.UserId)</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">Отправить</button>
}

Представление визуализирует простой набор меток и редакторов для свойств объекта User и содержит HTML-элемент <form>, отправляющий данные из редакторов методу действия CreateUser() (версии, декорированной атрибутом HttpPost). Этот метод использует представление /Views/Home/Index.cshtml для отображения данных, имеющихся в форме. Увидеть работу новых методов действий можно, запустив приложение и перейдя на /Home/CreateUser:

Использование методов действий CreateUser()

При отправке данных формы методу CreateUser() создается другая ситуация привязки моделей. Стандартный связыватель модели обнаруживает, что метод действия требует объект User, и обрабатывает все его свойства по очереди. Для каждого свойства простого типа связыватель пытается найти значение в запросе, как это делалось в предыдущем примере. Таким образом, встретив свойство UserId, связыватель будет искать значение данных UserId, которое находится внутри данных формы в рамках запроса.

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

Например, свойство HomeAddress класса User имеет тип Address, который показан в примере ниже:

// ...
public class Address
{
    [DisplayName("Адрес 1")]
    public string Line1 { get; set; }

    [DisplayName("Адрес 2")]
    public string Line2 { get; set; }

    [DisplayName("Город")]
    public string City { get; set; }

    [DisplayName("Почтовый индекс")]
    public string PostalCode { get; set; }

    [DisplayName("Страна")]
    public string Country { get; set; }
}
// ...

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

Создание легко привязываемой HTML-разметки

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

@model MvcModels.Models.User
@{
    ViewBag.Title = "CreateUser";
}

<h2>Добавить пользователя</h2>
@using (Html.BeginForm())
{
    <div>@Html.LabelFor(m => m.UserId)@Html.EditorFor(m => m.UserId)</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">Отправить</button>
}

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

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

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

@model MvcModels.Models.User
@{
    ViewBag.Title = "Index";
}

<h2>Данные пользователя</h2>
<div><label>ID:</label>@Html.DisplayFor(m => m.UserId)</div>
<div><label>Имя:</label>@Html.DisplayFor(m => m.FirstName)</div>
<div><label>Фамилия:</label>@Html.DisplayFor(m => m.LastName)</div>
<div><label>Роль:</label>@Html.DisplayFor(m => m.Role)</div>
<div><label>Город:</label>@Html.DisplayFor(m => m.HomeAddress.City)</div>
<div><label>Страна:</label>@Html.DisplayFor(m => m.HomeAddress.Country)</div>

Запустив приложение и перейдя на URL вида /Home/CreateUser, можно ввести значения для свойств City и Country, после чего отправить форму и убедиться, что значения были привязаны к объекту модели:

Привязка свойств в сложных объектах

Указание специальных префиксов

Встречаются ситуации, когда сгенерированная HTML-разметка относится к одному типу объекта, но ее необходимо привязать к другому типу. Это означает, что префиксы, содержащие представление, не будут соответствовать структуре, которую ожидает связыватель модели, и данные не смогут быть правильно обработаны. Чтобы продемонстрировать такую ситуацию, в папке Models создан новый файл класса по имени AddressSummary.cs. Содержимое этого файла приведено в примере ниже:

namespace MvcModels.Models
{
    public class AdressSummary
    {
        public string City { get; set; }
        public string Country { get; set; }
    }
}

Кроме того, в контроллер Home был добавлен новый метод действия, который использует класс AddressSummary, как показано в примере ниже:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Web.Mvc;
using MvcModels.Models;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        // ...

        public ActionResult DisplaySummary(AdressSummary summary)
        {
            return View(summary);
        }
    }
}

Новый метод действия называется DisplaySummary(). Он принимает параметр AddressSummary, который передается методу View(), так что он может быть отображен в стандартном представлении. Затем в папке /Views/Home создается файл представления DisplaySummary.cshtml с содержимым, приведенным в примере ниже:

@model MvcModels.Models.AdressSummary

@{
    ViewBag.Title = "DisplaySummary";
}

<h2>Адрес проживания</h2>
<div><label>Город:</label>@Html.DisplayFor(m => m.City)</div>
<div><label>Страна:</label>@Html.DisplayFor(m => m.Country)</div>

Это представление отображает значения двух свойств, определенных в классе AddressSummary. Чтобы проиллюстрировать проблему с префиксами во время привязки моделей разных типов, изменим вызов вспомогательного метода BeginForm() в файле /Views/Home/CreateUser.cshtml так, чтобы обратная отправка формы осуществлялась новому методу действия DisplaySummary():

@model MvcModels.Models.User
@{
    ViewBag.Title = "CreateUser";
}

<h2>Добавить пользователя</h2>
@using (Html.BeginForm("DisplaySummary", "Home"))
{
    ...
}

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

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

// ...
public ActionResult DisplaySummary(
    [Bind(Prefix="HomeAddress")]AdressSummary summary)
{
    return View(summary);
}
// ...

Несмотря на несколько неуклюжий синтаксис, результат оказывается вполне приемлемым. При заполнении свойств объекта AddressSummary связыватель модели будет искать в запросе значения данных для HomeAddress.City и HomeAddress.Country. В рассматриваемом примере мы отображаем редакторы для свойств объекта User, но используем связыватель модели для создания экземпляра класса AddressSummary после отправки данных формы:

Привязка для свойств объекта другого типа

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

Избирательная привязка свойств

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

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

// ...
public ActionResult DisplaySummary(
    [Bind(Prefix="HomeAddress", Exclude="Country")]AdressSummary summary)
{
    return View(summary);
}
// ...

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

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

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

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

Привязка массивов и коллекций

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

Привязка массивов

Одной из элегантных возможностей стандартного связывателя модели является его поддержка параметров методов действий, которые представляют собой массивы. В целях демонстрации в контроллер Home добавлен новый метод по имени Names(), код которого показан в примере ниже:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Web.Mvc;
using MvcModels.Models;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        // ...

        public ActionResult Names(string[] names)
        {
            names = names ?? new string[0];
            return View(names);
        }
    }
}

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

В примере ниже приведено содержимое файла представления /Views/Home/Names.cshtml, который был создан для демонстрации привязки массива:

@model string[]
@{
    ViewBag.Title = "Names";
}

<h2>Имена</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">Отправить</button>
    }
} else {
    foreach (string name in Model) {
        <p>@name</p>
    }
    @Html.ActionLink("Назад", "Names");
}

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

...
<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">Отправить</button>
</form>
...

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

Привязка моделей для массивов

Привязка коллекций

Привязку можно осуществлять не только для массивов, но также и для классов коллекций .NET. В примере ниже показано, как изменить тип параметра метода действия Names(), чтобы он стал строго типизированным списком:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Web.Mvc;
using MvcModels.Models;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        // ...

        public ActionResult Names(List<string> names)
        {
            names = names ?? new List<string>();
            return View(names);
        }
    }
}

В примере ниже приведено модифицированное содержимое файла представления Names.cshtml, в котором применяется новый тип модели:

@model List<string>
@{
    ViewBag.Title = "Names";
}

<h2>Имена</h2>
@if (Model.Count == 0) {
...

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

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

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

using System;
using System.Linq;
using System.Collections.Generic;
using System.Web.Mvc;
using MvcModels.Models;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        // ...

        public ActionResult Address(List<AdressSummary> addresses)
        {
            addresses = addresses ?? new List<AdressSummary>();
            return View(addresses);
        }
    }
}

Для этого метода действия создан файл представления /Views/Home/Address.cshtml, содержимое которого показано в примере ниже:

@using MvcModels.Models
@model List<AdressSummary>
@{
    ViewBag.Title = "Address";
}

<h2>Адреса</h2>
@if (Model.Count() == 0)
{
    using (Html.BeginForm())
    {
        for (int i = 0; i < 3; i++)
        {
            <fieldset>
                <legend>Адрес @(i + 1)</legend>
                <div><label>Город:</label>
                    @Html.Editor("[" + i + "].City")
                </div>
                <div><label>Страна:</label>
                    @Html.Editor("[" + i + "].Country")
                </div>
            </fieldset>
        }
        <button type="submit">Отправить</button>
    }
}
else
{
    foreach (AdressSummary address in Model)
    {
        <p>@address.City, @address.Country</p>
    }
    @Html.ActionLink("Назад", "Address");
}

Это представление визуализирует элемент <form>, если элементы в коллекции модели отсутствуют. Элемент <form> состоит из пары элементов <input>, атрибуты name которых имеют префиксы в виде индекса массива в результирующей HTML-разметке:

...
<fieldset>
    <legend>Адрес 1</legend>
    <div>
        <label>Город:</label>
        <input class="text-box single-line" name="[0].City" type="text" value="" />
    </div>
    <div>
        <label>Страна:</label>
        <input class="text-box single-line" name="[0].Country" type="text" value="" />
    </div>
</fieldset>
<fieldset>
    <legend>Адрес 2</legend>
    <div>
        <label>Город:</label>
        <input class="text-box single-line" name="[1].City" type="text" value="" />
    </div>
    <div>
        <label>Страна:</label>
        <input class="text-box single-line" name="[1].Country" type="text" value="" />
    </div>
</fieldset>
...

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

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

namespace MvcModels.Models
{
    // [System.Web.Mvc.Bind(Include="City")]
    public class AdressSummary
    {
        public string City { get; set; }
        public string Country { get; set; }
    }
}

Чтобы просмотреть результаты работы процесса привязки для коллекций специальных объектов, понадобится запустить приложение и перейти на URL вида /Home/Address. Введите значения в полях для городов и стран, после чего щелкните на кнопке "Отправить", чтобы отправить форму серверу. Связыватель модели найдет и обработает индексируемые значения данных и применит их для создания коллекции объектов AddressSummary, а затем передаст их обратно представлению для отображения:

Привязка коллекций специальных объектов
Пройди тесты
Лучший чат для C# программистов