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

159

B этой статье мы будем рассматривать привязку моделей, которая представляет собой одно из главных добавлений к инфраструктуре Web Forms в версии ASP.NET 4.5. Привязка моделей упрощает процесс создания экземпляров классов, используемых для представления бизнес-объектов в веб-приложениях, и является мощным инструментом для сокращения числа ошибок и снижения сложности классов отделенного кода. Она тесно связана с привязкой данных.

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

Для целей этой и последующих статей мы создали новый проект под названием ControlState, используя шаблон ASP.NET Empty Web Application (Пустое веб-приложение ASP.NET) в Visual Studio. Мы начали с создания папки по имени Models - это обычное место для классов, которые представляют объекты модели данных. Затем мы добавили в эту папку файл класса User.cs и определили в нем класс модели, как показано в примере ниже:

namespace Binding.Models
{
    public class User
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string Cell { get; set; }
        public string Zip { get; set; }
    }
}

Класс User - типичный простой класс модели. Одно из распространенных действий в приложениях ASP.NET Framework предусматривает создание экземпляров классов моделей из данных формы, чтобы можно было выполнять операции с ними и тем самым изменять состояние приложения. В приложении GameStore были определены классы моделей Game, Order и Cart, и мы создавали веб-формы, в которых данные форм применялись для создания и заполнения экземпляров упомянутых классов.

Объекты модели часто представляют строки в базе данных, как было в случае класса Game внутри приложения GamesStore, но они также могут использоваться для отслеживания работ в приложении, что делалось с помощью классов Cart и Order, когда пользователь совершал покупки в приложении GameStore и оформлял заказ.

Здесь мы собираемся применять класс модели User для более простых задач - мы будем собирать ввод из формы для заполнения объекта User и последующего отображения введенных значений свойств. Это позволит описать средство привязки моделей. С этой целью мы добавили в проект веб-форму по имени Default.aspx с контентом, приведенным в примере ниже:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="Binding.Default" %>

<!DOCTYPE html>
<html>
<head runat="server">
    <title>Привязка моделей</title>
    <style>
        label {display: inline-block;width: 100px;text-align: right; margin: 5px;}
        div.panel {float: left;margin-left: 10px;}
        div.panel label { text-align: right;}
        div.error, span.error { color: red;}
        button { margin: 10px 100px;}
    </style>
</head>
<body>
    <div class="panel">
        <form id="form1" runat="server">
            <div>
                <div>
                    <label>Имя:</label>
                    <input id="name" runat="server" />
                </div>
                <div>
                    <label>Возраст:</label>
                    <input id="age" runat="server" />
                </div>
                <div>
                    <label>Номер:</label>
                    <input id="cell" runat="server" />
                </div>
                <div>
                    <label>Индекс:</label>
                    <input id="zip" runat="server" />
                </div>
                <button type="submit">Отправить</button>
            </div>
        </form>
    </div>
    <div class="panel">
        <div><label>Ваше имя:</label><span id="sname" runat="server"></span></div>
        <div><label>Ваш возраст:</label><span id="sage" runat="server"></span></div>
        <div><label>Ваш номер:</label><span id="scell" runat="server"></span></div>
        <div><label>Ваш индекс:</label><span id="szip" runat="server"></span></div>
    </div>
</body>
</html>

Эта веб-форма содержит набор элементов <input> серверной стороны, которые используются для ввода пользователем значений для свойств User, и набор соответствующих им элементов <span> серверной стороны, в которых будут отображаться результаты.

Элементы серверной стороны применяются в файле Default.aspx по двум причинам, не относящимся к привязке моделей. Значения, введенные в элементах <input> серверной стороны, будут предохраняться элементами управления HTML, что упрощает внесение изменений в какое-то одно значение, избегая необходимости ввода полного набора значений каждый раз. Вдобавок используются элементы <span> серверной стороны, поэтому можно устанавливать их контент с помощью свойства InnerText в файле отделенного кода C#.

