Andrey on .NET | Проверка данных. Часть 1 – Механизм проверки данных

Проверка данных. Часть 1 – Механизм проверки данных

В текущей версии демонстрационного веб-приложения при заполнении формы пользователь может ввести и сохранить любые данные. В базу данных попадут даже заведомо 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] необходимо указать откуда нужно взять текст сообщения. В результате код классов Модели будет выглядеть следующим образом:

  • BookDetails.cs
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).

  • Language.cs
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; }
    }
}
  • Publisher.cs
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; }
    }
}
  • Tag.cs
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)

Давайте разберем особенности такого решения:

  1. С точки зрения проектирования, сообщение перестает быть явно связанным с Моделью.
  2. С практической точки зрения, теряются подробности причины ошибки.

Остановимся на втором пункте подробнее. Свойство может иметь несколько атрибутов проверки данных, сообщения кот��рых объединяются при выводе. Однако, в рассматриваемом варианте они будут заменены на заданную в параметре строку. Таким образом, для всех ошибок будет выведен одинаковый текст.

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


Исходный код проекта (C#, Visual Studio 2010): mvc3-in-depth-validation-01.zip

Комментарии (27) -

Виталий 07.03.2011 22:08:20

Классно написано. Не снижайте темп. Знакомлюсь с MVC по Вашим статьям.

Спасибо.

Валентина 17.03.2011 18:18:04

Очень хорошие статьи. Андрей, спасибо.

Andrey Yakovlev 13.04.2011 18:11:38

В файле ресурсов Resources\Shared\ErrorsRes.resx может быть лучше написать так:
Поле "{0}" является обязательным

Третья строка это коментарий в файле ресурсов (это ясно если открыть его на редактирование). Там от {0} толку не будет. Можно их вообще не писать.

Андрей 27.06.2011 0:56:28

Виталий :
Классно написано. Не снижайте темп. Знакомлюсь с MVC по Вашим статьям.
Я тоже, статьи то что надо, и с исходниками, всегда есть что почерпнуть в плане кода.

Спасибо. Постараюсь.

Евгений 22.02.2012 23:07:04

Андрей, помогите, пожалуйста.
У меня при проверке модели в контролере ModelState.IsValid всегда равен true.
Даже в стандартном контролере AccountController со стандартными моделями AccountModels. Никакие атрибуты не срабатывают. С чем это может быть связанно?

@ Евгений: Без кода сказать что-либо сложно.

HI. Снова вопрос. У поля LanguageId появились атрибуты, которых не было в предыдущих главах. Это какое-то продуманное изменение или где-то ошбка закралась?

@ Andriy: А прямо под кодом текст не заметили?

Не заметил. Каюсь. Другой вопрос сразу. ))))
Я убрал атрибут [Required(ErrorMessageResourceName = "MustSelectValue",
    ErrorMessageResourceType = typeof(ErrorsRes))]
у поля LanguageId и при попытке сохранить книгу без языка выдается сообщение
"Требуется поле Language."
Откуда оно помоги понять плз.

@ Andriy:
Все value-типы автоматически получают Required, даже если это явно не указано. Ведь "нет значения" это null, а как в int его записать? В такой ситуации для value-типов используется Nullable<T> или "?". Т.е. поле должно быть, например, "int?".

Андрей скажи пожалуста в какой статье рассматривается вопрос повященный вводу значений поля Tags? Все вроде прочитал (кроме тех кот помечены как старые).

@ Andriy: Нет такой. Планируется при рассмотрении Представлений.

Спасибо за ответ. А то я уж начал волноваться. Андрей поясните пожалуста такой вопрос: в Вашей модели в классе BookDetails есть поля public int LanguageId { get; set; } public int? PublisherId { get; set; }  и поля
public virtual Language Language { get; set; }
public virtual Publisher Publisher { get; set; }
Можете как-нибудь подробнее чем в самом тесте статьи описать зачем дублировать данные. Разве по ссылке нельзя получить значения?

@ Andriy: А посмотрите процесс создания Модели. Там как раз сказано, что свойства Language и Publisher нужны для хранения соответствующих экземпляров классов. Т.е. EF автоматически (при выполнении запроса или в момент обращения к этим свойствам) загрузит необходимые данные.

Евгений Липес 13.07.2012 17:15:12

Андрей, а можно разобрать вопрос использования атрибутов метаданных (DisplayAttribute, ValidationAttribute) для случая DataBaseFirst ?
У нас шеф почему-то категорически не приемлет CodeFirst Frown
Вот и сижу и думаю - как использовать [Display] и валидацию ?
Насколько я понимаю, при изменении структуры БД в DataBaseFirst все сущности будут пересозданы заново ?
У Вас в других ветках упоминается этот вопрос в комментариях, но не очень внятно.
Где-то Вы предлагаете использовать потомков с атрибутами, кто-то говорит, что это повлечет проблемы... Упоминаются partial классы, но как их создать из БД ?
Можно разобрать этот вопрос более внятно и подробно ?

@ Евгений Липес: Да, при изменении структуры БД все сущности будут пересозданы. Насчет статьи навряд ли в ближайшее время, т.к. времени мало. Но как вариант - сделать чтобы DAL отдавал уже нужные вам типы сущности, а внутри DAL настроить маппинг из сущностей EF в них. В частности, сам Linq запрос может возвращать не EF сущности, а указанные вами.

Скажите пожалуйста, в какое время срабатывает ValidationMessageFor? После обращения к контроллеру методом POST, отдавая ему не валидные данные, контроллер высылает ответ, а вью показывает сообщение? Или, все же, до отправки POST запроса?
Спасибо

@ Kostia: В данном случае для демонстрации JavaScript был принудительно отключён в начале статьи. Тогда данные передаются на сервер, там обрабатываются и генерируется новая страница, где уже есть сообщение.

При включенной валидации с помощью JavaScript (в MVC3 и выше это по умолчанию), предварительная проверка происходит до отправки данных. Но это не отменяет проверку на сервере. Об этом написано в статьях далее.

Большое спасибо!

Сергей 21.03.2014 22:13:15

Скажите, пожалуйста, как валидировать Email?

[DataType(DataType.EmailAddress, ErrorMessageResourceName =...)]

или
[EmailAddress(ErrorMessageResourceName =...)]

или еще как-то?
В первом случае не подхватывается сообщение из ресурса
Во втором - Razor ругается, что "Должно быть задано либо свойство ErrorMessageString, либо ErrorMessageResourceName, но не оба."
Не знаю, специально ли, но на этом сайте у поля Email валидация похоже тоже системная - на английском

@ Сергей: Для проверки email необходимо использовать RegularExpressionAttribute с соответствующим выражением.

А что касается ErrorMessageResourceName, то не забываете ли вы также указывать ErrorMessageResourceType?

@Сергей, я столкнулся с такой же проблемой. Нужно установить параметр ErrorMessage = null, тогда все работает. Это баг - connect.microsoft.com/.../emailaddress-attribute-is-unable-to-load-error-message-from-resource-mvc.
@Andrey, использовать регулярные выражения для проверки емайле вроде бы считается не очень хорошим подходом.

@ Evgeny:
Как сказать, но внутри EmailAddressAttribute

static EmailAddressAttribute()
{
    EmailAddressAttribute._regex = new Regex("{код убран}", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture | RegexOptions.Compiled);
}

Pingbacks and trackbacks (3)+

Добавить комментарий