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

    Прочитав обзор «Что нужно от форм?», мне захотелось рассказать, как в нашем фреймворке для быстрого создания LOB приложений eXpressApp Framework устроены «универсальные, динамически изменяемые формы».

    В первой части моего рассказа я продемонстрирую реализацию элементов динамики на примере популярных задач фильтрации значения, управления видимостью и доступностью, а также контроля данных полей на форме вот такого вот необычного бизнес объекта:
    [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 приложения.

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


    Если честно, то мне очень понравился термин ПУЗы (Поле-Условие-Значение), который активно использовался автором предыдущей статьи для определения элементов динамики или правил поведения формы. Одним из популярных подходов к объявлению ПУЗов является декларативный подход, подразумевающий украшение атрибутами бизнес объекта и его членов (наверняка хорошо знакомый вам по 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; }
    

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



    Не забываем, что всё это будет также прекрасно работать и в Вебе. Если нужно, вы также можете управлять этими фильтрами через метаданные приложения. Конечно же, можно реализовать и более сложные условия, в которых критерий задается не специальным объектно-ориентированным языком критериев, а программным кодом (например, в простейшем случае мы можем объявить приватное свойство, которое будет возвращать какой угодно список отфильтрованных объектов для 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 по умолчанию:



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



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

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

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



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

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

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

      [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" />
      

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

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

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



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

      Если мы слегка поменяем предыдущее правило, то сможем контролировать видимость вместо доступности:
      [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 или документацию на английском.

    Developer Soft

    74,00

    Компания

    Поделиться публикацией
    Комментарии 21
      +3
      eXpressApp это для быстрого создания прототипа, или можно результат и в продакшн пускать?
      В продакшене наверняка будут пара вредных требований заказчика, ради реализации которых придётся залезать внутрь вашего фреймворка. В принципе, если исходные коды фреймворка идут в комплекте, то решаемо.

        +5
        В первую очередь фреймворк предназначен для создания реальных (продакшин) бизнес приложений. Вот вам пару примеров продакшин приложений на XAF от российских компаний: раз, два.
        XAF в полном смысле этого слова является «фреймворком», т.е. полностью расширяемый под различные требования разработчика (в том числе и во время исполнения), так что вам возможно даже не придется лезть в его исходники. Как бы там ни было, полные исходные коды DevExpress компонент и самого фреймворка идут в поставке.
        +5
        И чем-то это мне напоминает 1С: Конфигуратор. Тоже «Создай бизнес-приложение за 5 минут».

        p.s. не спешите минусовать, да, отличия есть, но есть и сходство.
          +6
          Да везде есть такие фрейморвки и конфигураторы. В том же ПХП их полно. Тот же Битрикс на них построен. Только сколько бы я не сталкивался с ними, вечно утыкался в то, что сложность кастоматизации этого всего под конкретные нужды начинает отнимать слишком много времени, начинают появляться костыльные решения, городиться ненужные абстракции и т.д. В итоге начинаешт задавать себе вопрос, зачем вообще это все нужно, если я могу взять и написать под себя все это за пару дней
            +1
            Верно, ну тут просто последние веяния таковы, что сами по себе низкоуровневые фреймворки стали удобны для прикладных задач. Многие тривиальные задачи такие фреймворки, как XAF автоматизируют на ура, избавляя разработчиков от рутины, но для особых функциональных фич деваться некуда — придется лезть на уровень ниже. И тут возникает проблема — получается «мини-фреймворк» из костылей, параллельно с XAF, ибо в таких случаях абстракция в виде XAF уже скорее усложняет жизнь, нежели упрощает.
              +2
              Начал было писать большой развернутый ответ, а в итоге получилась «простынь» формата A4, которую вряд ли кто осилит прочесть — даже коллеги пожурили…

              Поэтому коротко отвечу так: XAF это не конфигуратор или генератор, а чистой воды фреймворк, который по сути дает тебе пустое приложение, для которого ты пишешь свои плагины/модули (например, свои бизнес объекты, контроллеры, команды и т.д.).
              Абсолютно все, что есть сейчас в XAF из коробки — это плагины (например плагин создания нового объекта, плагин удаления, плагин навигации, плагин системы безопасности или управления процессами). Не нравится стандартный плагин, настрой его через доступный API или напиши свой, где все делаешь как хочешь.
            +2
            XAF скорее ближе к фреймворку, чем к конфигуратору, несмотря на достаточно широкие возможности донастройки во время исполнения. Собственно и расчитан он был изначально больше на разработчиков, которые не просто умеют собирать что-то из конструктора, а используют TDD, BDD, наконец проверяют качество созданных приложений с помощью функциональных тестов, и просто хотят съэкономить время на таких рутинных операциях как реализация доступа к данным, ручное создание форм, реализации клиент-сервера, создание систем безопасности, контроля данных, отчетов, аналитики и др., т.е. те задачи, с которыми они столкнуться при создании любого нормального бизнес приложения уровня предприятия. И никто этим разработчикам не мешает настраивать существующие модули, создавать свои собственные модули и надстройки над фреймворком типа Galaktika Ranet XF и eXpand.

            Как бы там ни было раньше, в последние годы наметился устойчивый тренд к снижению порога вхождения, который принесет еще более продвинутые визуальные дизайнеры (например в добавок к исходным database-first и code-first будет еще и model-first (в 12.1) для облегчения создания бизнес сущностей), конструкторы (например конструктор «дашбордов») и др.
            +4
            Очень интересно!
            А какие есть инструменты контроля для проверки валидности выражений типа
            [Calculated(«Invoices[Status == 'Completed'].Sum(Amount)»)]
            что если я при наборе случайно укажу
            [Calculated(«Invoices[Status == 'Completed'].Sum(Anount)»)]
            как быстро обнаружится такой баг?

            Что вообще происходит с этим текстом? Копилируется? Преобразуется во чтото? Это код C#? Что там можно писать?
              +4
              В момент исполнения вы получите ошибку о неверном имени свойства.
              Этот текст разбирается в дерево выражений (CriteriaOperator). Грамматика выражения своя. Такие выражения используются во всех наших конролах, где нужна обработка данных, а также для запросов к БД.
                +3
                Поймать ошибку в этой строке (она не компилируется) вы должны в блочном тесте, который в идеале у вас должен быть создан еще до создания поля SaleAmount в коде. Например в демке, из которой я скопировал этот кусок, у нас написан вот такой тест:
                        [Test]
                        public void SaleAmount() {
                            IProduct product = ObjectSpace.CreateObject<IProduct>();
                            product.Price = 10;
                
                            ISaleItem saleItem1 = ObjectSpace.CreateObject<ISaleItem>();
                            saleItem1.Product = product;
                            saleItem1.Quantity = 10;
                
                            IInvoice invoice1 = ObjectSpace.CreateObject<IInvoice>();
                            invoice1.SaleItems.Add(saleItem1);
                            invoice1.Discount = 10;
                            invoice1.DiscountPercent = 15;
                
                            ICustomCustomer account = ObjectSpace.CreateObject<ICustomCustomer>();
                            account.Invoices.Add(invoice1);
                
                            Assert.AreEqual(0, account.SaleAmount);
                
                            invoice1.Status = InvoiceStatus.Completed;
                            Assert.AreEqual(76.5m, account.SaleAmount);
                        }
                

                Если вы не покрываете код блочными тестами по какой-то причине, то ошибка может быть отловлено за счет скрипта, сгенеренного встроенной системой функционального тестирования, поставляемой с фреймворком.
                Иначе, упадет во время исполнения при первом доступе к форме…

                Насчет самого объектно-ориентированного языка критериев (по ссылке есть синтаксис), это не C# и никакой другой язык. Его прототипом является OPath из ObjectSpaces. Он используется у нас во всех без исключения продуктах DevExpress для составления запросов выборки данных и вычисляемых выражений.
                  +3
                  Ок, спасибо за ответ.
                  Кстати, мы для контроля подобной ситуации используем PostSharp, который в момент компиляции может выполнять доп. проверки (В атрибутах указываем только название одного поля, готовлю статью на эту тему может на след неделе появится).
                +4
                А поддерживается только WinForms \ ASP.NET WebForms? Хотелось бы ещё WPF \ Silverlight
                  +2
                  Да, в данный момент поддерживается старые-добрые и неумирающие WinForms и ASP.NET Web Forms, которые постоянно совершенствуется. Также конечно не спим и смотрим в WinRT и мобильное будущее. Несмотря на то, что еще в 2009 году сделали прототип WPF UI за неделю (ядро фреймворка не трогалось, поменялась лишь рендережка UI), решили не инвестировать в него дальше, также по причине отсутвующих контролов на тот момент. Почему не планируем WPF/SL UI на ближайшее будущее описано тут.
                    +1
                    Эммм… WinForms совершенствуется? Ну разве что вот такими сторонними компаниями…
                      +2
                      Прошу прощения, я имел ввиду не саму платформу, а XAF WinForms UI, который совершенствуется за счет улучшений в самих компонентах и также нового функционала в XAF. Например в 12.1 XAF получит улучшенный Tabbed MDI интерфейс аля Visual Studio 2010 UI. А там глядишь какая-нибудь еще оболочка, оптимизированная под сенсорные экраны появится (будет полезно для приложений, используемых в киосках)…
                      +1
                      Именно поэтому убежал в Telerik. :(
                        +1
                        Да еще и туляк!
                        Если совсем серьезно, то прежде всего спасибо вам за отзыв.
                        Не могли бы пожалуйста отписать в личку или прямо сюда конкретные причины ухода (например отсутствие функционала Х, проблема Y и т.д.), помимо сильного желания использовать возможности WPF/SL, начальник требует? Еще раз спасибо за ваше время.
                    +3
                    Вопрос немного не по теме: у вас бывают полноценные тренинги/мастерклассы по вашим компонентам (особенно по «градообразоующим» типа XPO или XtraLayout)? А то иногда у меня возникает ощущение, что мы идем против течения. Помнится, когда я спросил в комментариях к одной из прошлых статей что-то про Layout, мне ответили в духе «вообще, это не тот способ, которым предполагается использовать компонент, но можно сделать вот так». Может быть в рамках DevCon организовать? Хотелось бы какое-то мероприятие не на час, а на день или лучше на пару.
                      +4
                      Сами мы пока не очень-то влезли в бизнес консалтинга, но есть много сторонних компаний делающих тренинги по нашим продуктам.
                      Например, наш бывший сотрудник Oliver Sturm профессионально начал заниматься DevExpress тренингами:
                      www.oliversturm.com/dxtraining.html
                      Оливер особенно силен в XAF и XPO, так как был продукт менеджером по этим продуктам одно время.

                      Советую вам стать членов группы DevExpress в LinkedIn, так как там постоянно такие темы проскакивают. Если что, пишите в личку, так как могу дать больше информации.

                      А так вообще наша техподдержка всегда рада (и главное бесплатно) направить вас в нужное русло, только обращайтесь, не стесняйтесь!
                        +2
                        Ну, Оливер — это конечно круто, но далековато :)
                        На линкедин запрос отправил, спасибо за наводку.
                        А ваша поддержка вообще вне всяких похвал и мы разумеется активно этим пользуемся! =) Но всё-таки это больше по частным вопросам, а хотелось бы чего-то комплексного.
                          +2
                          Оливер кстати из Шотландии, а не из Штатов (как вы могли подумать), так что не очень-то далеко от нас:-)
                          Ну и он уже давно облюбовал для своих классов Германию — вообще рукой подать:-)
                          Насчет своих курсов пока вот никаких конкретных планов, но может быть соберемся когда-нибудь!

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

                      Самое читаемое