Andrey on .NET | Часть 22.6 – Провайдер конфигурации Модели. Завершение

Часть 22.6 – Провайдер конфигурации Модели. Завершение

Реализация поддержки правил и разработка провайдера конфигурации Модели завершена. Давайте теперь задействуем его в проекте и посмотрим на него в действии.

Итоговая версия провайдера конфигурации Модели

Прежде всего добавим созданные провайдеры в класс ModelConfigurationProvider.

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

Для провайдера метаданных Модели отдельное поле не требуется. Его сразу можно объявить как свойство типа IModelMetadataProvider.

Кроме того, создадим метод AddValidatiorRule(), которой позволит пользователям добавлять новые реализации ModelValidatorRule в коллекцию Rules провайдера правил проверки данных. Его первым параметром будет имя элемента, с которым связано само правило. В качестве второго параметра будет передаваться ссылка на экземпляр реализации.

В итоге этих изменений, часть класса ModelConfigurationProvider, размещенная в файле ModelConfigurationProvider.cs, будет выглядеть следующим образом:

/// Copyright (c) 2011 Andrey Veselov. All rights reserved.
/// WebSite: http://andrey.moveax.ru 
/// Email: andrey@moveax.ru
/// This source is subject to the Microsoft Public License (Ms-PL).

namespace MVCDemo.Models.Configuration
{
    using System;
    using System.Collections.Generic;
    using System.Web.Mvc;
    using System.Xml.Linq;
    using MVCDemo.Models.Configuration.Rules;
    using MVCDemo.Models.Configuration.Sources;
    using MVCDemo.Models.MetadataProviders;

    public partial class ModelConfigurationProvider
    {
        #region Private fields

        private readonly Dictionary<Type, ModelConfiguraion> _cache;

        private readonly ModelValidatorProviderAdapter _validatorProvider;

        #endregion

        public ModelConfigurationProvider()
        {
            this.Sources = new List<IModelConfigurationSource>();
            this._cache = new Dictionary<Type, ModelConfiguraion>();

            this.MetadataProvider = new ModelMetadataProviderAdapter(this);
            this._validatorProvider = new ModelValidatorProviderAdapter(this);
        }

        #region Properties

        public IModelMetadataProvider MetadataProvider { get; private set; }

        public ModelValidatorProvider ValidatorProvider
        {
            get { return this._validatorProvider; }
        }

        public List<IModelConfigurationSource> Sources { get; private set; }

        #endregion

        public void AddValidatiorRule(string ruleName, IModelValidatorRule rule)
        {
            this._validatorProvider.Rules.Add(ruleName, rule);
        }

        private ModelConfiguraion GetModelConfiguraion(Type modelType)
        {
            foreach (var modelConfiguraion in this._cache) {
                if (modelConfiguraion.Key.Equals(modelType)) {
                    return modelConfiguraion.Value;
                }
            }

            foreach (var source in this.Sources) {
                bool useCache = false;
                XElement config = source.GetModelConfiguration(modelType, out useCache);
                if (config != null) {
                    var data = new ModelConfiguraion(config);

                    if (useCache) {
                        this._cache.Add(modelType, data);
                    }

                    return data;
                }
            }

            return null;
        }

        private class ModelConfiguraion
        {
            public ModelConfiguraion(XElement source)
            {
                this.Source = source;

                var resourceAttr = this.Source.Attribute("resource");
                if (resourceAttr != null) {
                    this.DefaultResourceType = Type.GetType(resourceAttr.Value, throwOnError: true);
                }
            }

            public XElement Source { get; private set; }

            public Type DefaultResourceType { get; private set; }
        }
    }
}

Перейдем практической части и подключим провайдер конфигурации Модели в проект.

Создаем новое описание Модели

Id и UserLogin

Начнем с описания класса Модели PaymentModel. Прежде всего, удалим все элементы <metadata>, устанавливающие свойство IsRequired в значение true. Причину этого действия разберем чуть позднее. А пока рассмотрим добавляемые правила для каждого свойства:

