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

54

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

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

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

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

К счастью, инфраструктура ASP.NET MVC Framework предлагает широкую поддержку проверки достоверности моделей.

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

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

using System;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;

namespace ModelValidation.Models
{
    public class Appointment
    {
        public string ClientName { get; set; }

        [DataType(DataType.Date)]
        public DateTime Date { get; set; }

        public bool TermsAccepted { get; set; }
    }
}

В классе модели Appointment определены три свойства. При этом с помощью атрибута DataType указано, что свойство Date должно быть выражено в виде даты без компонента времени. Для примера проекта был создан контроллер Home и определены методы действий, оперирующие на классе модели Appointmen:

using System;
using System.Web.Mvc;
using ModelValidation.Models;

namespace ModelValidation.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult MakeBooking()
        {
            return View(new Appointment { Date = DateTime.Now });
        }

        [HttpPost]
        public ViewResult MakeBooking(Appointment appt)
        {
            // В реальном приложении здесь находились бы операторы
            // для сохранения нового объекта Appointment в базе данных
            return View("Completed", appt);
        }
	}
}

Как уже неоднократно делалось в предшествующих статьях, здесь определяются две версии метода действия MakeBooking(). Наиболее интересной является версия, к которой был применен атрибут HttpPost, поскольку именно в ней будет использоваться привязка модели для конструирования объекта параметра Appointment.

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

Для ряда примеров, рассматриваемых далее, требуется простая компоновка. В связи с этим создается папка 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>
        .field-validation-error {
            color: #f00;
        }
        .validation-summary-errors {
            color: #f00;
            font-weight: bold;
        }
        .input-validation-error {
            border: 2px solid #f00;
            background-color: #fee;
        }
        input[type="checkbox"].input-validation-error {
            outline: 2px solid #f00;
        }
    </style>
</head>
<body>
    @RenderBody()
</body>
</html>

Кроме того, понадобится файл запуска представления, чтобы обеспечить автоматическое применение компоновки к представлениям. Для этого в папке Views создан файл _ViewStart.cshtml с содержимым, показанным в примере ниже:

@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}

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

@model ModelValidation.Models.Appointment

@{
    ViewBag.Title = "Запись на собеседование";
}

<h3>Записаться</h3>
@using (Html.BeginForm())
{
    <p>Ваше имя: @Html.EditorFor(m => m.ClientName)</p>
    <p>Дата записи: @Html.EditorFor(m => m.Date)</p>
    <p>@Html.EditorFor(m => m.TermsAccepted) Я принимаю правила и условия</p>
    <input type="submit" value="Записаться" />
}

При выполнении обратной отправки формы приложению метод действия MakeBooking() отображает детали встречи, которую пользователь создал с помощью представления Completed.cshtml, показанного в примере ниже:

@model ModelValidation.Models.Appointment

@{
    ViewBag.Title = "Вы записаны!";
}

<h3>Вы записаны!</h3>
<p>Ваше имя: <b>@Html.DisplayFor(m => m.ClientName)</b></p>
<p>Дата приема: <b>@Html.DisplayFor(m => m.Date)</b></p>

Как вы уже догадались, пример основан на создании встреч. Взглянуть на него в работе можно, запустив приложение и перейдя на URL вида /Home/MakeBooking. Ввод информации в форме и щелчок на кнопке "Записаться" приведет к отправке данных серверу и активизации процесса привязки моделей для создания объекта Appointment, детали которого визуализируются с использованием представления Completed.cshtml:

Работа примера приложения

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

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

Явная проверка достоверности модели

Самый прямой способ проверки достоверности модели предполагает ее выполнение в методе действия. В примере ниже показано, как добавить явные проверки для каждого свойства класса Appointment в версии HttpPost метода действия MakeBooking():

using System;
using System.Web.Mvc;
using ModelValidation.Models;

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

        [HttpPost]
        public ViewResult MakeBooking(Appointment appt)
        {
            if (String.IsNullOrEmpty(appt.ClientName))
                ModelState.AddModelError("ClientName", "Введите свое имя");

            if (ModelState.IsValidField("Date") && DateTime.Now > appt.Date)
                ModelState.AddModelError("Date", "Введите дату относящуюся к будущему");

            if (!appt.TermsAccepted)
                ModelState.AddModelError("TermsAccepted", "Вы должны принять условия");

            if (ModelState.IsValid)
            {
                // В реальном приложении здесь находились бы операторы
                // для сохранения нового объекта Appointment в базе данных
                return View("Completed", appt);
            }
            else
                return View();
        }
	}
}

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