В примере ниже 34.3 показано содержимое файла отделенного кода Default.aspx.cs, в котором определены методы, предназначенные для заполнения объекта User данными из элементов <input> и его отображения в элементах <span> внутри файла .aspx.

using System;
using Binding.Models;

namespace Binding
{
    public partial class Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (this.IsPostBack)
                DisplayUser(GetUser());
        }

        protected User GetUser()
        {
            User model = new User();
            model.Name = Request.Form["Name"];
            model.Age = int.Parse(Request.Form["age"]);
            model.Cell = Request.Form["Cell"];
            model.Zip = Request.Form["Zip"];
            return model;
        }

        protected void DisplayUser(User user)
        {
            sname.InnerText = user.Name;
            sage.InnerText = user.Age.ToString();
            scell.InnerText = user.Cell;
            szip.InnerText = user.Zip;
        }
    }
}

Метод GetUser() создает и устанавливает свойства объекта User с использованием данных, предоставленных внутри формы, которые получены через коллекцию HttpRequest.Form. Метод DisplayUser() принимает аргумент User и применяет элементы <span> серверной стороны для отображения значений свойств. Событие Load обрабатывается за счет вызова обоих методов, чтобы отобразить значения формы в элементах <span>, когда запрос является обратной отправкой.

Протестировать веб-форму можно, запустив приложение - веб-форма Default.aspx запрашивается как стандартный документ. Введите данные в полях формы и щелкните на кнопке "Отправить". Форма отправится серверу, а введенные данные отобразятся в элементах <span>:

Отображение значений данных формы

Суть проблемы

Понять привязку моделей лучше всего, ознакомившись с проблемой, которую она решает: сложность корректной обработки и проверки достоверности пользовательского ввода, которые гарантируют допустимость и осмысленность значений, используемых для заполнения объекта модели. В качестве примера возьмем свойства User.Name и User.Age и рассмотрим шаги, которые необходимо предпринять, чтобы удостовериться в том, что пользователь предоставил для них допустимые значения. Для свойства Name понадобится проверить следующее:

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

Для свойства Age необходимо выполнить следующие проверки Age:

Это не сложные ограничения, но мы должны проверить каждое из них и сообщить об ошибке, если полученное значение оказалось неподходящим. В примере ниже мы модифицировали метод GetUser(), определенный в файле отделенного кода Default.aspx.cs, чтобы проверять значение, полученное для полей name и age:

// ...
using System.Text.RegularExpressions;

namespace Binding
{
    public partial class Default : System.Web.UI.Page
    {
        // ...

        protected User GetUser()
        {
            User model = new User();

            string name = Request.Form["Name"];
            if (String.IsNullOrEmpty(name))
                throw new FormatException("Пожалуйста, введите имя");
            else if (name.Length < 3 || name.Length > 20)
                throw new FormatException("Имя должно содержать от 3 до 20 символов");
            else if (!Regex.IsMatch(name, @"^[A-Za-zА-Яа-я\s]+"))
                throw new FormatException("В имени допускаются только буквы и пробелы");
            else
                model.Name = name;

            string age = Request.Form["age"];
            if (String.IsNullOrEmpty(age))
                throw new FormatException("Пожалуйста, введите возраст");
            else
            {
                int ageValue;
                if (!int.TryParse(age, out ageValue))
                    throw new FormatException("Некорректное значение для возраста");
                else
                {
                    if (ageValue < 5 || ageValue > 100)
                        throw new FormatException("Возраст должен находится в пределах от 5 до 100");
                    else
                        model.Age = ageValue;
                }
            }

            model.Cell = Request.Form["Cell"];
            model.Zip = Request.Form["Zip"];
            return model;
        }

        // ...
    }
}

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

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

