В текущей версии демонстрационного веб-приложения при заполнении формы пользователь может ввести и сохранить любые данные. В базу данных попадут даже заведомо c некорректные с точки зрения бизнес-логики значения. Давайте исправим эту ситуацию и добавим контроль вводимых значений. А начнем с того, что разберемся как работает этот механизм в ASP.MVC 3.
Назад к ASP.NET MVC 2
Сначала, исключительно в демонстрационных целях, отключим некоторые возможности ASP.NET MVC 3. Сейчас не будем вдаваться в суть этих настроек. Просто откроем файл web.config и изменим значение на false для следующих двух параметров:
<appSettings>
<add key="ClientValidationEnabled" value="false"/>
<add key="UnobtrusiveJavaScriptEnabled" value="false"/>
</appSettings>
Чуть позже вернемся к этим параметрам и посмотрим на что они влияют.
Принцип работы механизма проверки данных
Давайте перейдем к Модели. В зависимости от смысла и типа каждого её свойства, можно определить правила, описывающие какие значения они могут принимать. Например, адрес сайта должен соответствовать шаблону "[http или https]://[текст].[от 2 до 3 символов]". Кроме того, на текстовые поля есть ограничение по числу символов в них. Некоторые параметры, такие как название книги, автор, язык и подобные, обязательны для ввода. Зачастую для свойства может набраться несколько различных правил.
Каркас ASP.NET MVC содержит механизм для проверки полученных от пользователя значений. Его работа основана на использовании специальных атрибутов. Несколько упрощая, этот процесс можно описать следующим образом:
- на этапе разработки, свойствам Модели задаются атрибуты, описывающие правила для проверки их значений и, при необходимости, определяющие тексты сообщений об ошибках;
- при выполнении веб-приложения, на основе установленных атрибутов, ядро ASP.NET MVC производит проверку данных, полученных от Представления;
- в Контроллере общий результат проверки Модели (успешная или нет) можно получить обратившись к его свойству ModelState.IsValid;
- Контроллер, зная результат, может определить свои дальнейшее действия.
Перейдем к практике. Давайте посмотрим для примера на метод Create() класса TagsController. В случае неудачи он используется вызов this.View() чтобы отобразить форму. Поскольку параметров нет, то для её создания будет использована текущая Модель, то есть объект tag.
// POST: /Tags/Create
[HttpPost]
public ActionResult Create(Tag tag)
{
if (ModelState.IsValid) {
this._tagRepository.InsertOrUpdate(tag);
this._tagRepository.Save();
return this.RedirectToAction("Index");
}
return this.View();
}
Здесь необходимо отметить, что в проекте используются заготовки по умолчанию из mvcScaffolding. При этом нагляднее передавать экземпляр явно: this.View(tag).
Но как пользователь узнает в чем заключалась ошибка? В рассматриваемом сценарии, ядро ASP.NET MVC вместе с Моделью отправит в Представление сообщения об ошибках. Их текст, как уже упоминалось, был определен при установке атрибутов проверки данных.
Для вывода сообщений в Представлениях используется метод ValidationMessageFor() класса HtmlHelper. Как и в случае с LabelFor(), в качестве параметра ему передается лямбда-выражение, возвращающее значение соответствующего свойства:
<div class="editor-label">
@Html.LabelFor(model => model.Text)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Text)
@Html.ValidationMessageFor(model => model.Text)
</div>
Результатом работы будет HTML код блока span с текстом сообщения.
Указание атрибутов проверки данных для свойств Модели
Давайте разберемся как задаются атрибуты. У нас уже были отмечены свойства, которые обязательны для заполнения. Этот же атрибут [Required] используется и в ASP.NET MVC для аналогичной цели.
[Display(Name = "Title", ResourceType = typeof(BookDetailsRes))]
[Required]
[StringLength(128)]
public string Title { get; set; }
Запустим веб-приложение и попробуем отправить форму с пустыми полями. Сервер обработает запрос и вновь вернет исходную страницу. Только теперь в местах, где в Представлении размещены вызовы метода ValidationMessageFor(), появятся сообщения об ошибках.
Как правило, атрибуты для проверки данных содержат текст сообщения по умолчанию, который и был выведен в этом случае. Однако, он не всегда может подходить для конкретного веб-приложения.
Существует способ задать свой вариант сообщения. Реализации атрибутов являются наследниками класса ValidationAttribute. Поэтому они все наследуют следующие свойства:
- ErrorMessage – определяет строку с текстовым сообщением об ошибке;
- ErrorMessageResourceType – указывает на ресурс, где храниться строка с сообщением;
- ErrorMessageResourceName – задает имя строки в файле ресурса.
Стоит отметить, что некоторые атрибуты позволяют использовать составное форматирование в строке сообщения. Например, [Required] заменит {0} на имя параметра. Причем, будет использован текст, полученный от атрибута [DisplayName] или [Display].
Для примера, откажемся от стандартного сообщения и перенесем его в файл ресурсов. Создадим два варианта: "You must enter a {0}" и "You must select a {0}". Второй пригодится для списков.
Также сразу укажем аналогичным образом сообщения о превышении максимальной длинны строки: "{0} length must be from {2} to {1} characters". Здесь вместо {0} будет подставлено имя параметра, а вместо {2} и {1} минимальное и максимальное число символов.
Все сообщения не связаны с Моделью явно. Поэтому в папке Resources\Shared создадим для их размещения файл ErrorsRes.resx:
Файл Resources\Shared\ErrorsRes.resx |
FieldIsRequired |
You must enter a {0}. |
Поле является обязательным. |
InvalidStringLenght |
{0} length must be from {2} to {1} characters. |
Ограничение по длине строки. |
MustSelectValue |
You must select a {0}. |
Необходимо выбрать значение. |
Теперь в атрибутах [Required] необходимо указать откуда нужно взять текст сообщения. В результате код классов Модели будет выглядеть следующим образом:
namespace BookCatalog.Models
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BookCatalog.Resources.Models;
using BookCatalog.Resources.Shared;
[Table("Catalog")]
public class BookDetails
{
public int Id { get; set; }
[Display(Name = "Title", ResourceType = typeof(BookDetailsRes))]
[Required(ErrorMessageResourceName = "FieldIsRequired",
ErrorMessageResourceType = typeof(ErrorsRes))]
[StringLength(128,
ErrorMessageResourceName = "InvalidStringLenght",
ErrorMessageResourceType = typeof(ErrorsRes))]
public string Title { get; set; }
[Display(Name = "Author", ResourceType = typeof(BookDetailsRes))]
[Required(ErrorMessageResourceName = "FieldIsRequired",
ErrorMessageResourceType = typeof(ErrorsRes))]
[StringLength(128,
ErrorMessageResourceName = "InvalidStringLenght",
ErrorMessageResourceType = typeof(ErrorsRes))]
public string Author { get; set; }
[Display(Name = "Language", ResourceType = typeof(BookDetailsRes))]
[Required(ErrorMessageResourceName = "MustSelectValue",
ErrorMessageResourceType = typeof(ErrorsRes))]
public int LanguageId { get; set; }
public int? PublisherId { get; set; }
[Display(Name = "PublishedAt", ResourceType = typeof(BookDetailsRes))]
[Required(ErrorMessageResourceName = "FieldIsRequired",
ErrorMessageResourceType = typeof(ErrorsRes))]
public DateTime PublishedAt { get; set; }
[Display(Name = "Url", ResourceType = typeof(BookDetailsRes))]
[Required(ErrorMessageResourceName = "FieldIsRequired",
ErrorMessageResourceType = typeof(ErrorsRes))]
[StringLength(256, MinimumLength = 11,
ErrorMessageResourceName = "InvalidStringLenght",
ErrorMessageResourceType = typeof(ErrorsRes))]
public string Url { get; set; }
[Display(Name = "Description", ResourceType = typeof(BookDetailsRes))]
[Required(ErrorMessageResourceName = "FieldIsRequired",
ErrorMessageResourceType = typeof(ErrorsRes))]
[StringLength(512, MinimumLength = 32,
ErrorMessageResourceName = "InvalidStringLenght",
ErrorMessageResourceType = typeof(ErrorsRes))]
public string Description { get; set; }
[Display(Name = "Rating", ResourceType = typeof(BookDetailsRes))]
public int? Rating { get; set; }
/// <summary>Gets or sets a value indicating whether
/// the book is free (true) or not (false).</summary>
[Display(Name = "IsFree", ResourceType = typeof(BookDetailsRes))]
public bool IsFree { get; set; }
/// <summary>Gets or sets a value indicating whether
/// the book is visible in the catalog (true) or not (false).</summary>
[Display(Name = "IsVisible", ResourceType = typeof(BookDetailsRes))]
public bool IsVisible { get; set; }
[Display(Name = "Tags", ResourceType = typeof(BookDetailsRes))]
public virtual ICollection<Tag> Tags { get; set; }
[Display(Name = "Language", ResourceType = typeof(BookDetailsRes))]
public virtual Language Language { get; set; }
[Display(Name = "Publisher", ResourceType = typeof(BookDetailsRes))]
public virtual Publisher Publisher { get; set; }
}
}
Обратите внимание, что здесь сообщение для свойства Language указано у LanguageId. Дело в том, что именно данные этого свойства используются в шаблоне.
Кроме того, для некоторых свойств здесь и в других классах указана минимальная длина строки. Значения определены исходя из бизнес-логики. Например, длина ссылки сайта не может быть меньше 11 символов, так как необходимо указать протокол и минимум еще четыре символа (например: http://a.ru).
namespace BookCatalog.Models
{
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BookCatalog.Resources.Models;
using BookCatalog.Resources.Shared;
public class Language
{
public int Id { get; set; }
[Display(Name = "Name", ResourceType = typeof(LanguageRes))]
[Required(ErrorMessageResourceName = "FieldIsRequired",
ErrorMessageResourceType = typeof(ErrorsRes))]
[StringLength(64, MinimumLength = 2,
ErrorMessageResourceName = "InvalidStringLenght",
ErrorMessageResourceType = typeof(ErrorsRes))]
public string Name { get; set; }
[Display(Name = "Books", ResourceType = typeof(LanguageRes))]
public virtual ICollection<BookDetails> Books { get; set; }
}
}
namespace BookCatalog.Models
{
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BookCatalog.Resources.Models;
using BookCatalog.Resources.Shared;
public class Publisher
{
public int Id { get; set; }
[Display(Name = "Title", ResourceType = typeof(PublisherRes))]
[Required(ErrorMessageResourceName = "FieldIsRequired",
ErrorMessageResourceType = typeof(ErrorsRes))]
[StringLength(128,
ErrorMessageResourceName = "InvalidStringLenght",
ErrorMessageResourceType = typeof(ErrorsRes))]
public string Title { get; set; }
[Display(Name = "Homepage", ResourceType = typeof(PublisherRes))]
[StringLength(256, MinimumLength = 11,
ErrorMessageResourceName = "InvalidStringLenght",
ErrorMessageResourceType = typeof(ErrorsRes))]
public string Homepage { get; set; }
[Display(Name = "Books", ResourceType = typeof(PublisherRes))]
public virtual ICollection<BookDetails> Books { get; set; }
}
}
namespace BookCatalog.Models
{
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BookCatalog.Resources.Models;
using BookCatalog.Resources.Shared;
public class Tag
{
public int Id { get; set; }
[Display(Name = "Text", ResourceType = typeof(TagRes))]
[Required(ErrorMessageResourceName = "FieldIsRequired",
ErrorMessageResourceType = typeof(ErrorsRes))]
[StringLength(32, MinimumLength = 2,
ErrorMessageResourceName = "InvalidStringLenght",
ErrorMessageResourceType = typeof(ErrorsRes))]
public string Text { get; set; }
[Display(Name = "Books", ResourceType = typeof(TagRes))]
public virtual ICollection<BookDetails> Books { get; set; }
}
}
Запустим веб-приложение и убедимся, что при отсутствии значений выводятся сообщения об ошибках.
Указание сообщения об ошибке в Представлении
Необходимо упомянуть еще об одном способе вывода сообщений, который используется непосредственно в Представлении. Это передача его в качестве параметра в метод ValidationMessageFor(). Например, вот так будет выглядеть код, использующий текст из созданного выше ресурса:
@using BookCatalog.Resources.Shared;
@Html.ValidationMessageFor(model => model.Title, ErrorsRes.FieldIsRequired)
Давайте разберем особенности такого решения:
- С точки зрения проектирования, сообщение перестает быть явно связанным с Моделью.
- С практической точки зрения, теряются подробности причины ошибки.
Остановимся на втором пункте подробнее. Свойство может иметь несколько атрибутов проверки данных, сообщения кот��рых объединяются при выводе. Однако, в рассматриваемом варианте они будут заменены на заданную в параметре строку. Таким образом, для всех ошибок будет выведен одинаковый текст.
Хорошо это или плохо зависит от контекста использования. Например, появляется возможность указать обобщенный вариант сообщения. Такой подход может пригодиться, если с точки зрения пользователя нет разницы между причинами ошибки. Кроме того, исходные сообщения могут быть очень длинными, сильно нагруженные техническими подробностями и т.д.
Исходный код проекта (C#, Visual Studio 2010):
mvc3-in-depth-validation-01.zip