Альтернативные способы проверки достоверности модели

125

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

Выполнение проверки достоверности в связывателе модели

Стандартный связыватель модели выполняет проверку достоверности как часть процесса привязки. В качестве примера на рисунке ниже показано, что произойдет, если очистить поле "Дата записи" и отправить форму:

Сообщение проверки достоверности

Сообщение об ошибке, отображаемое для поля "Дата записи", было добавлено связывателем модели, поскольку он не смог создать объект DateTime из пустого поля, отправленного в форме. Связыватель модели выполняет некоторую базовую проверку достоверности для каждого свойства в объекте модели. Если значение не было задано, отображается сообщение, показанное на рисунке выше. Если же значение задано, но не может быть успешно преобразовано к типу свойства модели, отображается другое сообщение:

Сообщение об ошибке формата при проверке достоверности, отображаемое связывателем модели

Встроенный класс стандартного связывателя модели, DefaultModelBinder, предлагает несколько полезных методов, которые можно переопределить для добавления проверки достоверности к связывателю. Эти методы описаны в таблице ниже:

Методы класса DefaultModelBinder, предназначенные для добавления проверки достоверности к процессу привязки моделей
Метод Описание Стандартная реализация
OnModelUpdated()

Вызывается, когда связыватель пытается присвоить значения всем свойствам в объекте модели

Применяет правила проверки достоверности, определенные метаданными модели, и регистрирует любые ошибки с помощью ModelState.

SetProperty()

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

Если свойство не поддерживает значения null и никакого значения не предоставлено, с помощью ModelState регистрируется ошибка «The <имя> field is required (Требуется поле <имя>)», показанная на первом рисунке. Если же значение предоставлено, но не может быть разобрано, регистрируется ошибка The value <значение> is not valid for <имя> (Значение <значение> не является допустимым для <имя>), как видно на втором рисунке.

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

Указание правил проверки достоверности с использованием метаданных модели

Инфраструктура ASP.NET MVC Framework поддерживает использование метаданных для выражения правил проверки достоверности моделей. Преимущество применения метаданных заключается в том, что правила проверки достоверности удовлетворяются везде в приложении, где участвует процесс привязки, а не только в одиночном методе действия. Атрибуты проверки достоверности обнаруживаются и учитываются встроенным классом стандартного связывателя модели DefaultModelBinder.

В примере ниже демонстрируется применение нескольких атрибутов проверки достоверности к классу модели Appointment:

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

namespace ModelValidation.Models
{
    public class Appointment
    {
        [Required(ErrorMessage = "Введите свое имя")]
        public string ClientName { get; set; }

        [DataType(DataType.Date)]
        [Required(ErrorMessage = "Введите дату")]
        public DateTime Date { get; set; }

        [Range(typeof(bool), "true", "true",
            ErrorMessage = "Вы должны принять условия")]
        public bool TermsAccepted { get; set; }
    }
}

В этом примере используются два атрибута проверки достоверности - Required и Range. Атрибут Required указывает, что ошибка проверки достоверности возникает, если пользователь не отправил значение для свойства. Атрибут Range задает подмножество приемлемых значений. В таблице ниже приведен набор встроенных атрибутов проверки достоверности, доступных в приложении MVC.

Встроенные атрибуты проверки достоверности
Атрибут Пример Описание
Compare [Compare("Другое Свойство")]

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

Range Range[Range(10, 20)]

Числовое значение (или значение свойства любого типа, реализующего интерфейс IComparable) не должно выходить за пределы, заданные минимальным и максимальным величинами. Для определения границы только с одной стороны применяется константа MinValue или MaxValue - например, [Range(int.MinValue, 50)]

RegularExpression [RegularExpression("шаблон")]

Строковое значение должно соответствовать указанному шаблону регулярного выражения. Обратите внимание, что шаблон должен соответствовать всему предоставленному пользователем значению, а не только какой-то подстроке. По умолчанию при сопоставлении учитывается регистр символов, но с помощью модификатора (?i) его можно сделать нечувствительным к регистру, например, [RegularExpression("(?i)шаблон")]

Required [Required]

Значение не должно быть пустым или быть строкой, состоящей только из пробелов. Чтобы трактовать пробельные символы как допустимые, необходимо использовать [Required(AllowEmptyStrings = true)]

StringLength [StringLength(10)]

Строковое значение не должно быть длиннее заданной максимальной длины. Можно также указывать минимальную длину: [StringLength(10, MinimumLength=2)]

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

[Required(ErrorMessage = "Введите дату")]

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

[Range(typeof(bool), "true", "true",
	ErrorMessage = "Вы должны принять условия")]

Мы хотим удостовериться, что пользователь отметил флажок для принятия условий. Мы не можем использовать атрибут Required, поскольку шаблонизированный вспомогательный метод для значений типа bool генерирует скрытый HTML-элемент, который гарантирует получение значения, даже если флажок не отмечен. Чтобы обойти эту проблему, мы используем возможность атрибута Range, которая позволяет предоставить тип Type, а также указать верхнюю и нижнюю границы как строковые значения. Устанавливая обе границы в true, мы создаем эквивалент атрибута Required для свойств bool, которые редактируются с применением флажков.

Атрибут DataType не может использоваться для проверки достоверности пользовательского ввода, а только с целью предоставления подсказок для визуализации значений с применением шаблонизированных вспомогательных методов. Таким образом, например, не следует ожидать, что атрибут DataType(DataType.EmailAddress) обеспечит использование специфического формата.

Создание специального атрибута проверки достоверности свойства