Чтобы протестировать код проверки достоверности, запустите приложение и щелкните на кнопке "Отправить", не вводя данные в поля. Если приложение было запущено из отладчика Visual Studio, отладчик остановится в точке генерации исключения FormatException внутри класса отделенного кода. Нажатие клавиши <F5> возобновит выполнение приложения и приведет к отображению стандартной страницы ошибки.

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

Привязка модели решает основную проблему - многословность и хрупкая природа кода, который обрабатывает и проверяет пользовательские данные. Первое действие предусматривает применение привязки модели для автоматизации процесса получения значений формы и последующее использование этих значений для заполнения свойств объекта User. Это делается в примере ниже, в котором показаны изменения, внесенные в файл Default.aspx.cs:

// ...
using System.Web.ModelBinding;

namespace Binding
{
    public partial class Default : System.Web.UI.Page
    {
        // ...

        protected User GetUser()
        {
            User model = new User();

            IValueProvider provider = new FormValueProvider(ModelBindingExecutionContext);
            if (this.TryUpdateModel<User>(model, provider))
                return model;
            else
                throw new FormatException("Не удалось привязать модель");
        }

        // ...
    }
}

Это пример базовой привязки модели, который включает два шага - создание поставщика значений и обновление объекта модели. На первом шаге при создании поставщика значений указывается, откуда будут поступать значения для свойств модели. Поставщики значений являются реализациями интерфейса IValueProvider; в текущем примере используется класс FormValueProvider, который получает значения из данных формы.

Аргумент конструктора для класса FormValueProvider - это экземпляр класса ModelBindingExecutionContext, который предоставляет доступ к контексту запроса; мы получаем экземпляр указанного класса через свойство Page.ModelBindingExceptionContext. Вся трудная работа выполняется строго типизированным методом TryUpdateModel<T>(), где T представляет собой тип модели, которая должна обновляться значениями из поставщика. В этом примере T является User, что приводит к применению следующего оператора в примере:

if (this.TryUpdateModel<User>(model, provider))

Метод TryUpdateModel<T>() просматривает каждое свойство, определенное в классе модели, и пытается получить соответствующие значения из реализации IValueProvider. Когда используется класс FormValueProvider, это означает, что свойство под названием Name или Age отображается на значение элемента формы по имени name или age (отображение не зависит от регистра символов). Процесс привязки модели применяет класс System.ComponentModel.TypeConverter для преобразования значения формы в корректный тип C# для свойства модели.

Метод TryUpdateModel<T>() возвращает значение bool, указывающее на успешность обновления объекта модели - в этом примере значение true означает, что данные формы были применены к объекту модели. Значение false указывает на возникновение, по крайней мере, одной проблемы. В примере выше значение false обрабатывается генерацией исключения FormatException, но позже мы продемонстрируем более подходящий способ реагирования на ошибки.

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

Чтобы увидеть процесс привязки модели в работе, запустите приложение и введите значения в поля формы внутри веб-формы Default.aspx. Для полей, которые обновляют свойства модели Name, Cell и Zip, можно задавать любые значения (или оставлять их пустыми). В поле, обновляющем свойство модели Age, должно быть введено значение, которое может быть преобразовано в int. В противном случае автоматическое преобразование типов, выполняемое во время привязки модели для установки значений свойств, приведет к тому, что метод TryUpdateModel<T>() возвратит false, если значение формы не может быть разобрано.

Применение атрибутов проверки достоверности модели

Использование метода TryUpdateModel<T>() позволяет привести в порядок код GetUser() в файле Default.aspx.cs и также устраняет все проверки, позволяющие удостовериться в том, что получены подходящие значения. Второй этап применения привязки модели заключается в восстановлении проверки достоверности, что делается за счет указания атрибутов из пространства имен System.ComponentModel.DataAnnotations для объекта модели. Использование атрибутов в классе User демонстрируется в примере ниже:

using System.ComponentModel.DataAnnotations;

