«Умные» формы eXpressApp Framework (XAF). Часть 1

в 8:13, , рубрики: .net, devexpress, framework, LOB, xaf, Блог компании DevExpress, Программирование, метки: , , , ,

Прочитав обзор «Что нужно от форм?», мне захотелось рассказать, как в нашем фреймворке для быстрого создания LOB приложений eXpressApp Framework устроены «универсальные, динамически изменяемые формы».
«Умные» формы eXpressApp Framework (XAF). Часть 1
В первой части моего рассказа я продемонстрирую реализацию элементов динамики на примере популярных задач фильтрации значения, управления видимостью и доступностью, а также контроля данных полей на форме вот такого вот необычного бизнес объекта:

[DomainComponent]
public interface ICustomer : IOrganization, IAccount { }

В начале было Слово… точнее бизнес объект!

Из коробки фреймворк предоставляет несколько основных видов форм или «Views», предназначение которых во многом понятно из названия:
• List View – служит для представления списка бизнес объектов;
• Detail View – служит для представления детальной информации об объекте;
• Dashboard View – служит для показа нескольких различных представлений, т.е. является контейнером, который в общем случае ничего не знает о бизнес объектах.

И причем здесь бизнес объект? Дело в том, что eXpressApp Framework умеет автоматически генерировать эти формы на базе определений бизнес объектов/сущностей. Технически, фреймворк сначала создает метаданные, представляющие скелет будущего приложения, а потом во время его исполнения использует эти метаданные для построения конечного пользовательского интерфейса, включая системы навигации и команд, CRUD представления объектов, а также многое другое, что сейчас принято называть модным словечком «UI Scaffolding».

Думаю достаточно теории, давайте продемонстрируем вышесказанное на конкретном примере. Сначала создадим бизнес сущность ICustomer (для простоты вместо классов я буду использовать интерфейсы или Domain Components, как мы их называем):

[DomainComponent]
public interface ICustomer : IOrganization, IAccount { }

, которая будет у нас состоять из нескольких компонент:

[DomainComponent]
public interface IAccount {
    string Email { get; set; }
    string Password { get; set; }
}
[DomainComponent]
public interface IPerson {
    string LastName { get; set; }
    string FirstName { get; set; }
    DateTime Birthday { get; set; }
}
[DomainComponent]
public interface IOrganization {
    string Name { get; set; }
    IList<IPerson> Staff { get; }
    IPerson Manager { get; set; }
}

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

«Умные» формы eXpressApp Framework (XAF). Часть 1

«Умные» формы eXpressApp Framework (XAF). Часть 1

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

Так, в нашем случае имея одну лишь бизнес сущность ICustomer, я за несколько минут получил многофункциональные Windows и Web приложения.

Запускаем сердце «умной» формы

Если честно, то мне очень понравился термин ПУЗы (Поле-Условие-Значение), который активно использовался автором предыдущей статьи для определения элементов динамики или правил поведения формы. Одним из популярных подходов к объявлению ПУЗов является декларативный подход, подразумевающий украшение атрибутами бизнес объекта и его членов (наверняка хорошо знакомый вам по Data Annotations). В этой главе я расскажу, как в нашем фреймворке, используя декларативный подход, реализовать несколько популярных бизнес-правил.

Фильтрация значений полей в зависимости от бизнес правила

Одним из способов фильтрации значений полей является использование встроенных атрибутов: DataSourceProperty и DataSourceCriteria (узнать о них больше из документации). Например, нам нужно, чтобы свойство Manager нашего IOrganization показывало только записи из коллекции Staff, а не все записи типа IPerson. Сделать это очень просто:

[DataSourceProperty("Staff")]
IPerson Manager { get; set; }

Как вы могли догадаться, главным параметром этого атрибута является имя свойства, содержащего список объектов нужного типа. Атрибут достаточно умен, чтобы понимать вложенные свойства. Так, например, если бы у нас было свойство Department с коллекцией Staff, мы могли бы написать вот так:

[DataSourceProperty("Department.Staff")]

Если мы хотим дополнительно уточнить фильтр, добавим DataSourceCriteriaAttribute c необходимым критерием:

[DataSourceProperty("Staff"),DataSourceCriteria("StartsWith(FirstName, '123')")]
IPerson Manager { get; set; }

В итоге мы получим вот такой вот ожидаемый результат:

«Умные» формы eXpressApp Framework (XAF). Часть 1

