ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

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

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

Валидация в механизме связывания

В механизме связывания по умолчанию валидация выполняется как часть процесса связывания. Например, в рисунке 23-6 показано, что произойдет, если мы очистим поле Date и отправим форму.

Рисунок 23-6: Сообщение валидации от механизма связывания

Ошибка, отображенная для поля Date, была добавлена механизмом связывания, потому что он не смог создать объект DateTime из пустого поля, которое мы отправили в форме. Механизм связывания проводит базовую проверку для каждого свойства в объекте модели. Если значение не предоставлено, будет отображаться сообщение, показанное на рисунке 23-6. Если мы предоставим значение, которое не может быть преобразовано в тип свойства модели, то будет отображаться другое сообщение, как показано на рисунке 23-7.

Рисунок 23-7: Ошибка формата, отображенная механизмом связывания

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

Таблица 23-2: Методы класса DefaultModelBinder для добавления валидации модели в процесс связывания
Метод Описание Реализация по умолчанию
OmModelUpdated Будет вызван, когда механизм связывания уже сделал попытку присвоить значения всем свойствам в объекте модели. Применяет правила валидации, определенные в метаданных модели, и регистрирует любые ошибки в ModelState. Использование метаданных для валидации будет описано позже в этой главе.
SetProperty Будет вызван, когда механизм связывания хочет применить значение к конкретному свойству. Будет вызван, когда механизм связывания хочет применить значение к конкретному свойству. Если свойство не может содержать значение null и для него нет никакого другого значения, то в ModelState будет зарегистрирована ошибка The <name> field is required (см. рисунок 23-6). Если значение имеется, но его невозможно проанализировать, то будет зарегистрирована ошибка The value <value> is not valid for <name> (см. рисунок 23-7).

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

Определяем правила валидации с помощью метаданных

MVC Framework поддерживает использование метаданных для создания правил валидации моделей. Преимущество использования метаданных заключается в том, что правила валидации будут применяться в приложении везде, где применяется процесс связывания, а не только в одном методе действия. Атрибуты валидации обнаруживаются и применяются стандартным классом механизма связывания, DefaultModelBinder, о которых мы рассказывали в главе 22. В листинге 23-10 показано, как мы применили атрибуты валидации к классу модели Appointment.

Листинг 23-10: Определяем правила валидации для класса модели Appointment с помощью атрибутов
using System;
using System.ComponentModel.DataAnnotations;

namespace ModelValidation.Models
{
	public class Appointment
	{
		[Required]
		public string ClientName { get; set; }

		[DataType(DataType.Date)]
		[Required(ErrorMessage = "Please enter a date")]
		public DateTime Date { get; set; }

		[Range(typeof (bool), "true", "true", ErrorMessage = "You must accept the terms")]
		public bool TermsAccepted { get; set; }
	}
}

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

Таблица 23-3: Встроенные атрибуты валидации
Атрибут Пример Описание
Compare [Compare("MyOtherProperty")] Два свойства должны иметь одинаковые значения. Используется, когда пользователь должен предоставить одну и ту же информацию дважды, например адрес электронной почты или пароль.
Range [Range(10, 20)] Числовое значение (или любой тип свойства, который реализует IComparable) не должна выходить за указанные минимальное и максимальное значения. Чтобы указать границу только с одной стороны, используйте константу MinValue или MaxValue, например, [Range (int.MinValue, 50)].
RegularExpression [RegularExpression("pattern")] Строковое значение должно соответствовать указанной схеме регулярного выражения. Обратите внимание, что шаблон должен полностью совпадать с предоставленным пользователем значение, а не только с подстрокой в нем. По умолчанию при сопоставлении учитывается регистр, но вы можете отключить учет регистра с помощью модификатора (?i), например, [RegularExpression("(?i)mypattern")].
Required [Required] Значение не должно быть пустым и не должно быть быть строкой, состоящей только из пробелов. Если вы хотите сделать пробелы действительными значениями, используйте [Required(AllowEmptyStrings = true)].
StringLength [StringLength (10)] Строковое значение не должно превышать указанную максимальную длину. Можно также установить минимальную длину: [StringLength (10, MinimumLength = 2)].

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