<?xml version="1.0" encoding="utf-8" ?>
<model resource="MVCDemo.Resources.Models.PaymentRes">

  <property name="Id">
    <metadata name="ShowForDisplay" value="false" />
    <metadata name="ShowForEdit" value="false" />
  </property>

  <property name="UserLogin">
    <metadata name="DisplayName" resname="UserLogin" />
    <rule name="Require"  resname="FieldIsRequired" resource="MVCDemo.Resources.Shared.ErrorsRes" />
    <rule name="BlockHtml" resname="HtmlTagsNotAllowed" resource="MVCDemo.Resources.Shared.ErrorsRes" />
    <rule name="StringLength" max="64" resname="StringTooLong" resource="MVCDemo.Resources.Shared.ErrorsRes" />
    <rule name="Remote" controller="Payments" action="IsLoginNotExists"  method="POST"
          resname="InvalidUserLogin" resource="MVCDemo.Resources.Shared.ErrorsRes" />
    <rule name="CustomValidation" type="MVCDemo.Models.Validators.PaymentModelValidator" method="IsUserExists"
              resname="InvalidUserLogin" resource="MVCDemo.Resources.Shared.ErrorsRes" />    
  </property>

Свойство Id является системным не нужно для вывода на форму. Ему уже присвоены соответствующие метённые. Поэтому сразу перейдем к следующему свойству UserLogin. Оно получило сразу четыре правила. Рассмотрим их по очереди:

  • Require – указывает на то, что данное поле необходимо заполнить. Как можно понять из исходного кода, его сообщение будет использовать текст под имением FieldIsRequired из общего файла ресурсов ErrorsRes.resx.
  • BlockHtml – отключит в нем ввод элементов HTML.
  • StringLength – обеспечит ограничит длину строки до 64 символа. Минимальное значение контролировать нет необходимости, т.к. есть четверное правило...
  • Remote – организует удаленную проверку значения, которая требует обязательного совпадения введенного имени с уже существующим. Требует указать Контроллер и Действие, которые будут использоваться для обслуживания этой проверки.
  • CustomValidation – данное правило гарантирует, что имя пользователя существует в базе данных. Метод для него создадим чуть позже.

Для правила Remote в ресурсах веб-приложения зададим текст сообщения об ошибке:

Файл Resources\Shared\ErrorsRes.resx
InvalidUserLogin Invalid user login. Неизвестное имя пользователя

Для этого же правила реализуем также Действие IsLoginNotExists в Контролере PaymentsController:

namespace MVCDemo.Controllers
{
    using System;
    using System.Web.Mvc;
    using MVCDemo.Models;

    public class PaymentsController : Controller
    {

        .........

        // POST: /Payments/IsLoginNotExists/
        [HttpPost]
        public JsonResult IsLoginNotExists(string userLogin)
        {
            var userRepository = new UserRepository();

            return this.Json(userRepository.IsLoginExists(userLogin));
        }
    }
}

Обратите внимание, что имя параметра метода IsLoginNotExists() совпадает, без учета регистра букв, с именем свойства, от которого необходимо получить данные. Если это правило не выполнить, то полученная от формы информация не будет присвоена в данную переменную.

Осталось создать метод для аналогичной проверки на стороне сервера, которая указана в правиле CustomValidation. Для этого реализуем класс PaymentModelValidator, который создадим в папке Models\Validators:

namespace MVCDemo.Models.Validators
{
    using System.ComponentModel.DataAnnotations;
    using MVCDemo.Resources.Shared;

    public static class PaymentModelValidator
    {
        public static ValidationResult IsUserExists(
            string userLogin, ValidationContext context)
        {
            var userRepository = new UserRepository();

            return userRepository.IsLoginExists(userLogin) ?
                ValidationResult.Success :
                new ValidationResult(ErrorsRes.InvalidUserLogin);
        }
    }
}

Date

  <property name="Date">
    <metadata name="DisplayName" resname="Date" />
    <metadata name="DataTypeName" value="DateTime" />
    <rule name="Require"  resname="FieldIsRequired" resource="MVCDemo.Resources.Shared.ErrorsRes" />
    <rule name="RegExp" resname="IncorrectDateTimeFormat"          
          regexp="(\d{1,2})[-/.](\d{1,2})[-/.](?:\d{4}|\d{2})[ ](\d{1,2}):(\d{1,2}):{0,1}(\d{0,2})" />
  </property>

