понедельник, 15 декабря 2008 г.

DSL, валидация и jQuery

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

Введение

Итак, напомню, что мы реализовали на данный момент:

  1. Объектная модель DSL валидации;
  2. Удобный механизм конструирования синтаксического дерева;
  3. Интерпретатор, который транслирует синтаксическую структуру дерева в логику валидации.

Для демонстрационных целей рассмотрим два альтернативных подхода - один с использованием серверных валидаторов ASP.NET, а второй будет чисто клиентским. Таким образом, мы увидим, как разделение логики DSL на декларативную и интерпретационную части помогает нам расширять его функциональность.

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

Затем мы разработаем "компилятор", который и будет превращать код, написанный на нашем DSL в логику валидации, которая будет встраиваться в форму по продуманному заранее сценарию.

Исходный код валидатора и модификации в DSL

В качестве подопытного кролика на сей раз выберем класс Person:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Comments { get; set; }
}

Интересным тут является наличие поля Email, для которого должно срабатывать условие соответствия шаблону. Поэтому модифицируем структуру констрейнтов, приведенную в предыдущем примере:

Email Constraint

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

Таким образом, пусть код валидатора будет таким:

var v = DefineValidator.For<Person>()

   .WhereProperty(p => p.FirstName).SatisfiedAs(Should.NotBeNullOrEmpty)
        .WithReason("Введите имя").AsError()
   .WhereProperty(p => p.FirstName).SatisfiedAs(Should.NotBeLongerThan(10))
        .WithReason("Слишком длинное имя")

   .WhereProperty(p => p.LastName).SatisfiedAs(Should.NotBeNullOrEmpty)
        .WithReason("Введите фамилию").AsError()
   .WhereProperty(p => p.LastName).SatisfiedAs(Should.NotBeLongerThan(10))
        .WithReason("Слишком длинная фамилия")

   .WhereProperty(p => p.Email).SatisfiedAs(Should.MatchEmailTemplate)
        .WithReason("Введите правильный емейл");

Динамическое добавление серверных валидаторов

Для начала приведем код формы, которую мы будем расширять логикой валидации:

<form runat="server">
    Имя:<br />
    <cc:ValidableTextBox ID="FirstName" runat="server" Type="Product"></cc:ValidableTextBox><br />
    Фамилия:<br />
    <cc:ValidableTextBox ID="LastName" runat="server" Type="Product"></cc:ValidableTextBox><br />
    Емейл:<br />
    <cc:ValidableTextBox ID="Email" runat="server" Type="Product"></cc:ValidableTextBox><br />
    Комментарии:<br />
    <cc:ValidableTextBox ID="Comments" runat="server" Type="Product"></cc:ValidableTextBox><br />
    <br />
    <asp:Button runat="server" Text="Submit" />
</form>

В приведенном фрагменте вместо стандартного контрола TextBox мы использовали его расширенную версию ValidableTextBox, в котором появилось поле Type. Для чего это нужно? Дело в том, что мы должны связать контрол на форме с конкретным свойством конкретного класса. В простейшем случае имя свойства можно использовать в качестве идентификатора для контрола, а свойство Type будет использоваться для указания типа, к которому это свойство принадлежит.

В WebForms валидаторы - это обычные серверные контролы, поэтому добавление валидаторов на форму - это обычная манипуляция коллекцией контролов:

protected void Page_Init(object sender, EventArgs e)
{
    const string maxLengthPattern = @"[\s\S]{{0,{0}}}";
    const string emailPattern = @"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*";

    AddValidator<Person>(p => p.FirstName,
        new RequiredFieldValidator(),
        "Введите имя");

    AddValidator<Person>(p => p.FirstName,
        new RegularExpressionValidator { ValidationExpression = string.Format(maxLengthPattern, 10) },
        "Слишком длинное имя");

    AddValidator<Person>(p => p.LastName,
        new RequiredFieldValidator(),
        "Введите фамилию");

    AddValidator<Person>(p => p.LastName,
        new RegularExpressionValidator { ValidationExpression = string.Format(maxLengthPattern, 10) },
        "Слишком длинная фамилия");

    AddValidator<Person>(p => p.Email,
        new RegularExpressionValidator { ValidationExpression = emailPattern },
        "Введите правильный емейл");
}