[Required(ErrorMessage="Please enter a date")]

Если мы не создаем пользовательское сообщение об ошибке, то будет использоваться сообщение по умолчанию, вроде тех, которые были показаны на рисунках 23-6 и 23-7. Встроенные атрибуты валидации обеспечивают только базовую функциональность, и их можно использовать только для валидации свойств. К тому же, нам все еще придется потрудиться, чтобы они выполняли свои функции последовательно. Например, рассмотрим атрибут валидации, который мы применили к свойству TermsAccepted:

[Range(typeof(bool), "true", "true", ErrorMessage="You must accept the terms")]

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

Подсказка

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

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

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

Листинг 23-11: Пользовательский атрибут валидации свойств
using System.ComponentModel.DataAnnotations;

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

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

Мы используем простую логику валидации: значение будет действительным, если это bool, который имеет значение true. Чтобы указать, что значение допустимо, мы возвращаем true из метода IsValid. В листинге 23-12 показано, как мы заменили атрибут Range на пользовательский атрибут MustBeTrue в классе Appointment.

Листинг 23-12: Применяем пользовательский атрибут валидации в классе модели
using System;
using System.ComponentModel.DataAnnotations;
using ModelValidation.Infrastructure;

namespace ModelValidation.Models
{
	public class Appointment
	{
		[Required]
		public string ClientName { get; set; }

		[DataType(DataType.Date)]
		[Required(ErrorMessage = "Please enter a date")]
		public DateTime Date { get; set; }

		[MustBeTrue(ErrorMessage = "You must accept the terms")]
		public bool TermsAccepted { get; set; }
	}
}

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

Рисунок 23-8: Сообщение об ошибке от пользовательского атрибута валидации

Наследуем встроенные атрибуты валидации

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

Листинг 23-13: Содержимое файла FutureDateAttribute.cs
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. В листинге 23-14 показано, как мы применили наш новый атрибут в классе модели Appointment.

Листинг 23-14: Применяем пользовательский атрибут валидации модели в классе Appointment
using System;
using System.ComponentModel.DataAnnotations;
using ModelValidation.Infrastructure;
using System.Web.Mvc;

namespace ModelValidation.Models
{
	public class Appointment
	{
		[Required]
		public string ClientName { get; set; }

		[DataType(DataType.Date)]
		[FutureDate(ErrorMessage = "Please enter a date in the future")]
		public DateTime Date { get; set; }

		[MustBeTrue(ErrorMessage = "You must accept the terms")]
		public bool TermsAccepted { get; set; }
	}
}

Создаем атрибут валидации модели

Пользовательские атрибуты валидации, которые мы создали до сих пор, применялись к отдельным свойствам модели. Это означает, что они в состоянии обнаружить только ошибки валидации уровня свойств. Атрибуты можно использовать также для валидации всей модели, что позволяет нам обнаружить ошибки уровня модели. В качестве демонстрации, мы создали класс NoJoeOnMondaysAttribute.cs в папке Infrastructure. Его содержание показано в листинге 23-15.

Листинг 23-15: Создаем пользовательский атрибут валидации модели
using System;
using System.ComponentModel.DataAnnotations;
using ModelValidation.Models;

namespace ModelValidation.Infrastructure
{
	public class NoJoeOnMondaysAttribute : ValidationAttribute
	{
		public NoJoeOnMondaysAttribute()
		{
			ErrorMessage = "Joe cannot book appointments on Mondays";
		}

		public override bool IsValid(object value)
		{
			Appointment app = value as Appointment;
			if (app == null || string.IsNullOrEmpty(app.ClientName) ||
				  app.Date == null)
			{
				// we don't have a model of the right type to validate, or we don't have
				// the values for the ClientName and Date properties we require
				return true;
			}
			else
			{
				return !(app.ClientName == "Joe" &&
					        app.Date.DayOfWeek == DayOfWeek.Monday);
			}
		}
	}
}

