Часть 10 – Модель, проверяющая сама себя

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

Одним из возможных способов, подходящим для решения данной задачи, является применение атрибута [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)

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

Александр 21.04.2011 18:38:14

Спасибо, очень интересный цикл статей. Очень жду продолжения!
Есть вопрос: делаю почти тоже самое, что и в примере выше, только дополнителько еще проверяю, а не тот же самый ли это объект:

public bool isNumberExists(byte number, int ignoreID)

Но в модели
if (reportRepository.isNumberExists(this.Number, this.ReportID))
{
      yield return new ValidationResult(
           Resources.Models.FundRes.NumberAlreadyExists,
           new string[] { "Number" });
}
поле ReportID почему-то недоступно...
Помогите, пожалуйста!

Александр 21.04.2011 19:00:07

Сейчас копался дебагером и понял, что в модели как раз поле ReportID доступно.
Что-то не так с

[Remote("IsNumberExists", "Reports", HttpMethod = "POST", 

          ErrorMessageResourceName = "NumberAlreadyExists", ErrorMessageResourceType = typeof(ReportRes))]
        public byte Number { get; set; }

Я, наверное, не совсем понял тему... Мне нужно, чтобы здесь проходила проверка на уникальность номера, но кроме владельца данного номера. Т.е. есть отчет с ID = 5, то проверяем введенный номер на уникальность у всех отчетов кроме номера 5

Александр 21.04.2011 19:24:38

Ну какой-же я недотёпа)))
Там где вы про Remote писали, там же вы писали и про AdditionalField.
Вот как я использовал:

[Remote("IsNumberExists", "Reports", HttpMethod = "POST", AdditionalFields = "ReportID"

И, еще: насколько я понял (методом тыка) важно, чтобы в контроллере второй параметр тоже назывался ReportID

@ Александр: Буду краток - да, имена параметров Действия должны совпадать с именами передаваемых полей.

Вопрос по этому куску кода:



public IEnumerable 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;
}

Скажите, Андрей, а как в этом методе можно было бы добраться до _profiles в классе UserRepository если бы оно не было статическим полем этого класса?

И еще, пожалуйста, скажит есть разница между:


int index = _profiles.FindIndex(profile => string.Equals(profile.Login, login));

и

int index = _profiles.FindIndex(profile => profile.Login.Equals(login));

?

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

2) Во-втором случае при profile.Login==null получим исключение.

Andrey :
1) Не совсем понял первый вопрос. Статическое это поле только для того, чтобы все экземпляры класса отдавали одни и те же данные. Т.е. эмуляция базы данных.

2) Во-втором случае при profile.Login==null получим исключение.
у меня просто 2-ой вариант почему не работает: -1 возвращает, т.е не находит совпадений...

@ Nuts: А отладчик что говорит про сравниваемые значения?

@ Andrey:
просмотрел c F11 построчно и  теперь почему-то работает) видимо до этого просто пару раз опечатался когда логин вводил...
Хотелось бы уточнить, а почему будет выброшено исключение для profile.Login==null?

Андрей, огромное спасибо вам за этот ресурс - все очень емко, по делу -
продолжайте в том же духе!

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

@ Nuts:
Если profile.Login == null, то какое поведение вы ожидаете от вызова profile.Login.Equals() ?

За отзыв спасибо, но с вводную статью пока не планирую. Работы много, что не успеваю даже закончить начатое по MVC.

"...Более интересно возвращаемое значение. Это перечисление, содержащее объекты типа ValidationResult..."

Перечислением принято называть enum, а не IEnumerable Smile

А так цикл статей очень даже хорош!

Oleg Спасибо.
А насчет IEnumerable - ну как его еще назвать. Список - List. Перечисление самое подходящее. Хотя согласен есть путаница с enum.

Распечатал почти полностью Ваш цикл статей (получилось около 100 страниц), очень детальный анализ вопросов метаданных и валидации в ASP.NET MVC. Вполне тянет на книгу... Smile

Pingbacks and trackbacks (3)+

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