Атрибуты привязки модели

117

До сих пор все значения, используемые для привязки модели, поступали из данных HTML-формы посредством класса FormValueProvider:

...
IValueProvider provider = new FormValueProvider(ModelBindingExecutionContext);
TryUpdateModel<User>(model, provider);
...

Работа с данными формы важна, но это не единственный источник данных в приложении. Среда ASP.NET Framework включает другие реализации интерфейса IValueProvider, которые позволяют выполнять привязку моделей из набора разных источников. (Как упоминалось ранее, интерфейс IValueProvider обозначает источник привязки данных и реализован классом FormValueProvider, который применялся в предшествующих примерах.)

Реализации интерфейса IValueProvider описаны в таблице ниже; все они определены в пространстве имен System.Web.ModelBinding:

Классы поставщиков данных IValueProvider для привязки моделей
Класс Описание
ControlValueProvider

Получает значения из свойства в элементе управления

CookieValueProvider

Получает значения из cookie-наборов в запросе

FormValueProvider

Получает значения из данных формы в запросе

QueryStringValueProvider

Получает значения из строки запроса

ProfileValueProvider

Получает значения из данных профиля пользователя

RouteDataValueProvider

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

ViewStateValueProvider

Получает значения из состояния представления, ассоциированного с запросом

Можно применить несколько классов реализации IValueProvider для постепенного построения классов моделей из разных источников, но такой прием работает не очень хорошо в сочетании с атрибутами проверки достоверности вроде Required. Метод TryUpdateModel<T>() предполагает, что он будет вызван только один раз, и сообщит об ошибке проверки достоверности для свойств Required, несмотря на то, что этим свойствам впоследствии будут предоставлены значения с помощью других реализаций IValueProvider.

Основная проблема в том, что ручная привязка моделей, которая использовалась до сих пор в примерах, не является тем способом, на который рассчитаны реализации интерфейса IValueProvider от Microsoft. Вместо этого указанные реализации предназначены для предоставления значений методам, которые поставляют данные элементам управления, таким как Repeater. Чтобы продемонстрировать проблему, которую могут помочь решить множество поставщиков значений, мы создали в проекте папку Controls и добавили в нее файл класса по имени OperationSelector.cs с содержимым, показанным в примере ниже:

using System;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace Binding.Controls
{
    public class OperationSelector : WebControl
    {
        private string[] operators = { "Сложить", "Вычесть" };
        private string selectedOperator;

        public string SelectedOperator
        {
            get
            {
                return selectedOperator ?? operators[0];
            }
        }

        public OperationSelector()
        {
            Load += (src, args) =>
            {
                if (Page.IsPostBack)
                {
                    selectedOperator = Context.Request[GetFormId("op")];
                }
            };
        }

        protected override void RenderContents(HtmlTextWriter writer)
        {
            writer.AddAttribute(HtmlTextWriterAttribute.Name, GetFormId("op"));
            writer.RenderBeginTag(HtmlTextWriterTag.Select);

            foreach (string op in operators)
            {
                writer.AddAttribute(HtmlTextWriterAttribute.Value, op);
                if (op == SelectedOperator)
                {
                    writer.AddAttribute(HtmlTextWriterAttribute.Selected, "selected");
                }
                writer.RenderBeginTag(HtmlTextWriterTag.Option);
                writer.Write(op);
                writer.RenderEndTag();
            }
            writer.RenderEndTag();
        }

        private string GetFormId(string name)
        {
            return string.Format("{0}{1}{2}", ClientID, ClientIDSeparator, name);
        }
    }
}

В этом файле класса создается специальный серверный элемент управления, который генерирует элемент <select>, содержащий значения "Сложить" и "Вычесть". Этот элемент управления применяется внутри новой веб-формы по имени Data.aspx:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Data.aspx.cs" Inherits="Binding.Data" %>
<%@ Register TagPrefix="CC" Assembly="Binding" Namespace="Binding.Controls" %>

<!DOCTYPE html>
<html>
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <input id="max" value="5" runat="server" />
            <CC:OperationSelector id="opSelector" runat="server" />
            <button type="submit">Выполнить</button>
        </div>
        <div>
            <asp:Repeater SelectMethod="GetData" ItemType="System.String" 
                    runat="server" ViewStateMode="Disabled">
                <ItemTemplate>
                    <p><%# Item %></p>
                </ItemTemplate>
            </asp:Repeater>
        </div>
    </form>
</body>
</html>

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

using System;
using System.Collections.Generic;
using Binding.Controls;

namespace Binding
{
    public partial class Data : System.Web.UI.Page
    {
        int maxValue;
        string operation;

        protected void Page_LoadComplete(object sender, EventArgs e)
        {
            if (IsPostBack)
            {
                maxValue = int.Parse(max.Value);
                OperationSelector selector = FindControl("opSelector") as OperationSelector;

                if (selector != null)
                    operation = selector.SelectedOperator;
            }
        }