Когда мы применим атрибут валидации к классу модели, а не к одному свойству, параметр объекта, который механизм связывания передаст в метод IsValid, будет являться объектом модели (в данном примере - Appointment). Наш атрибут валидации проверит, существует ли объект Appointment, далее - есть ли у нас действительные значения для свойств ClientName и Date. Если да, мы проверяем, не пытается ли Джо записаться на прием в понедельник. В листинге 23-16 показано, как мы применили пользовательский атрибут к классу Appointment.

Листинг 23-16: Применяем пользовательский атрибут валидации уровня модели к классу модели
using System;
using System.ComponentModel.DataAnnotations;
using ModelValidation.Infrastructure;

namespace ModelValidation.Models
{
	[NoJoeOnMondays]
	public class Appointment
	{
		[Required]
		public string ClientName { get; set; }

		[DataType(DataType.Date)]
		[FutureDate(ErrorMessage = "Please enter a date in the future")]
		public DateTime Date { get; set; }

		[MustBeTrue(ErrorMessage = "You must accept the terms")]
		public bool TermsAccepted { get; set; }
	}
}

На данный момент мы применяем одни и те же правила валидации в методе действия и в атрибуте валидации, что означает, что пользователь увидит два одинаковых сообщения об ошибке для одной и той же проблемы . Чтобы этого не происходило, мы удалили явную проверку из метода действия MakeBooking в контроллере Home, как показано в листинге 23-17, и переложили всю ответственность за выполнение валидации на пользовательские атрибуты.

Листинг 23-17: Удаляем явную проверку из метода действия MakeBooking
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)
			{
				// statements to store new Appointment in a
				// repository would go here in a real project
				return View("Completed", appt);
			}
			else
			{
				return View();
			}
		}
	}
}

Важно отметить, что атрибуты валидации уровня модели не будут использоваться, если будет обнаружена проблема на уровне свойства. Чтобы увидеть, как это работает, запустите приложение и перейдите по ссылке /Home/MakeBooking. Введите Joe в поле для имени, 2/17/2014 для даты, и оставьте чекбокс неотмеченным.

Когда вы отправите форму, то увидите только предупреждение о чекбоксе. Отметьте чекбокс и отправьте форму снова. Только теперь вы увидите ошибку уровня модели, как показано на рисунке 23-9.

Рисунок 23-9: Ошибки уровня свойства отображается раньше ошибок уровня модели

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

Определяем модели с автоматической валидацией

Другой способ валидации - создать модели с автоматической валидацией, где логика валидации будет частью класса модели. Классы моделей с автоматической валидацией реализуют интерфейс IValidatableObject, как показано в листинге 23-18.

Листинг 23-18: Класс модели с автоматической валидацией
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using ModelValidation.Infrastructure;

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("Please enter your name"));
			}
			if (DateTime.Now > Date)
			{
				errors.Add(new ValidationResult("Please enter a date in the future"));
			}
			if (errors.Count == 0 && ClientName == "Joe"
				  && Date.DayOfWeek == DayOfWeek.Monday)
			{
				errors.Add(
					new ValidationResult("Joe cannot book appointments on Mondays"));
			}
			if (!TermsAccepted)
			{
				errors.Add(new ValidationResult("You must accept the terms"));
			}
			return errors;
		}
	}
}

Интерфейс IValidatableObject определяет один метод - Validate. Этот метод принимает параметр ValidationContext, хотя этот тип не является специфическим для MVC и редко используется.Результатом метода Validate является перечисление объектов ValidationResult, каждый из которых представляет собой ошибку валидации.

Если класс модели реализует интерфейс IValidatableObject, то метод Validate будет вызван после того, как механизм связывания присвоит значения каждому из свойств модели. Преимущество этого подхода в том, что он объединяет гибкость логики валидации, содержащейся в методе действия, и последовательность ее применения каждый раз, когда механизм связывания создает экземпляр типа нашей модели. Мы также выигрываем от объединения валидации уровня моделей и свойств в одном месте, что означает, что все ошибки будут отображены одновременно, как показано на рисунке 23-10. Некоторые программисты не любят помещать логику валидации в класс модели, но мы считаем, что этот подход хорошо вписывается в шаблон проектирования MVC – и, конечно, нам нравится его гибкость и последовательность.

Рисунок 23-10: Эффект модели с автоматической валидацией
или RSS канал: Что новенького на smarly.net