Будем контролировать обязательность ввода поля Date, а так же формат вводимого значения. Установим его в виде "DD.MM.YYYY hh:mm:ss" с помощью регулярного выражения. Кроме того, потребуется создать сообщение об ошибке:

Файл Resources\Models\PaymentRes.resx
IncorrectDateTimeFormat Date format should be DD.MM.YYYY hh:mm:ss ERROR: Неверный формат при указании даты и времени

Amount

  <property name="Amount">
    <metadata name="DisplayName" resname="Amount" />
    <metadata name="DataTypeName" value="Currency" />
    <rule name="Require"  resname="FieldIsRequired" resource="MVCDemo.Resources.Shared.ErrorsRes" />
    <rule name="Range" min="0.01" max="100000000.0" type="System.Double" resname="IncorrectAmountValue" />
  </property>

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

Свойство Amout является числом и по определению не может быть null. Для таких полей ввод их значения требуется автоматически. Может возникнуть вопрос зачем добавлено правило Require? Однако цель здесь очень простая – установка сообщения об ошибке для данного поля. Особенно это может быть необходимо, если сами сообщения локализованы.

Кроме того, будем контролировать с помощью правила Range соответствие вводимого значения заданному диапазону. При этом используется тот же тип System.Double, что и у свойства Amount. Поскольку это сумма в рублях, то минимум установим равным одной копейке. Нет смысла заводить платеж на ноль рублей роль копеек. Максимум приравняем к ста миллионам рублей.

Кроме того, добавим новое сообщение об ошибке для данного правила:

Файл Resources\Models\PaymentRes.resx
IncorrectAmountValue Incorrect amount value. ERROR: Отрицательное или очень большое значение суммы

Info

Для последнего свойства Info просто ограничим ввод HTML элементов:

  <property name="Info">
    <metadata name="DisplayName" resname="Info" />
    <metadata name="DataTypeName" value="MultilineText" />
    <rule name="BlockHtml" resname="HtmlTagsNotAllowed" resource="MVCDemo.Resources.Shared.ErrorsRes" />
  </property>

</model>

Подключение провайдера конфигурации Модели

После того, как описание Модели задано, все что необходимо, это добавить несколько строк в файл Global.asax.cs. Посмотрим в начале на исходный код, а затем разберем его в подробностях:

namespace MVCDemo
{
    using System.Web.Mvc;
    using System.Web.Routing;
    using MVCDemo.Models.Configuration;
    using MVCDemo.Models.Configuration.Rules;
    using MVCDemo.Models.Configuration.Sources;
    using MVCDemo.Models.MetadataProviders;

    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801

    public class MvcApplication : System.Web.HttpApplication
    {
        .........

        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            RegisterGlobalFilters(GlobalFilters.Filters);
            RegisterRoutes(RouteTable.Routes);

            var modelProvider = new ModelConfigurationProvider();

            modelProvider.Sources.Add(
                new XmlModelConfigurationSource(
                    Server.MapPath("~/App_Data/ModelMetadata/")));

            var manager = new ModelMetadataProvidersManager(ModelMetadataProviders.Current);
            manager.Providers.Add(modelProvider.MetadataProvider);
            ModelMetadataProviders.Current = manager;

            modelProvider.AddValidatiorRule("BlockHtml", new BlockHtmlRule());
            modelProvider.AddValidatiorRule("Equal", new EqualRule());
            modelProvider.AddValidatiorRule("StringLengthRange", new StringLengthRangeRule());

            ModelValidatorProviders.Providers.Insert(0, modelProvider.ValidatorProvider);
        }
    }
}

В первую очередь отметим подключение соответствующих пространств имен.

Затем перейдем к методу Application_Start(). В нем создадим экземпляр ModelConfigurationProvider и установим его как провайдера метаданных и правил проверки данных для текущей Модели. Кроме того, подключим реализации пользовательских трех правил: BlockHtml, Equal и StringLengthRange.

В завершении стоит обратить внимание на способ добавления провайдера правил проверки в коллекцию ModelValidatorProviders.Providers. Как правило, можно встретить примеры, когда используется метод Add(). Т.е. новый провайдер будет последним в их списке. В данном случае используется вставка экземпляра в начало списка.