        public IEnumerable<string> GetData()
        {
            if (operation != null)
            {
                for (int i = 1; i < maxValue; i++)
                {
                    yield return string.Format("{0} {1} {2} = {3}",
                        maxValue, operation == "Сложить" ? "+" : "-",
                        i, operation == "Сложить" ? (maxValue + i) : (maxValue - i));
                }
            }
        }
    }
}

Класс отделенного кода построен на основе приемов, описанных в предыдущих статьях. Значение формы и выбранная операция получаются в методе обработчика события LoadComplete. Мы должны применять этот метод, поскольку элемент управления OperationSelector устанавливает свое свойство SelectedOperator в ответ на событие Load.

Значение из элемента <input> серверной стороны получается через объект HtmlControl, используемый для его представления, а с помощью метода FindControl() находится элемент управления OperationSelector. Чтобы увидеть результат, запустите приложение, запросите веб-форму Data.aspx и щелкните на кнопке "Выполнить":

Генерация значений на основе данных из элемента input и элемента управления

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

Большая часть кода в файле Data.aspx.cs отвечает за поддержку метода GetData(), применяемого элементом управления Repeater; мы определили пару полей, так что значения элемента <input> и элемента управления могут быть доступны в методе GetData(), а код, который обрабатывает событие LoadComplete, устанавливает эти поля в рамках подготовительных работ для элемента управления Repeater.

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

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

namespace Binding
{
    public partial class Data : System.Web.UI.Page
    {
        public IEnumerable<string> GetData([Form("max")] int? maxValue,
            [Control("opSelector", "SelectedOperator")] string operation)
        {
            if (operation != null)
            {
                for (int i = 1; i < maxValue; i++)
                {
                    yield return string.Format("{0} {1} {2} = {3}",
                        maxValue, operation == "Сложить" ? "+" : "-",
                        i, operation == "Сложить" ? (maxValue + i) : (maxValue - i));
                }
            }
        }
    }
}

Мы удалили метод обработчика события LoadComplete и поля, которые использовались для хранения значений формы и элемента управления. Взамен мы получаем необходимые значения путем добавления аргументов к методу GetData() и аннотирования этих аргументов атрибутами - атрибут Form получает значение через класс FormValueProvider, тогда как атрибут Control получает значение посредством класса ControlValueProvider.

В таблице ниже перечислены атрибуты привязки моделей и показано, к каким классам поставщиков значений, описанным ранее, они относятся:

Атрибуты привязки моделей
Имя Поставщик Описание
ControlAttribute ControlValueProvider

Извлекает значение из свойства, определенного в элементе управления. Аргументами для этого атрибута являются идентификатор элемента управления и имя свойства

CookieAttribute CookieValueProvider

Извлекает значение из cookie-набора, отправленного браузером как часть запроса. Аргументом для этого атрибута является имя cookie-набора

FormAttribute FormValueProvider

Извлекает значение из данных формы. Аргументом для этого атрибута является имя элемента данных формы

ProfileAttribute ProfileValueProvider

Извлекает значение из данных профиля пользователя. Аргументом для этого атрибута является имя элемента данных профиля

Attribute QueryStringValueProvider

Извлекает значение из строки запроса. Аргументом для этого атрибута является имя параметра строки запроса

RouteDataAttribute RouteDataValueProvider

Извлекает значение из переменных сегментов маршрута URL. Аргументом для этого атрибута является имя переменной сегмента

ViewStateAttribute ViewStateValueProvider

Извлекает значение из данных состояния представления, ассоциированного с запросом. Аргументом для этого атрибута является имя элемента состояния представления

Атрибуты привязки моделей могут применяться только к типам, допускающим значение null, поэтому типом аргумента maxValue является int?, а не int.

Согласно таблице, в примере выше было указано, что значение для аргумента maxValue метода GetData() будет получено из значения данных формы по имени max, а значение для аргумента operation - из свойства SelectedOperation, определенного экземпляром элемента управления OperationSelector, идентификатором которого является opSelector.

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

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

public IEnumerable<string> GetData([Form] int? max,
    [Control("opSelector", "SelectedOperator")] string operation)
{
    if (operation != null)
    {
        for (int i = 1; i < max; i++)
        {
            yield return string.Format("{0} {1} {2} = {3}",
                    max, operation == "Сложить" ? "+" : "-",
                    i, operation == "Сложить" ? (max + i) : (max - i));
        }
    }
}

Мы провели рефакторинг метода GetData(), указав max в качестве имени первого аргумента, которое совпадает с именем элемента <input>, предназначенного для получения значения. Если аргумент атрибута не указан, процесс привязки моделей будет использовать соответствующее имя аргумента метода.

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

Использование атрибутов привязки моделей для сложных типов

В классе Data.aspx привязка модели применялась для получения простых типов данных. Атрибуты привязки моделей предоставляют полный доступ к системе привязки моделей ASP.NET, а это означает, что атрибуты можно использовать также и для привязки сложных типов. В примере ниже приведен модифицированный контент веб-формы Default.aspx, которая теперь содержит элемент управления Repeater:

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

<!DOCTYPE html>
<html>
<head runat="server">
    ...
