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

Проверка данных. Часть 4 – Создаем атрибуты проверки данных

Рассмотрев в прошлой части основы создания атрибутов проверки данных, приступаем к их реализации. Но сначала установим два соглашения:

  1. Все атрибуты будем разрабатывать так, чтобы была возможность использовать их в других проектах. Это означает, что они не должны быть связаны с конкретной Моделью.
  2. Для размещения файлов с их классами создадим папку Attributes, а в ней еще одну – Validation.

Кроме того, в .NET также существует соглашение об именах классов, реализующих атрибуты. Они должны обязательно иметь окончание Attribute. Однако, его можно не указывать при присвоении самого атрибута объекту. Например, реализация [Display] расположена в классе DisplayAttribute.

Атрибут [ValidDate] – проверка значения указанной даты

Разработаем атрибут, который будет проверять корректно ли значение введенной даты. Для этого будем поверять его на попадание в заданный диапазон. В частности, дата не должна быть больше текущей и меньше некой минимальной, если она задана. Кроме того, условимся, что атрибут [ValidDate] будет работать только со свойствами типа DateTime.

Может возникнуть вопрос: а если полученное от поля значение будет равно пустой ссылке? В общем случае, сравнение любого значение с null заведомо окажется неудачным. А значит, в этом случае атрибут будет требовать ввода значение, то есть копирует [Require].

Вполне логично разделить ответственности. Это также даст большую гибкость при определении ограничений. Поэтому, в общем случае, стоит считать сравнение с полученным от поля ввода значением null всегда успешным. Кстати, именно так ведут себя и другие атрибуты, например [RegularExpression].

Но в рассматриваемом случае есть один нюанс. Дело в том, что в метод IsValid() передается не строковое значение, а свойство конкретного экземпляра Модели, которое надо проверить. Для [ValidDate] оно должно быть типа DateTime по условию, поставленному при разработке. Но поскольку это структура, то его значение не может быть null. А значит подобная проверка не нужна.

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

Начнем разработку. Добавим новый класс ValidDateAttribute в папку Attributes\Validation.

Ограничим область использования атрибута с помощью [AttributeUsage]. Разрешим его назначение только для свойств и без повторного указания. Кроме того, при наследовании класса Модели атрибут будет тоже унаследован.

Поскольку обязательных параметров в данном случае нет, то и конструктор будет без аргументов. Его единственная задача – установить исходное сообщение об ошибке, вызвав конструктор базового класса.

Для ввода минимального значения даты создадим свойство MinDate. При присвоении ему значения будем копировать значение в поле типа DataTime.

Будем дружественны к пользователю и позволим выводить два разных сообщения об ошибке.

  1. Введённая дата некорректна. Для его установки будет использоваться один из двух стандартных вариантов: ErrorMessage или связка из ErrorMessageResourceName и ErrorMessageResourceType.
  2. Введённая дата меньше минимального значения. Чтобы указать это сообщение добавим по аналогии два свойства: MinDateErrorMessage и MinDateErrorResourceName. Кроме того, создадим метод GetMinDateErrorMessage() для получения её строкового представления.

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

Перейдем к главному методу IsValid(). В нем в первую очередь проверим тип полученного значения. После чего следует простая проверка попадания введенной даты в заданный интервал.

namespace BookCatalog.Attributes.Validation
{
    using System;
    using System.ComponentModel.DataAnnotations;

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
    public class ValidDateAttribute : ValidationAttribute
    {
        private DateTime _minimumDate;
        private string _minimumDateString;

        public ValidDateAttribute()
            : base("Invalid date value.")
        {
        }

        public string MinDate
        {
            get { return this._minimumDateString; }
            set
            {
                if (!DateTime.TryParse(value, out this._minimumDate)) {
                    throw new ArgumentException("Invalid MinimumDate value.");
                }

                this._minimumDateString = value;
            }
        }

        public string MinDateErrorMessage { get; set; }

        public string MinDateErrorMessageResourceName { get; set; }

        #region ValidationAttribute overrides

        public override string FormatErrorMessage(string name)
        {
            return string.Format(
                this.ErrorMessageString, name, this._minimumDateString);
        }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            if (!(value is DateTime)) {
                throw new ArgumentException(
                    string.Format(
                        "{0} property type is not DataTime. [ValidDate] attribute must be used with DataTime properties only.",
                        validationContext.DisplayName));
            }

            var enteredDate = (DateTime)value;

            if (enteredDate < this._minimumDate) {
                return new ValidationResult(
                    this.GetMinDateErrorMessage(validationContext.DisplayName));
            }

            if (enteredDate > DateTime.Now) {
                return new ValidationResult(
                    this.FormatErrorMessage(validationContext.DisplayName));
            }

            return ValidationResult.Success;
        }

        #endregion

        private string GetMinDateErrorMessage(string name)
        {
            if (this.ErrorMessageResourceType == null) {
                return this.MinDateErrorMessage;
            }

            var errorMessageProperty =
                this.ErrorMessageResourceType.GetProperty(this.MinDateErrorMessageResourceName);

            var errorMessage = (string)errorMessageProperty.GetValue(null, null);

            return string.Format(errorMessage, name, this._minimumDateString);
        }
    }
}

Создадим сообщение об ошибке для свойства PublishedAt (приведены только добавленные строки).

Файл Resources\Shared\ErrorsRes.resx
DateMustBeGraterThan {0} must me greater than {1}. Дата должна быть больше заданной.
DateСannotBeInTheFuture Date cannot be in the future. Нельзя указать дату из будущего.

Остается добавить новый атрибут к свойству класса BookDetails. Значение минимальной даты установим равным 1 января 1990 года.

namespace BookCatalog.Models
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using BookCatalog.Attributes.Validation;
    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))]
        [DataType(DataType.Text)]
        public string Title { get; set; }

        [Display(Name = "Author", ResourceType = typeof(BookDetailsRes))]
        [Required(ErrorMessageResourceName = "FieldIsRequired",
            ErrorMessageResourceType = typeof(ErrorsRes))]
        [StringLength(128,
            ErrorMessageResourceName = "InvalidStringLenght",
            ErrorMessageResourceType = typeof(ErrorsRes))]
        [DataType(DataType.Text)]
        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))]
        [DataType(DataType.Date)]
        [ValidDate(MinDate = "01.01.1990",
            ErrorMessageResourceName = "DateСannotBeInTheFuture",
            MinDateErrorMessageResourceName = "DateMustBeGraterThan",
            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))]
        [DataType(DataType.Url)]
        [RegularExpression(@"(http(s)?://)?([\w-]+\.)+[\w-]+(/[\w- ;,./?%&=]*)?",
            ErrorMessageResourceName = "InvalidUrl",
            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))]
        [DataType(DataType.MultilineText)]
        public string Description { get; set; }

        [Display(Name = "Rating", ResourceType = typeof(BookDetailsRes))]
        [Range(1, 5,
            ErrorMessageResourceName = "ValueOutOfRange",
            ErrorMessageResourceType = typeof(ErrorsRes))]
        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; }
    }
}

Запустим проект и убедимся, что теперь введенные дата проверяется не только на обязательное наличие, но и на попадание в заданный диапазон.


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

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