Рассмотрев в прошлой части основы создания атрибутов проверки данных, приступаем к их реализации. Но сначала установим два соглашения:
- Все атрибуты будем разрабатывать так, чтобы была возможность использовать их в других проектах. Это означает, что они не должны быть связаны с конкретной Моделью.
- Для размещения файлов с их классами создадим папку Attributes, а в ней еще одну – Validation.
Кроме того, в .NET также существует соглашение об именах классов, реализующих атрибуты. Они должны обязательно иметь окончание Attribute. Однако, его можно не указывать при присвоении самого атрибута объекту. Например, реализация [Display] расположена в классе DisplayAttribute.
Атрибут [ValidDate] – проверка значения указанной даты
Разработаем атрибут, который будет проверять корректно ли значение введенной даты. Для этого будем поверять его на попадание в заданный диапазон. В частности, дата не должна быть больше текущей и меньше некой минимальной, если она задана. Кроме того, условимся, что атрибут [ValidDate] будет работать только со свойствами типа DateTime.
Может возникнуть вопрос: а если полученное от поля значение будет равно пустой ссылке? В общем случае, сравнение любого значение с null заведомо окажется неудачным. А значит, в этом случае атрибут будет требовать ввода значение, то есть копирует [Require].
Вполне логично разделить ответственности. Это также даст большую гибкость при определении ограничений. Поэтому, в общем случае, стоит считать сравнение с полученным от поля ввода значением null всегда успешным. Кстати, именно так ведут себя и другие атрибуты, например [RegularExpression].
Но в рассматриваемом случае есть один нюанс. Дело в том, что в метод IsValid() передается не строковое значение, а свойство конкретного экземпляра Модели, которое надо проверить. Для [ValidDate] оно должно быть типа DateTime по условию, поставленному при разработке. Но поскольку это структура, то его значение не может быть null. А значит подобная проверка не нужна.
Также, забегая немного вперед, стоит сказать, что контроль соответствия введенной строки формату даты производится в другом месте. В итоге, в методе IsValid() достаточно просто контролировать тип полученного значения.
Начнем разработку. Добавим новый класс ValidDateAttribute в папку Attributes\Validation.
Ограничим область использования атрибута с помощью [AttributeUsage]. Разрешим его назначение только для свойств и без повторного указания. Кроме того, при наследовании класса Модели атрибут будет тоже унаследован.
Поскольку обязательных параметров в данном случае нет, то и конструктор будет без аргументов. Его единственная задача – установить исходное сообщение об ошибке, вызвав конструктор базового класса.
Для ввода минимального значения даты создадим свойство MinDate. При присвоении ему значения будем копировать значение в поле типа DataTime.
Будем дружественны к пользователю и позволим выводить два разных сообщения об ошибке.
- Введённая дата некорректна. Для его установки будет использоваться один из двух стандартных вариантов: ErrorMessage или связка из ErrorMessageResourceName и ErrorMessageResourceType.
- Введённая дата меньше минимального значения. Чтобы указать это сообщение добавим по аналогии два свойства: 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