Трюк с использованием атрибута Range для воссоздания поведения атрибута Required выглядит несколько неуклюже. К, счастью, проверка достоверности не ограничивается только встроенными атрибутами; можно также создать собственный атрибут, унаследовав его от класса ValidationAttribute и реализовав нужную логику проверки достоверности. Это намного удобнее, и чтобы продемонстрировать такой подход в работе, в пример проекта добавлена папка Infrastructure, в которой создан файл класса по имени MustBeTrueAttribute.cs. Содержимое этого нового файла класса приведено в примере ниже:

using System.ComponentModel.DataAnnotations;

namespace ModelValidation.Infrastructure
{
    public class MustBeTrueAttribute : ValidationAttribute
    {
        public override bool IsValid(object value)
        {
            return value is bool && (bool)value;
        }
    }
}

Этот класс определяет новый атрибут MustBeTrue, в котором переопределен метод IsValid() базового класса. Указанный метод связыватель модели будет вызывать дня проверки достоверности свойств, к которым применен атрибут, передавая ему в качестве параметра значение, которое предоставил пользователь.

Логика проверки достоверности проста: значение считается допустимым, если оно относится к типу bool и равно true. Допустимость значения указывается возвратом true из метода IsValid(). В примере ниже приведен код класса Appointment, в котором ранее применяемый атрибут Range заменен специальным атрибутом MustBeTrue:

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

namespace ModelValidation.Models
{
    public class Appointment
    {
        // ...

        [MustBeTrue(ErrorMessage="Вы должны принять условия")]
        public bool TermsAccepted { get; set; }
    }
}

Это намного аккуратнее и проще для понимания, чем ненормальное употребление атрибута Range. Результат работы специального атрибута проверки достоверности модели показан на рисунке ниже:

Сообщение об ошибке, полученное от специального атрибута проверки достоверности

Наследование от встроенных атрибутов проверки достоверности

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

using System;
using System.ComponentModel.DataAnnotations;

namespace ModelValidation.Infrastructure
{
    public class FutureDateAttribute : RequiredAttribute
    {
        public override bool IsValid(object value)
        {
            return base.IsValid(value) && ((DateTime)value) > DateTime.Now;
        }
    }
}

Новый класс FutureDataAttribute унаследован от класса RequiredAttribute и метод IsValid() в нем переопределен для проверки того, что дата относится к будущему. Поскольку внутри него вызывается базовая реализация метода IsValid(), специальный атрибут будет выполнять все базовые шаги проверки достоверности, предусмотренные в атрибуте Required. В примере ниже демонстрируется применение нового атрибута к классу модели Appointment:

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

namespace ModelValidation.Models
{
    public class Appointment
    {
        // ...

        [DataType(DataType.Date)]
        [FutureDate(ErrorMessage = "Введите дату относящуюся к будущему")]
        public DateTime Date { get; set; }
    }
}

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

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

using System;
using System.ComponentModel.DataAnnotations;
using ModelValidation.Models;

namespace ModelValidation.Infrastructure
{
    public class NoVasyaOnMondayAttribute : ValidationAttribute
    {
        public NoVasyaOnMondayAttribute()
        {
            ErrorMessage = "Васи в понедельник отдыхают!";
        }

        public override bool IsValid(object value)
        {
            Appointment app = value as Appointment;
            if (app == null || string.IsNullOrEmpty(app.ClientName) ||
                    app.Date == null)
            {
                // Отсутствует модель правильного типа для проверки или
                // нет значений для обязательных свойств ClientName и Date
                return true;
            }
            else
            {
                return !(app.ClientName == "Вася" && app.Date.DayOfWeek == DayOfWeek.Monday);
            }
        }
    }
}

Когда атрибут проверки достоверности применяется к классу модели в противоположность его применению к одиночному свойству, параметром object, который связыватель модели передает методу IsValid(), будет объект модели - Appointment в этом примере. Атрибут проверки достоверности удостоверяется в том, что объект Appointment действительно имеется, и для свойств ClientName и Date предусмотрены значения, с которыми можно работать. Если все в порядке, выполняется проверка того, что Вася не пытается назначить встречу в понедельник.

В примере ниже специальный атрибут применяется к классу Appointment:

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

namespace ModelValidation.Models
{
    [NoVasyaOnMonday]
    public class Appointment
    {
        // ...
    }
}

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

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

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

Сообщения об ошибках уровня свойств отображаются раньше сообщений об ошибках уровня модели

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

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

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

using System;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
using ModelValidation.Infrastructure;
using System.Collections.Generic;

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

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

        public bool TermsAccepted { get; set; }

        public IEnumerable<ValidationResult> Validate(
            ValidationContext validationContext)
        {
            List<ValidationResult> errors = new List<ValidationResult>();

            if (String.IsNullOrEmpty(ClientName))
                errors.Add(new ValidationResult("Введите свое имя"));

            if (DateTime.Now > Date)
                errors.Add(new ValidationResult("Введите дату относящуюся к будущему"));

            if (errors.Count == 0 && ClientName == "Вася" &&
                Date.DayOfWeek == DayOfWeek.Monday)
                errors.Add(new ValidationResult("Васи в понедельник отдыхают!"));

            if (!TermsAccepted)
                errors.Add(new ValidationResult("Вы должны принять условия"));

            return errors;
        }
    }
}

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

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

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

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

Некоторые программисты предпочитает не помещать логику проверки достоверности в класс модели, но мы считаем, что это хорошо вписывается в шаблон проектирования MVC — к тому же, естественно мне нравится гибкость и согласованность применения.

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