if (String.IsNullOrEmpty(appt.ClientName))
    ModelState.AddModelError("ClientName", "Введите свое имя");

Пользователь должен задать значение для данного свойства, и это проверяется посредством метода String.IsNullOrEmpty(). Если значение не получено, с помощью метода ModelState.AddModelError() указывается имя свойства, вызвавшего проблему (ClientName), и сообщение, которое будет отображаться пользователю, чтобы помочь ему исправить проблему.

Для проверки того, смог ли связыватель модели присвоить значение свойству, служит метод ModelState.IsValidField(). Он используется для свойства Date, позволяя удостовериться, что связыватель модели сумел произвести разбор отправленного пользователем значения; если это не так, то проводить дополнительные проверки и сообщать о других ошибках не имеет смысла.

После проверки достоверности всех свойств в объекте модели производится чтение свойства ModelState.IsValid, чтобы выяснить, возникали ли ошибки. Это свойство возвращает true, если во время проверок вызывался метод Model.State.AddModelError() или у связывателя модели возникали проблемы с созданием объекта Appointment:

if (ModelState.IsValid)
{
    // В реальном приложении здесь находились бы операторы
    // для сохранения нового объекта Appointment в базе данных
    return View("Completed", appt);
}
else
    return View();

Объект Appointment является допустимым, если отсутствуют проблемы, сообщаемые с помощью свойства IsValid, и можно визуализировать представление Completed.cshtml (а в реальном проекте также записывать объект Appointment в базу данных). Если свойство IsValue возвращает false, значит, имеется проблема, поэтому вызывается метод View() для визуализации стандартного представления.

Отображение пользователю ошибок проверки достоверности

Обработка ошибки проверки достоверности путем вызова метода View() может показаться странным подходом, однако шаблонизированные вспомогательные методы представлений, применяемые для генерации элементов ввода в представлении MakeBooking.cshtml, проверяют модель представления на предмет таких ошибок.

Если для соответствующих свойств обнаружены ошибки, то вспомогательные методы добавляют к элементам <input> класс CSS по имени input-validation-error, для чего такие стили CSS и были добавлены в компоновку при создании примера проекта:

...
.input-validation-error {
    border: 2px solid #f00;
    background-color: #fee;
}
input[type="checkbox"].input-validation-error {
    outline: 2px solid #f00;
}
...

Первый стиль обеспечивает установку красной рамки и розового фона для любого элемента, содержащего ошибочные данные. Второй стиль применяет красную рамку к элементам типа флажков. Такие элементы трудно стилизовать и обычно они требуют особого внимания. Чтобы протестировать явный подход к проверке достоверности, необходимо запустить приложение, перейти на URL вида /Home/MakeBooking и щелкнуть на кнопке "Записаться", не вводя никаких данных в форму. Результат показан на рисунке ниже:

Подсветка ошибок модели

Если отправить форму, не заполненную данными, будут выделены как ошибочные элементы ввода для свойств ClientName и TermsAccepted, т.к. значения для них не предоставлены. Стандартное значение, отображаемое для свойства Date, является допустимой датой, но поскольку она не относится к будущему, поле также помечается как содержащее ошибку проверки достоверности.

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

Отображение сообщений проверки достоверности

Классы, которые шаблонизированные вспомогательные методы применяют к элементам <input>, указывают на наличие проблемы с полем, но не сообщают, в чем конкретно состоит проблема. К счастью, существует несколько удобных вспомогательных методов HTML, которые решают эту задачу. В примере ниже демонстрируется применение одного из таких методов к представлению MakeBooking.cshtml (поскольку именно здесь сообщения об ошибках отображаются пользователю):

@model ModelValidation.Models.Appointment

@{
    ViewBag.Title = "Запись на собеседование";
}

<h3>Записаться</h3>
@using (Html.BeginForm())
{
    @Html.ValidationSummary()
    <p>Ваше имя: @Html.EditorFor(m => m.ClientName)</p>
    <p>Дата записи: @Html.EditorFor(m => m.Date)</p>
    <p>@Html.EditorFor(m => m.TermsAccepted) Я принимаю правила и условия</p>
    <input type="submit" value="Записаться" />
}

Вспомогательный метод Html.ValidationSummary() добавляет сводку по ошибкам проверки достоверности, предназначенную для пользователя. Если ошибок нет, никакой HTML-разметки этот вспомогательный метод не генерирует. Пример сводки по проверке достоверности показан на рисунке ниже. Этот результат был получен путем очистки полей данных и отправки формы.

Отображение сводки по проверке достоверности