Не забываем, что всё это будет также прекрасно работать и в Вебе. Если нужно, вы также можете управлять этими фильтрами через метаданные приложения. Конечно же, можно реализовать и более сложные условия, в которых критерий задается не специальным объектно-ориентированным языком критериев, а программным кодом (например, в простейшем случае мы можем объявить приватное свойство, которое будет возвращать какой угодно список отфильтрованных объектов для DataSourcePropertyAttribute). Больше примеров по реализации этого сценария в нашем фреймворке можно найти в документации.

Контроль значений полей в зависимости от бизнес-правила

Давайте сделаем наше поле Manager обязательным, если коллекция Staff не пуста. Для этого воспользуемся встроенным атрибутом RuleRequiredField и выставим необходимое условие в его параметр TargetCriteria:

[RuleRequiredField(TargetCriteria = "Staff.Count > 0")]
IPerson Manager { get; set; }

Всё это довольно просто, но что если нам нужно реализовать что-то посложнее обязательного поля? На это у фреймворка тоже есть ответ, так как «из коробки» он предосталяет пару десятков популярных правил контроля данных, пригодных почти на все случаи жизни.

Проверим это! Например, мы хотим убедиться, что поле Email нашего IAccount будет уникальным и валидным адресом электронной почты. Для этого достаточно добавить еще парочку готовых атрибутов:

[RuleRequiredField, RuleUniqueValue]
[RuleRegularExpression(@"^[_a-z0-9-]+(.[_a-z0-9-]+)*@[a-z0-9-]+(.[a-z0-9-]+)*(.[a-z]{2,4})$")]
string Email { get; set; }

Визуализация ошибок выполнена средствами встроенного модуля Validation, который используется в новом решении eXpressApp Framework по умолчанию:

«Умные» формы eXpressApp Framework (XAF). Часть 1

Стоит ли напоминать, что также присутствует легкая возможность создать более сложные правила контроля данных в коде, или же настроить существующие правила/объявить новые в метаданных приложения:

«Умные» формы eXpressApp Framework (XAF). Часть 1

Узнать больше о возможностях контроля данных форм можно из документации.

Изменения внешнего вида, видимости и доступности полей, а также других элементов управления в зависимости от бизнесс-правила

Эта функциональность предоставляется встроенным модулем Conditional Appearance, который легко добавить в наш проект простым перетаскиванием из Visual Studio Toolbox, находясь в дизайнере модуля (показан на рисунке ниже) или дизайнере всего XAF приложения:

«Умные» формы eXpressApp Framework (XAF). Часть 1

В результате, в проект будет добавлена ссылка на сборку модуля. После этого вы получите возможность использовать встроенный AppearanceAttribute для настройки стиля, доступности, видимости редакторов и заголовков полей, а также других элементов управления, типа команд меню «Сохранить», «Обновить» и пр.

Итак, начнем:

  1. Изменение стиля

    [Appearance("MarkUnsafePasswordInRed", "Len(Password) < 6", FontColor = "Red")]
    string Password { get; set; }
    

    Это правило подстветит поле Password красным, если в нем будет меньше шести символов:

    «Умные» формы eXpressApp Framework (XAF). Часть 1

    В реальном приложении, я думаю, такой подсветкой мало кого испугаешь, и наверное лучше опять же применить правило контроля данных вот с такой вот регуляркой: "^(?=.*[a-zA-Z])(?=.*d).{6,}$".

    Вы также можете менять цвет фона и настройки шрифта с помощью одноименных параметров атрибута.

    Отдельно хотел отвлечься «потрындеть» на подсветку полей, так как по опыту она часто оказывается полезной не только для индикации состояния объекта (нормально/так себе/плохо), но и для подсказки правильной последовательности шагов на форме (“workflow”).

    Так, например, в нашей внутренней системе отслеживания ошибок (кстати, написанной на XAF еще году в 2006) у меня настроено следующее правило, которое подсвечивает зелененьким обязательное поле Duplicate ID, тем самым визуально указывая мне верный путь после выставления статуса Duplicate у отчета об ошибке:

    <AppearanceRule Id="HighlightDuplicateWhenNonDraft" Criteria="Status.Name == 'Duplicate' AND !Draft"
    BackColor="192, 255, 192" Context="DetailView" 
    TargetItems="OriginalIssue" Index="9" IsNewNode="True" />
    

  2. Изменение доступности

    [Appearance("ChangeManagerAvailabilityAgainstStaff", "Staff.Count = 0", 
    Enabled = false)]
    IPerson Manager { get; set; }
    

    Это правило сделает поле Manager недоступным, если коллекция Staff будет пуста:

    «Умные» формы eXpressApp Framework (XAF). Часть 1

  3. Изменения видимости

    Если мы слегка поменяем предыдущее правило, то сможем контролировать видимость вместо доступности:

    [Appearance("ChangeManagerAvailabilityAgainstStaff", "Staff.Count = 0", 
    Visibility = ViewItemVisibility.Hide)]
    

    «Умные» формы eXpressApp Framework (XAF). Часть 1

    Перечисление ViewItemVisibility содержит следующие значения: Hide, Show, ShowEmptySpace. Hide не оставляет никакой «дырки» после себя, в то время как ShowEmptySpace буквально оставит «дырку» на форме (у наших пользователей были сценарии, когда это не так уж и плохо, ну или просто не все пользователи любят, когда на форме у них что-то двигается и перестраивается). Важно отметить, что если в контейнере полей на форме в результате скрытия ничего не окажется, то он и сам рекурсивно скроется.