namespace Binding.Models
{
    public class User
    {
        [Required(ErrorMessage = "Пожалуйста, введите имя")]
        [StringLength(20, MinimumLength = 3, ErrorMessage = "Имя должно содержать от 3 до 20 символов")]
        [RegularExpression(@"^[A-Za-zА-Яа-я\s]+", ErrorMessage = "В имени допускаются только буквы и пробелы")]
        public string Name { get; set; }

        [Required(ErrorMessage = "Пожалуйста, введите возраст")]
        [Range(5, 100, ErrorMessage = "Возраст должен находится в пределах от 5 до 100")]
        public int Age { get; set; }

        public string Cell { get; set; }
        public string Zip { get; set; }
    }
}

Атрибуты применяются к свойствам, определенным в объекте модели, чтобы обеспечить проверку достоверности для этих свойств. Среда ASP.NET включает множество разных атрибутов проверки достоверности, которые описаны в таблице ниже:

Атрибуты проверки достоверности модели
Имя Описание
Compare

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

Range

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

RegularExpression

Требует, чтобы значение соответствовало регулярному выражению, указанному в аргументе конструктора.

Required

Требует, чтобы пользователь предоставил значение. В этом атрибуте определено свойство AllowEmptyStrings, которое указывает, принимаются ли пустые строки, позволяя различать ситуацию с отсутствием значения и ситуацию со значением в виде пустой строки. Стандартным значением свойства AllowEmptyStrings является false

StringLength

Требует, чтобы количество символов значения входило в заданный диапазон. Максимально приемлемая длина указывается с использованием аргумента конструктора, а (необязательная) минимальная длина устанавливается с помощью свойства MinimumLength. Пример был показан выше.

CustomValidation

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

Благодаря этой таблице, можно понять, что в примере выше реализована политика проверки достоверности с использованием атрибутов Required, StringLength, RegularExpression и Range. Это намного более согласованный и управляемый подход, чем реализация проверки достоверности внутри веб-формы или класса элемента управления.

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

Атрибуты проверки достоверности сообщают об ошибке, когда значение не соответствует указанным условиям - мы объясним, как получать доступ к таким ошибкам чуть позже. Каждый атрибут имеет стандартное сообщение, однако оно довольно общее, поэтому мы рекомендуем переопределять его с помощью свойства ErrorMeesage, как было сделано в примере.

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

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

using System.ComponentModel.DataAnnotations;

namespace Binding
{
    public class CustomChecks
    {
        public static ValidationResult CheckZip(string zipCode)
        {
            return zipCode != null && zipCode.ToLower().StartsWith("00") ?
                ValidationResult.Success : new ValidationResult("Индекс должен начинаться с 00");
        }
    }
}

Класс CustomChecks содержит метод CheckZip(), который вскоре будет применен к свойству Zip класса User. Специальные методы проверки достоверности должны быть статическими, принимать аргумент для проверки и возвращать объект ValidationResult.

Класс ValidationResult определен в пространстве имен System.ComponentModel.DataAnnotations. Для указания на успешную проверку используется статическое свойство ValidationResult.Success, а для ошибок понадобится создать новый экземпляр класса ValidationResult с передачей конструктору в качестве аргумента сообщения об ошибке.

Метод CheckZip() принимает аргумент типа string, однако можно указывать другой тип, и тогда будет предпринята попытка преобразования в него. Мы проверяем почтовый индекс, который естественным образом выражается как значение string. Реализованная проверка достоверности проста и предусматривает выяснение, предоставлено ли значение и начинается ли оно с двух нулей. Разумеется, это не точная проверка почтового индекса, тем не менее, она позволяет продемонстрировать расширяемую природу проверки достоверности.

В примере ниже показано, как специальная проверка достоверности применяется в классе модели User:

using System.ComponentModel.DataAnnotations;

namespace Binding.Models
{
    public class User
    {
        // ...

        [CustomValidation(typeof(Binding.CustomChecks), "CheckZip")]
        public string Zip { get; set; }
    }
}

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

Пройди тесты
Лучший чат для C# программистов