</head>
<body>
    <form id="form1" runat="server">
        ...
        <div class="panel">
            <asp:Repeater SelectMethod="GetUser" ItemType="Binding.Models.User" 
                ViewStateMode="Disabled" runat="server">
                <ItemTemplate>
                    <div><label>Ваше имя:</label><span><%# Item.Name %></span></div>
                    <div><label>Ваш возраст:</label><span><%# Item.Age %></span></div>
                    <div><label>Ваш номер:</label><span><%# Item.Cell %></span></div>
                    <div><label>Ваш индекс:</label><span><%# Item.Zip %></span></div>
                </ItemTemplate>
            </asp:Repeater>
        </div>
    </form>
</body>
</html>

В примере ниже видно, что файл отделенного кода Default.aspx.cs был упрощен за счет применения атрибута Form для привязки модели User напрямую к методу GetData(), который элемент управления Repeater использует для получения элементов данных:

using System;
using Binding.Models;
using System.Web.ModelBinding;

namespace Binding
{
    public partial class Default : System.Web.UI.Page
    {
        public User GetUser([Form] User user)
        {
            return user;
        }
    }
}

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

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

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

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

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

using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;

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

        public string Cell { get; set; }

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

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            List<ValidationResult> errors = new List<ValidationResult>();
            if (Name == "Вася" && Age < 20)
            {
                errors.Add(
                    new ValidationResult("Васям до 20 лет доступ на сайт запрещен!"));
            }
            return errors;
        }
    }
}

Объект ValidationContext, передаваемый методу Validate() в качестве аргумента, предоставляет информацию о проверяемом экземпляре класса модели, но мы обычно реализуем интерфейс IValidatableObject, чтобы можно было проверять комбинации значений свойств, как показано в примере. Результатом является последовательность объектов ValidationResult, каждый из которых представляет ошибку проверки достоверности - в рассматриваемом примере мы проверяем только одну комбинацию значений свойств, чтобы обеспечить выдачу сообщения об ошибке, когда свойство Name равно "Вася" и свойство Age имеет значение меньше 20.

Для просмотра результата запустите приложение, запросите веб-форму Default.aspx, введите "Вася" в поле "Имя" и 18 в поле "Возраст" и отправьте форму:

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

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

Создание элементов управления для ошибок на уровне полей

В ранних версиях ASP.NET проверка достоверности выполнялась за счет добавления элементов управления в разметку веб-формы - это приводило к путанице и требовало многократного дублирования элементов управления; гораздо лучше применять атрибуты проверки достоверности. Тем не менее, при этом теряется одно средство - возможность отражения ошибок для отдельных полей на основе их атрибутов проверки достоверности. Чтобы воссоздать такую возможность, мы создали в папке Controls файл класса под названием FieldValidator.cs, содержимое которого приведено в примере ниже:

using System.Web.ModelBinding;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace Binding.Controls
{
    public class FieldValidator : WebControl
    {
        public string PropertyName { get; set; }

        protected override void RenderContents(HtmlTextWriter writer)
        {
            ModelState mState;
            if (PropertyName != null && !Page.ModelState.IsValid
                && (mState = Page.ModelState[PropertyName]) != null
                && mState.Errors != null && mState.Errors.Count > 0)
            {
                if (CssClass != null)
                {
                    writer.AddAttribute("class", CssClass);
                }
                writer.RenderBeginTag(HtmlTextWriterTag.Span);
                writer.Write("*");
                writer.RenderEndTag();
            }
        }
    }
}

Серверный элемент управления FieldValidator имеет свойство PropertyName, которое используется для указания свойства модели. Метод RenderContents() с помощью свойства Page.ModelState выясняет, существует ли ошибка состояния модели для указанного свойства, и если это так, визуализирует элемент <span>, который содержит символ звездочки.

Ниже показана регистрация и применение элемента управления FieldValidator внутри веб-формы Default.aspx:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="Binding.Default" %>
<%@ Register TagPrefix="CC" Assembly="Binding" Namespace="Binding.Controls" %>

<!DOCTYPE html>
<html>
<head runat="server">
   ...
</head>
<body>
    <form id="form1" runat="server">
        <asp:ValidationSummary HeaderText="Исправьте следующие ошибки:" CssClass="error" runat="server" />
        <div class="panel">
            <div>
                <div>
                    <label>Имя:</label>
                    <input id="name" runat="server" />
                    <CC:FieldValidator PropertyName="Name" CssClass="error" runat="server" />
                </div>
                <div>
                    <label>Возраст:</label>
                    <input id="age" runat="server" />
                    <CC:FieldValidator PropertyName="Age" CssClass="error" runat="server" />
                </div>
                <div>
                    <label>Номер:</label>
                    <input id="cell" runat="server" />
                    <CC:FieldValidator PropertyName="Cell" CssClass="error" runat="server" />
                </div>
                <div>
                    <label>Индекс:</label>
                    <input id="zip" runat="server" />
                    <CC:FieldValidator PropertyName="Zip" CssClass="error" runat="server" />
                </div>
                <button type="submit">Отправить</button>
            </div>
        </div>
        <div class="panel">
            ...
        </div>
    </form>
</body>
</html>

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

Выделение полей, вызывающих ошибки привязки модели

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

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