- PVSM.RU - https://www.pvsm.ru -
Прочитав обзор «Что нужно от форм?» [1], мне захотелось рассказать, как в нашем фреймворке для быстрого создания LOB приложений eXpressApp Framework [2] устроены «универсальные, динамически изменяемые формы».
В первой части моего рассказа я продемонстрирую реализацию элементов динамики на примере популярных задач фильтрации значения, управления видимостью и доступностью, а также контроля данных полей на форме вот такого вот необычного бизнес объекта:
[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; }
}
Чтобы оценить, насколько фреймворк облегчает жизнь разработчика, приведу результат, полученный после первого запуска приложения, которое содержит только бизнес сущности из примера выше:
Хочу отдельно отметить, что во многом благодаря наличию слоя метаданных, наш фреймворк умеет автоматически строить пользовательский интерфейс для нескольких платформ, используя одну и ту же базу кода (в общем случае это бизнес сущности, контроллеры и команды, редакторы полей и представлений).
Так, в нашем случае имея одну лишь бизнес сущность 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; }
В итоге мы получим вот такой вот ожидаемый результат:
Не забываем, что всё это будет также прекрасно работать и в Вебе. Если нужно, вы также можете управлять этими фильтрами через метаданные приложения. Конечно же, можно реализовать и более сложные условия, в которых критерий задается не специальным объектно-ориентированным языком критериев [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 по умолчанию:
Стоит ли напоминать, что также присутствует легкая возможность создать более сложные правила контроля данных в коде, или же настроить существующие правила/объявить новые в метаданных приложения:
Узнать больше о возможностях контроля данных форм можно из документации [8].
Эта функциональность предоставляется встроенным модулем Conditional Appearance [9], который легко добавить в наш проект простым перетаскиванием из Visual Studio Toolbox, находясь в дизайнере модуля (показан на рисунке ниже) или дизайнере всего XAF приложения:
В результате, в проект будет добавлена ссылка на сборку модуля. После этого вы получите возможность использовать встроенный AppearanceAttribute для настройки стиля, доступности, видимости редакторов и заголовков полей, а также других элементов управления, типа команд меню «Сохранить», «Обновить» и пр.
Итак, начнем:
[Appearance("MarkUnsafePasswordInRed", "Len(Password) < 6", FontColor = "Red")]
string Password { get; set; }
Это правило подстветит поле Password красным, если в нем будет меньше шести символов:
В реальном приложении, я думаю, такой подсветкой мало кого испугаешь, и наверное лучше опять же применить правило контроля данных вот с такой вот регуляркой: "^(?=.*[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" />
[Appearance("ChangeManagerAvailabilityAgainstStaff", "Staff.Count = 0",
Enabled = false)]
IPerson Manager { get; set; }
Это правило сделает поле Manager недоступным, если коллекция Staff будет пуста:
Если мы слегка поменяем предыдущее правило, то сможем контролировать видимость вместо доступности:
[Appearance("ChangeManagerAvailabilityAgainstStaff", "Staff.Count = 0",
Visibility = ViewItemVisibility.Hide)]
Перечисление ViewItemVisibility содержит следующие значения: Hide, Show, ShowEmptySpace. Hide не оставляет никакой «дырки» после себя, в то время как ShowEmptySpace буквально оставит «дырку» на форме (у наших пользователей были сценарии, когда это не так уж и плохо, ну или просто не все пользователи любят, когда на форме у них что-то двигается и перестраивается). Важно отметить, что если в контейнере полей на форме в результате скрытия ничего не окажется, то он и сам рекурсивно скроется.
Все эти правила будут также работать и в List View, неважно находится ли оно в режиме просмотра или редактирования (см. картинку из нашего демо приложения):
Опять же, вы можете задавать такие правила не только через атрибуты в коде, но и через метаданные приложения:
Хотел привести пару примеров, как сделать поля на форме вычисляемыми. Для этого в 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
Нажмите здесь для печати.