Все эти правила будут также работать и в List View, неважно находится ли оно в режиме просмотра или редактирования (см. картинку из нашего демо приложения):

«Умные» формы eXpressApp Framework (XAF). Часть 1

Опять же, вы можете задавать такие правила не только через атрибуты в коде, но и через метаданные приложения:

«Умные» формы eXpressApp Framework (XAF). Часть 1

Изменение значения полей в зависимости от бизнес правила

Хотел привести пару примеров, как сделать поля на форме вычисляемыми. Для этого в Domain Components используется CalculatedAttribute, который принимает выражение для вычисления на нашем языке критериев. При этом, если поля из выражения хранятся в базе данных, выражение будет посчитано на сервере, а не на клиенте. Вот парочка примеров таких вычисляемых полей:

[Calculated("Concat(FirstName, ' ', LastName)")]
string FullName { get; }
[Calculated("Invoices[Status == 'Completed'].Sum(Amount)")]
decimal SaleAmount { get; }

Что-то более сложное уже можно запрограммировать без использования атрибутов.

Добавим огня...

По умолчанию большинство ПУЗов пересчитывается не моментально, а при уходе фокуса с редактора какого-либо изменившегося поля, т.е. когда значение из редактора поля попадает непосредственно в объект. Пересчет также часто может быть вызван какими-то внешним факторами или событиями, например сохранением, обновлением, сменой текущего объекта на форме и др. Такое поведение приемлемо во многих случаях, так как позволяет избежать лишнего «шума» на форме, не говоря уже о запросах на сервер. Тем не менее, есть ряд сценариев, где просто необходимо, чтобы наши ПУЗы были «порасторопнее». Для этого наш фреймворк предоставляет специальный атрибут — ImmediatePostDataAttribute, который, будучи примененным к полю бизнес сущности, вызывает событие изменения значения моментально, а не дожидаясь ухода фокуса с редактора этого поля.

К сожалению, бывали случаи, когда этот атрибут приносил и сомнительную пользу. Не могу не вспомнить индивидуумов, которые имели порядка 80-ти правил настройки внешнего вида полей на одной форме в купе с ImmediatePostDataAttribute (не знаю, правда, от большой ли это любви к атрибутам, то ли от странности исходных бизнес требований). Нам пришлось немного попотеть, чтобы производительность оставалась «на уровне», и форма не «умирала» от таких инсинуаций.

Дело в том, что при изменении значения одного из полей, сначала необходимо пересчитать всю эту сотню правил («брутфорс»-подход), а потом в лучшем случае поменять цвета и доступность полей, а в худшем многократно перестраивать структуру формы. Не сильно вдаваясь в детали нашей реализации, скажу, что нам удалось достигнуть поставленной цели в основном за счет «ленивого» создания редакторов полей, грамотного кэширования и смены состояния элемента формы, только если его текущее состояние отличается от нового, вычисленного по правилу. Наверное, в теории, можно было бы еще улучшить производительность путем исключения некоторых правил с помощью хитрой эвристики, которая бы разбирала критерий ПУЗа и применяла бы его, только если он реально зависит от измененного поля, брр…

Продолжение следует...

В следующих частях я надеюсь рассказать поподробнее о предназначении и возможностях метаданных приложения, определяющих как наши формы в конце концов будут выглядеть и вести себя. Также я думаю, что сообществу будет интересно побольше узнать про технологию Domain Components (все привыкли к классам, а тут какие-то интерфейсы вдруг появились непонятные), которую я использовал при создании бизнес сущностей. Если вкратце, эта технология была придумана нами для более гибкого и удобного создания повторно используемых библиотек бизнес сущностей. Пока, кому интересно, можно посмотреть статью на Code Project или документацию на английском.

Автор: DenisGaravsky


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js