Имя входа это уникальное значение в рамках сайта. Процедура его проверки, при создании нового профиля, простая, но специфичная. Поэтому нет особого смысла разрабатывать новый атрибут. Ведь использовать его повторно вряд ли будет необходимость.
Одним из возможных способов, подходящим для решения данной задачи, является применение атрибута [CustomValidation]. Но есть и другое решение данного вопроса. Посмотрим на его реализацию и отличия.
Самостоятельная проверка данных Моделью
А ведь и правда, зачем выносить такой крайне специфичный алгоритм за рамки Модели? Используемый в ASP.NET MVC механизм DataAnnotations, предоставляет возможность объекту самостоятельно осуществлять проверку данных. Для этого ему необходимо поддерживать IValidatableObject.
Посмотрим, что представляет из себя этот интерфейс. Он содержит объявление только одного метода:
namespace System.ComponentModel.DataAnnotations
{
public interface IValidatableObject
{
IEnumerable<ValidationResult> Validate(ValidationContext validationContext);
}
}
Рассмотрим внимательнее, какие данные передаются в Validate() при вызове.
Тип получаемого параметра ValidationContext уже встречался при разработке атрибутов. Его экземпляр содержит информацию о контексте проверки данных. В большинстве случаев, в нем нет особой необходимости. Ведь метод и так принадлежит проверяемому объекту и может обратиться к любому полю, свойству и методу.
Стоит отметить, что в переменной validationContext передается реальный тип. Т.е. если IValidatableObject реализован в базовом классе, то он получит тип потомка. Данный факт стоит учитывать при использовании механизма наследования.
Более интересно возвращаемое значение. Это перечисление, содержащее объекты типа ValidationResult. Таким образом, предоставляется возможность провести тестирование значений всех свойств объекта и вернуть перечень ошибок. Для такого сценария хорошо подходит использование ключевых слов yield return и yield break.
Возвращаемые экземпляры ValidationResult позволяют указать не только сообщение об ошибке, но и для каких свойств оно предназначено. Достаточно использовать вариант конструктора ValidationResult, который дополнительно принимает список имен полей в виде перечисления.
Перед тем как начать реализацию интерфейса IValidatableObject, добавим в ресурсы веб-приложения новое сообщение об ошибке. Поскольку оно характерно для данного класса, то и разместим его в файле NewUserProfile.resx. Его текст будет следующим:
Файл Resources\Model\NewUserProfileRes.resx |
LoginAlreadyExists |
Login already exists, please choose another. |
ERROR: Такое имя входа на сайт уже существует |
Поскольку процедура проверки значения на уникальность относится к бизнес-логике, то разместим её в коде Модели. Для этого в класс UserRepository, ответственный за работу с списком профилей, добавим метод IsLoginExists(). Его возвращаемое значение будет равно true, если указанное имя входа было найдено. В противном случае – false (часть кода класса убрана для компактности):
namespace MVCDemo.Models
{
using System.Collections.Generic;
public class UserRepository
{
.........
public bool IsLoginExists(string login) {
int index = _profiles.FindIndex(
profile => string.Equals(profile.Login, login));
return index != -1;
}
}
}
Теперь необходимо решить, в каком из классов Модели будет реализована проверка уникальности имени пользователя. Рассмотрим с точки зрения их целей. Сам класс UserProfileModel или его потомки могут быть использованы для отображения и, что важно в данном случае, изменения данных пользователя. Логично, что при этом не нужно проверять уникальность имени. С другой стороны, реализация проверки вполне соответствует задаче класса NewUserProfileModel.
Перейдем к его исходному коду и добавим поддержку IValidatableObject. С помощью редактора Visual Studio создадим заготовку для реализации метода Validate(). В его теле осуществим проверку имени входа, вызвав метод IsLoginExists(). Обратите внимание на то, как возвращается результат проверки:
- используется yield return для создания списка ошибок;
- каждую ошибку можно привязать к нескольким свойствам, задав их имена в массиве.
Посмотрим исходный код (часть кода класса убрана для компактности):
namespace MVCDemo.Models
{
.........
public class NewUserProfileModel : UserProfileModel, IValidatableObject
{
.........
#region IValidatableObject Members
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var userRepository = new UserRepository();
if (userRepository.IsLoginExists(this.Login)) {
yield return new ValidationResult(
Resources.Models.NewUserProfileRes.LoginAlreadyExists,
new string[] { "Login" });
}
yield break;
}
#endregion
}
}
Реализация закончена. Теперь вполне уместно будет спросить: а чем она отличается от версии с использованием атрибута [CustomValidation]? Только ли тем, что в том случае можно разместить метод проверки данных в отдельном классе?
Давайте не будем заглядывать в документацию, а запустим веб-приложение и проведем небольшой эксперимент. Он позволит найти ответ на заданный вопрос. Откроем форму добавления профиля. Не будем её заполнять и нажмем кнопку отправки. Пустые значения будут переданы на сервер, обработаны механизмом проверки данных и возвращены вместе с текстами сообщений об ошибках. Рядом с полем ввода "Login" будет выведен текст о необходимости ввести имя.
Попробуем еще раз, но теперь введем только имя входа и обязательно уже существующее. Пусть это будет значение "axel". Отправим форму. После обработки данных сообщение об ошибке рядом с полем Login исчезнет. Причем не смотря на то, что пользователь с таким именем входа уже существует. В этот момент можно решить, что реализованный интерфейс не работает.
Все встанет на свои места, если заполнить по правилам все остальные поля формы. Вместо всех сообщений, причины которых исправлены, появится одно – "Login already exists, please choose another".
Таким образом можно убедиться, что метод Validate() интерфейса IValidatableObject не вызывается до тех пор, пока хотя бы один атрибут не проходит проверку данных. Версия с [CustomValidation] проводила бы тест значения при каждой отправке формы. В этом и заключено отличие между данным вариантами.
Можно сделать вывод, что реализация интерфейса IValidatableObject может пригодиться, если:
- выполнить в классе наследника дополнительные проверки свойств родительского класса;
- логика проверки сложная, например включает много взаимодействий свойств между собой;
- нужно отложить "тяжелые" проверки до тех пор, пока не будут пройдены остальные;
- нет смысла создавать атрибуты, т.к. логика проверки специфическая и лучше создать метод.
Но так же стоит учесть тот факт, что пользователь не увидит результат отложенных проверок до тех пор, пока есть другие ошибки.
Исходный код проекта (C#, Visual Studio 2010):
MVCDemo-Part10.zip (475 Kb)