В сводке по проверке достоверности отображаются сообщения об ошибках, которые были зарегистрированы с помощью ModelState в методе действия MakeBooking(). Ниже приведена HTML-разметка, сгенерированная этим вспомогательным методом:

...
<div class="validation-summary-errors" data-valmsg-summary="true">
    <ul>
	    <li>Введите свое имя</li>
        <li>Введите дату относящуюся к будущему</li>
        <li>Вы должны принять условия</li>
    </ul>
</div>
...

Сообщения об ошибках выражаются в виде списка, содержащегося внутри элемента <div>, к которому применен класс validation-summary-errors. Этот класс соответствует одному из стилей, определенных в файле _Layout.cshtml при создании проекта в начале статьи:

.validation-summary-errors { color: #f00; font-weight: bold;}

Существует несколько перегруженных версий метода ValidationSummary(), и в таблице ниже описаны наиболее полезные из них.

Полезные перегруженные версии вспомогательного метода ValidationSummary()
Перегруженная версия Описание
Html.ValidationSummary()

Генерирует сводку для всех сообщений об ошибках проверки достоверности

Html.ValidationSummary(bool)

Если параметр типа bool равен true, отображаются только сообщения об ошибках уровня модели. Если же параметр bool равен false, отображаются все сообщения об ошибках

Html.ValidationSummary(string)

Отображает сообщение (переданное в параметре типа string) перед сводкой обо всех ошибках проверки достоверности

Html.ValidationSummary (bool, string)

Отображает сообщение (переданное в параметре типа string) перед сообщениями об ошибках проверки достоверности. Если параметр типа bool равен true, будут показаны только сообщения об ошибках уровня модели

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

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

using System;
using System.Web.Mvc;
using ModelValidation.Models;

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

        [HttpPost]
        public ViewResult MakeBooking(Appointment appt)
        {
            // ...

            if (ModelState.IsValidField("ClientName") && ModelState.IsValidField("Date")
                && appt.ClientName == "Вася" && appt.Date.DayOfWeek == DayOfWeek.Monday)
            {
                ModelState.AddModelError("", "Васи в понедельник отдыхают!");
            }

            // ...
        }
	}
}

Перед выполнением проверки, не пытается ли Вася назначить встречу в понедельник, необходимо вызвать метод ModelState.IsValidField(), чтобы удостовериться в допустимости значений ClientName и Date. Это означает, что ошибка уровня модели не будет генерироваться, если предыдущие проверки свойств оказались неудачными. Ошибка уровня модели регистрируется передачей пустой строки ("") в первом параметре методу ModelState.AddModelError(), например:

ModelState.AddModelError("", "Васи в понедельник отдыхают!");

Затем можно обновить файл представления MakeBooking.cshtml для использования версии вспомогательного метода ValidationSummary(), которая принимает параметр типа bool, чтобы отображать только ошибки уровня модели, как показано в примере ниже:

...
@Html.ValidationSummary(true)
...

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

Отображение сводки по проверке достоверности для ошибок уровня модели

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

Отображение сообщений об ошибках уровня свойств

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

@model ModelValidation.Models.Appointment

@{
    ViewBag.Title = "Запись на собеседование";
}

<h3>Записаться</h3>
@using (Html.BeginForm())
{
    @Html.ValidationSummary(true)
    <p>@Html.ValidationMessageFor(m => m.ClientName)</p>
    <p>Ваше имя: @Html.EditorFor(m => m.ClientName)</p>
    <p>@Html.ValidationMessageFor(m => m.Date)</p>
    <p>Дата записи: @Html.EditorFor(m => m.Date)</p>
    <p>@Html.ValidationMessageFor(m => m.TermsAccepted)</p>
    <p>@Html.EditorFor(m => m.TermsAccepted) Я принимаю правила и условия</p>
    <input type="submit" value="Записаться" />
}

Вспомогательный метод Html.ValidationMessageFor() отображает сообщения об ошибках проверки достоверности для одиночного свойства модели. Результат для представления MakeBooking можно видеть на рисунке ниже:

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

Этот вспомогательный метод вставляет HTML-разметку в ответ, если со свойством, к которому он применен, связана ошибка проверки достоверности, и генерирует элементы, подобные приведенным ниже:

...
<p>
    <span class="field-validation-error" data-valmsg-for="ClientName" 
	    data-valmsg-replace="true">
		Введите свое имя
	</span>
</p>
...

Назначенный элементам класс соответствует одному из стилей, которые были определены в файле _Layout.cshtml:

...
.field-validation-error {
    color: #f00;
}
...
Пройди тесты
Лучший чат для C# программистов