Это необходимо для того, чтобы экземпляры ModelValidator были созданы реализованным провайдером  о того, как начнет свою работу DataAnnotationsModelValidatorProvider. В противном случае, даже без явного указания он автоматически создаст для некоторых свойств атрибут [Required] и соответствующее правило проверки. Если после этого добавить свою копию атрибута, то это приведет к ошибке на этапе выполнения. Ведь будут два правила с одинаковыми "уникальными" именами.

Отказаться от создания [Required] нет смысла, т.к. зачастую необходимо задать текст сообщения об ошибке на родном для пользователя языке.

Чтобы избежать этого, необходимо создать свою версию [Required] заранее. Поэтому воспользуемся вставкой своего провайдера правил проверки в начало коллекции.

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


Исходный код проекта (C#, Visual Studio 2010): MVCDemo-Part22.zip (516 Kb)

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

Сергей 01.04.2011 21:46:24

Можно запустить проект и убедиться в его работоспособности...А вот и нельзя...баг там

А можно чуть подробнее? Сейчас еще раз проверил проект, загрузив его с сайта. Запускается. Добавляет без проблем и пользователей и платежи. Данные через JS контролируются.

Александр 21.05.2011 16:38:40

Андрей, огромное спасибо за обучающий курс по MVC. Супер!
По данному разделу у меня есть вопрос:
зачем для свойства UserLogin добавлено 2 правила проверки совпадения введенного имени с уже существующим - Remote и CustomValidation?
Разве одного из них будет недостаточно?

Александр 21.05.2011 16:56:16

Хотя по поводу атрибута Remote у меня есть соображения, что он позволяет сделать проверку по AJAX (во время заполнения формы), а CustomValidation только после отправки формы. Или я ошибаюсь?

Александр 21.05.2011 18:29:49

Нашёл одну ошибочку:
в класс ModelValidatorProviderAdapter нужно добавить:

Rules.Add("CustomValidation", new CustomValidationRule());

Заодно вопрос с вводом дробных значений. Разделитель в системе запятая.
Ввожу 123,45 ошибка - "The field Сумма (RUR) must be a number."
ввод 123.45 (как и 123.450) после отправки формы дает ошибку "The value '123.45' is not valid for Сумма (RUR)."

ввод 123,450 дает ошибку "Отрицательное или очень большое значение суммы"

как же вводить нецелые числа?

@ Александр: Прошу прощения что не сразу ответил сразу. На выходных было не до компьютера.

Теперь по вопросам – Remote осуществляет проверку на стороне клиента. Т.е. еще до того, как будут отправлены данные. CustomValidation – на сервере (страховка от назойливых). Хотя вы и сами догадались.

По числам – у вас какой разделитель в системе стоит (запятая)?

Александр 24.05.2011 0:24:22

разделитель в системе - запятая
в .config файле описания модели тоже изменил на запятую
но ввести дробное значение не получается, только целое.

и если правило  <rule name="Range" min="0.01" max="100000000.0" type="System.Double" resname="IncorrectAmountValue" />
убрать вообще, то ввести дробное значение можно, но только 3 знаками после запятой. пробовал в разных браузерах

Не получается воспроизвести. Так что пока не могу дать ответ.

Александр 26.05.2011 23:11:21

эффект проявляется только тогда, когда в системе разделитель запятая. Если стоит "." (точка), то любые значения вводятся нормально.

Александр 26.05.2011 23:22:52

У меня еще такой вопрос к Вам. Я думаю Вы неспроста решили создавать объекты класса ModelMetadata нестандартным путём. Во-первых, хранение атрибутов модели и правил валидации где-то отдельно от самой сборки позволяет менять поведение модели без перекомпиляции этой сборки. Ну а во-вторых, этот способ наверняка быстрее стандартного, в котором используется отражение (тем более прочитанные из любого источника данные можно закэшировать). Отсюда вопрос: сравнивали ли Вы быстродействие обоих способов создания объектов ModelMetadata???

@ Александр: Я сейчас переделываю примеры под обновленный MVC3. Погляжу как дойду до этой части.

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

Pingbacks and trackbacks (2)+

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