void AddValidator<T>(Expression<Func<T, object>> property, BaseValidator validator, string message)
{
    var fildName = ((MemberExpression)property.Body).Member.Name;

    var validableControl = FindControl(fildName) as IValidableControl;

    if (validableControl != null)
    {
        validator.ErrorMessage = message;
        validator.Display = ValidatorDisplay.Dynamic;
        validator.ControlToValidate = ((Control) validableControl).ID;

        var container = ((Control)validableControl).Parent.Controls;
        container.AddAt(container.IndexOf((Control) validableControl) + 1, validator);
    }
}

В ходе экспериментов с динамическим добавлением контролов мы увидели, что констрейнт StringMaxLengthConstraint разумно реализовать на базе RegularExpressionConstraint, потому что ограничение по длине поля с использованием серверных валидаторов в WebForms реализуется с помощью RegularExpressionValidator.

Показательным является то, что перекроив логику работы StringMaxLengthConstraint мы никак не повлияли на синтаксис нашего DSL.

Динамическая генерация javascript-кода для валидации

Раз уж речь пошла о том, что второй подход у нас будет ориентирован на использование ASP.NET MVC, то логично будет развить эту мысль и сказать, что код клиентской валидации мы будем писать, используя jQuery. И раз уж мы решили использовать jQuery, то стоит взглянуть на плагин Validation. Главным плюсом этого плагина является то, что вся логика описывается в декларативном стиле и может быть локализована в одном месте.

Код нашей формы будет выглядеть так:

<form action="ValidateHtmlForm.aspx" method="post" id="PersonForm">
    Имя:<br />
    <input type="text" name="Person$FirstName" /><br />
    Фамилия:<br />
    <input type="text" name="Person$LastName" /><br />
    Емейл:<br />
    <input type="text" name="Person$Email" /><br />
    Комментарии:<br />
    <input type="text" name="Person$Comments" /><br />
    <br />
    <input type="submit" value="Submit" />
</form>

По аналогии с серверными контролами, нам нужно связать поле ввода со свойством нужного нам типа. Для этого можно просто дать правильные имена полям, составив их из имени типа и названия свойства.

И в этом случае код валидации будет таким:

$(document).ready ( 
    function() {
        $("#PersonForm").validate(
            {
                rules: {
                    Person$FirstName: {
                        required: true,
                        maxlength: 10
                    },
                    Person$LastName: {
                        required: true,
                        maxlength: 10
                    },
                    Person$Email: {
                        email: true
                    }
                },
                messages: {
                    Person$FirstName: {
                        required: "Введите имя",
                        maxlength: "Слишком длинное имя"
                    },
                    Person$LastName: {
                        required: "Введите фамилию",
                        maxlength: "Слишком длинная фамилия"
                    },
                    Person$Email: {
                        email: "Введите правильный емейл"
                    }
                }
            }
        )
    }
);

Как видим, в отличии от серверных контролов для проверки длины поля и его соответствия шаблону используются готовые спецификаторы maxLength и email. Далее мы увидим, как констрейнты в нашем DSL спроектированны таким образом, что они могут без труда интерпретироваться и как RegularExpressionValidator с конкретным паттерном, и как спецификаторы maxlength и email.

Автоматизация генерации кода

Стандартным решением для пододбных задач являтся использование паттерн Visitor. Именно реализацию этого паттерна все желающие могут найти в недрах исходников LINQ в виде абстрактного intenal-класса ExpressionVisitor. Итак, напомним, в нашем примере синтаксическая структура DSL описывается иерархией констрейнтов. Поэтому разработаем абстрактный Visitor с реализациями метода Visit для каждого из типов констрейнтов и добавим две реализации для каждого из типов валидации:

2_Visitors

