- PVSM.RU - https://www.pvsm.ru -

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

Прочитав обзор «Что нужно от форм?» [1], мне захотелось рассказать, как в нашем фреймворке для быстрого создания LOB приложений eXpressApp Framework [2] устроены «универсальные, динамически изменяемые формы».
«Умные» формы 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 приложения.

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

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

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

Одним из способов фильтрации значений полей является использование встроенных атрибутов: DataSourceProperty и DataSourceCriteria (узнать о них больше из документации [4]). Например, нам нужно, чтобы свойство 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

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

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

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

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

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

Проверим это! Например, мы хотим убедиться, что поле 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

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

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

Эта функциональность предоставляется встроенным модулем Conditional Appearance [9], который легко добавить в наш проект простым перетаскиванием из 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 [10] или документацию на английском [11].

Автор: DenisGaravsky


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/4040

Ссылки в тексте:

[1] «Что нужно от форм?»: http://habrahabr.ru/blogs/erp_systems/139328/

[2] eXpressApp Framework: http://habrahabr.ru/company/devexpress/blog/140325/

[3] Data Annotations: http://msdn.microsoft.com/en-us/library/dd901590(v=vs.95).aspx

[4] узнать о них больше из документации: http://documentation.devexpress.com/#Xaf/CustomDocument2993

[5] объектно-ориентированным языком критериев: http://documentation.devexpress.com/#XPO/CustomDocument4928

[6] найти в документации: http://documentation.devexpress.com/#Xaf/CustomDocument2681

[7] пару десятков популярных правил: http://documentation.devexpress.com/#Xaf/DevExpressPersistentValidation

[8] из документации: http://documentation.devexpress.com/#Xaf/CustomDocument3009

[9] Conditional Appearance: http://documentation.devexpress.com/#Xaf/CustomDocument3286

[10] статью на Code Project: http://www.codeproject.com/Articles/231457/Using-Domain-Components-DC-in-XAF-DevExpress-Part

[11] документацию на английском: http://documentation.devexpress.com/#Xaf/CustomDocument3261