"Кирпичиком" наших валидаторов является инетрфейс IRule. Модифицируем его так, чтобы он выполнял роль Element для паттерна Visitor:

3_ValidatorElement 

При этом реализация метода Accept для каждого из констрейнтов будет выглядеть почти одинаково:

public class StringNotNullOrEmptyConstraint : IConstraint
{
    ....
    public void Accept(IValidatorVisitor visitor)
    {
        visitor.Visit(this);
    }
}

Т.е. единственная польза от этого метода - полиморфная диспетчеризация вызовов.

Точкой входа для всей конструкции является метод VisitValidator. Этот метод получает на вход список валидаторов, которые необходимо "посетить", перебирая их, вызывает метод Accept. А сами констрейнты "возвращают" вызовы в Visitor через метод Visit, как было показано выше.

Реализация метода VisitValidator для WebFormValidatorVisitor:

public void VisitValidator(IEnumerable<IRulesGroup> rulesGroups)
{
    foreach (var validator in rulesGroups)
        foreach (var rule in validator.Rules)
            rule.Accept(this);
}

Обернув логику связывания в некие классы-хелперы, на примере JQueryValidatorVisitor код использования будет выглядеть так:

<head runat="server">
    
    <script type="text/javascript" src="jquery-1.2.6.js"></script>
    <script type="text/javascript" src="jquery.validate.js"></script>

    <%= ValidateForm("PersonForm") %>
    
</head>

Реализация метода ValidateForm для случая с использованием JQueryValidatorVisitor:

public string ValidateForm(string fortmId)
{
    var script = new StringBuilder();

    IValidatorVisitor visitor = new JQueryValidatorVisitor(script, fortmId);
    visitor.VisitValidator(CreateValidators());

    return string.Format("<script type=\"text/javascript\">\n{0}</script>", script);
}

Методы Visit в свою очередь обрабатывают каждый конкретный констрейнт, "накапливая" и возвращая результат инерпретации клиентскому коду. Например, вот так вот выглядит код одного из методов класса WebFormValidatorVisitor:

public void Visit(StringNotNullOrEmptyConstraint constraint)
{
    var validator = new RequiredFieldValidator
                        {
                            ErrorMessage = _currentRule.Message,
                            Display = ValidatorDisplay.Dynamic,
                            ControlToValidate = _controlToValidate.ID
                        };
    var container = _controlToValidate.Parent.Controls;
    container.AddAt(_controlIndex + 1, validator);
}

Заключение

В последних двух постах мы увидели лишь верхушку огромного айсберга, под названием DSL. Эта тема сейчас активно разрабатывается многими крупными игроками на рынке средств разработки. Главное направление этих изысканий - упрощение разработки и внедрения DSL в приложения. Например, Microsoft на последней PDC анонсировала интересный продукт, под названием Oslo. Хороший обзор написал Мартин Фаулер. Кроме этого, старина Мартин работает над книгой по DSL. Как обычно, это будет каталог паттернов, с которым уже можно познакомиться у него на сайте. А еще, совсем недавно JetBrains выпустили бета-релиз своего нового продукта Meta Programming System, который решает схожие задачи с Microsoft Oslo. И что-то мне подсказывает, впереди нас ждет еще много интересного.

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

Опубликовать

2 комментария:

  1. http://rsdn.ru/Forum/Message.aspx?mid=3213116&only=1

    Там уже есть ссылка на ваш пост. Вдруг у вас найдеться что еще сказать.

    ОтветитьУдалить
  2. Statji super! Avtor Molodez! nu ochenj interessno.
    no! ne izabretaete li Vi velosiped? zachem tak slogno? moget validation ne ochne horoshij primer dlja primenenija DSL? nu ne budu ja vremja tratitj chtobi delatj vse tak universalno. dumaju eto prosto lishnee. privedite pogalusta drugie primeri oblasti primenenija DSL. nikogda s etim ne rabotal i ne znaju kak i s chego nachatj i nugno li mne eto v moih projektah. Ogromnoe Spasibo za statji i za otveti! Vsego Vam nailuchego!

    ОтветитьУдалить