четверг, 27 марта 2008 г.

Safe event pattern

Рылся в кодах Prism и натолкнулся на вот такой код:

public event EventHandler Updated = delegate { };

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

Updated(null, new AccountPositionEventArgs());

вместо:

EventHandler handler = Updated;
if (handler != null)
handler(null, new AccountPositionEventArgs());

так что на заметку.


Удачи!

среда, 26 марта 2008 г.

English mirror

here: http://robbbloggg.blogspot.com

Ну и как обычно удачи! =)

понедельник, 24 марта 2008 г.

Make your objects "UI'ish" easily

Ранее я писал о приемах, позволяющих WPF binding engine взаимодействовать с объектами из унаследованного, стороннего или сгенерированного тулзами кода (назову эти объекты "сырыми") при помощи обертки ObservableObjectWrapper. Также в моей реализации ConfigDialog я использовал интерфейс IEditableObject для редактируемых объектов.

Paul Stovell нашел универсальное решение проблемы взаимодействия UI layer с объектами, оно основано на использовании custom TypeDescriptor'ов, позволяющих подсунуть UI fake-свойства, fake-конвертеры для реализации интерфейсов, отсутствующих в "сырых" объектах.
Данное решение работает в любом UI framework'е, не только в WPF.

Отсылаю вас к оригиналу - http://www.paulstovell.net/blog/index.php/runtime-ui-binding-behavior-ieditableobject-adapter/

must read.

Удачи.

среда, 12 марта 2008 г.

CustomFields "framework", datamodel decorator

Идеологически мой проект является не просто приложением, но расширяемой и настраиваемой платформой. Это громкое звание достигается за счет нескольких функциональных возможностей, заложенных в систему, одной из них является возможность кастомизации (customizing - настройка, расширение функциональности) сущностей приложения. Иными словами говоря, администратор системы в приложении-конфигураторе выбирает сущности, которыми оперирует система и добавляет или удаляет поля, управляет их отображением в UI. Эту функциональность реализует customfields "framework" (опять я использовал это громкое слово).

Функциональность, которую необходимо достичь:
  • добавление и удаление полей сущностей в run-time;
  • кастомизация представлений в различных приложениях и wizard'ах системы (расположение и отображение заложенных в сущность и только что созданных полей);
Поскольку мы были сильно ограничены по срокам, то для реализации требуемой функциональности мы выбрали самое простое решение, которое состоит из:
  • хеш-таблица в базе данных, которая хранит пару имя поля и значение, проиндексированные по уникальному ключу сущности;
  • таблица с настройками, в которой в виде CLOB хранится xaml представлений и список custom полей;
  • административный сервис, который поставляет/сохраняет список custom полей
  • сервис, который поставляет собственно список custom полей по уникальному идентификатору сущности, поставляет/сохраняет UI представления (lose-xaml) в БД.
  • DataModel и CustomFieldsProvider - шлюзы данных для UI.

Как вы поняли, реализация предельно простая. Custom представления хранятся в виде xaml (форматированный xml). Для отображения этого xaml во всех представлениях используется написанный мною примитивный контрол XamlViewer, о которм я уже упоминал при описании локализации форматированного текста.

Итак, самое интересное о чем я вам хочу рассказать - реализация шлюза данных для сущности с custom полями.

Решений данной задачи несколько и, поскольку Blend 1 не поддерживает формат решений VS2008, то для совместной работы с дизайнером, мы остаемся в VS2005 и пишем, используя .Net 3.0, соответственно и решения будут C# 2 специфичные. Это я к тому, что в C# 3 появилась замечательная функциональность (я бы даже сказал синтаксический сахар) - методы расширения. С ними решение было бы просто тривиальным. Но поскольку мы ограничены исполняемой средой, то и будем исходить из этого. Итак, решения:

  • Для каждой сущности написать отдельный класс DataModel, поставляющий только кастомные поля;
  • Или же сделать получение кастомных полей прозрачным для клиента, использующего DataModel - поставщик сущности.

Второй вариант звучит неплохо, но как его реализовать? Обратимся к классикам - в книге GoF описан структурный паттерн Decorator, описание которого дословно звучит: "Динамически возлагает на объект новые функции. Декораторы применяются для расширения имеющейся функциональности и являются гибкой альтернативой порождению подклассов". Бинго! То что нужно. Ведь данное решение позволит добавить к имеющемуся объекту custom поля и мы сможем динамически, основываясь на различных данных (к примеру права доступа), подменять объект его декоратором.


Приступим. Прежде всего опрделимся кого же мы будем декорировать. Если вы помните в посте про DM-V-VM я описал generic-класс SEModel (Single Entity Model). Вот его мы и будем декорировать.


Прежде всего определим интерфейс для CustomFieldsModelDecorator (CFModelDecorator).




На диаграмме вы видите ICFModel - интерфейс для модели, поставляющей custom fields. Custom поля хранятся в специальной коллекции ObservableDictionary - словаре, реализующем databinding-специфичные интерфейсы INotifyPropertyChanged, INotifyCollectionChanged. Готовой реализации в BCL нет, поэтому я написал свою примитивную обертку над Dictionary.

Теперь реализация декоратора:


CFDataModel - generic класс как и SEModel и он также реализует интерфейс ISEModel. Он параметризуется интерфейсом оборачиваемой модели, теми же типами сущности и интерфейсом провайдера, что и оборачиваемая модель, + интерфейсом провайдера custom полей.



Провайдер custom полей предельно прост, вот его интерфейс:



В конструктор декоратора IoC-контейнером инжектируются SEModel и CFProvider. Декоратор для обернутой модели устанавливает режим синхронного обновления и подписывается на событие изменения свойств.


При изменении свойств обернутой модели надо следить за изменением Id.



Основная работа выполняется в методе DoUpdate. Декоратор сначала обновляет custom поля для индексированной по Id сущности, а затем делегирует обновление обернутой DataModel.



Вот собственно и все осталось лишь продемонстрировать применение нашего декоратора.

помещаем его в IoC-контейнер как ISEModel:



и теперь клиент, использующий ISEModel получит наш декоратор совершенно прозрачно, т.е. клиент даже не обязан знать, что он использует ICFModel. Например ViewModel, отображающий сущность Visitor получит декоратор:



и будет работать с ним так, как будто это обычный класс SEDataModel.

Единственное место, где к декоратору обращаются явно - custom xaml, который хранится в БД и создается в конфигураторе. В данном примере его будет поставлять ICustomContentModel. Xaml как я уже говорил отображается XamlViewer:



, а сам xaml может выглядеть вот так:



Все метаданные (список custom полей, xaml с данными - имя поля к которому надо привязываться (binding), порядок расположения полей, их имена в UI, отображаени/скрытие выбранных полей) конфигурируются в специальном приложении - Configurator.

Удачи.

четверг, 6 марта 2008 г.

WPF ConfigurationDialog

Сообщу вам новость - во всех приложениях есть настройки! И их надо (вы не поверите) настраивать! Так вот об этом и пойдет сегодня речь - создание конфигурационного диалога.

Во-первых определимся с дизайном:

  1. это должно быть окно;
  2. оно должно быть красивое (не ко мне в другой блог пожалуйста);
  3. должен быть какой-то унифицированный механизм рендеринга настроек в диалоге.
1. Window

За основу я взял класс Lightbox.CustomDialog, т.к. мне очень понравилась анимация - диалог может анимировать 4 процесса:

  • свое появление
  • уход в тень главного окна
  • свое исчезновение
  • переход на передний план главного окна
В общем мне анимация понравилась, а видеокарточке на моем старом компе нет, поэтому я немного модифицировал класс, введя в него свойство IsAnimationEnabled, которое по умолчанию = false.

В дефолтном стиле данного диалога задается Layout - верхняя зона для контента, а в нижней части 2 кнопки Ок и Отмена.



Обратите внимание на свойство SharedSizeGroup у ColumnDefinitions и IsSharedSizeScope у Grid - удивительная прозорливость разработчиков WPF - они предусмотрели такую возможность, как разделяемый размер столбцов или строк в Grid'е. Задавая им одинаковый идентификатор вы объединяете их в одну группу с разделяемым размером (одним общим размером, максимальным из всех необходимых). Таким образом, если не задавать явно нашим столбцам размеры, их ширина будет определяться лишь максимальной шириной кнопки, которая в свою очередь определяется контентом (длиной строки в нашем случае). В итоге мы имеем 2 кнопки одинаковой ширины, которые синхронно изменят свой размер, при смене языка, т.к. на другом языке строки могут быть длиннее или короче. Удивительная прозорливость, и удивительное отсутствие нормальных средств для локализации.

Далее, кнопка "Отмена" - IsCancel = true - принажатии на нее DialogResult автоматически = false

В триггере ControlTemplate следим за свойством Validation.HasError, - используя технику продемонстрированную в предыдущем посте, можно пометить диалог как невалидный и тогда кнопка "Ок" станет неактивной.

2. Beautiful window.

Ладно, с базовым диалогом разобрались, теперь можно рисовать само окно. Layout его будет примерно следующий:


Цифрами на картинке я обозначил зоны диалога. 1,2,3 - зона контента, 4 - зона кнопок. Они определены в дефолтном стиле DialogBox. Наш класс ConfigDialog будет наследоваться от DialogBox, поэтому зоны 1,2,3 - это Content окна.

  1. Список настраиваемых элементов. Каждый элемент списка должен иметь картинку и короткий заголовок.
  2. Зона настроек все эти бла-бла-бла - настройки.
  3. Зона длинного заголовка текущего настраиваемого элемента.
Откуда берутся все эти данные? Они берутся из настраиваемых элементов, конечно.

3. Tunes.

Кому же поручить столь отвественную роль - быть настраиваемым элементом? Обратимся к предыдущим постам и вспомним паттерн DM-V-VM. Здесь VM является абстракцией View, вынося всю, неотносящуюся к rendering'у логику и состояние (state). Так если VM хранит state, тогда это самое логичное место, в которое можно поместить настройки!

Чтобы унифицировать общение диалога с настраиваемыми элементами я ввел интерфейс ITuned.


Который, реализуется классом TunedViewModel.



Теперь мы знаем, что откуда берется и можно писать сам ConfigDialog. В конструкторе ему мы будем передавать список настраиваемых элементов, которые он будет render'ить.



Для списка создается CollectionView через xaml-proxy CollectionViewSource, к которому привязан список. Здесь мы имеем классический пример master-detail binding, все элементы привязываются в binding к CollectionView, который синхронизирован с ListView, таким образом все элементы отображают текущий выбранный элемент в списке. По поводу CollectionView - это можно сказать встроенная в библиортеку реализация ViewModel для отображения коллекций в UI.

Далее, откуда берется контент настроек? Что мы собственно настраиваем?

Мы собственно настраиваем настройки - интерфейс ITune:


Этот интерфейс реализуют 2 класса. Первый Tune - простая реализация поддерживающая сохранение значений в очереди и отмену, либо подтверждение изменений через методы базов интерфейсов. Вторая же - блестящая реализация транзакционного менеджера ресурсов в памяти от Juwal Lowy, описанная в его статье в MSDN Volatile Resource Managers in .NET to bring transactions to the Common type. Данное решение позволяет максимально просто использовать преимущества транзакций при работе с данными в оперативной памяти.



Так вот эти самые настройки и предлагается редактировать в xaml. UI для редактирования настроек (Content ConfigDialog'а зона 2) передается в виде стиля WPF. Хранение его в виде стиля, а не например UserControl'а позволяет добиться следующих преимуществ:
  • хранится в View в ResourceDictionary и будет создан единожды при загрузке View как синглтон.
  • всю обработку событий этого UI code-behind View должен делегировать тому же VM классу, т.е. вся логика в VM.
  • в стиле можно использовать MarkupExtensions как источники данных для StaticResource.
Для нашего примера стиль может выглядеть вот так:


Если ваше представление имеет какие-либо настройки, оберните их в один из классов Tune или TransactionalTune и при загрузке View положите их в его ресурсы (StaticResources bla1, bla2), т.к. нет другого способа привязать их к UI (стиль в ресурсах).

ConfigDialog в обработчике события Loaded начинает транзакцию и вызывает у всех настроечных элементов метод BeginEdit, оповещая их таким образом, что редактирование началось:


В обработчике события Unloaded обрабатывется DialogResult и, в случае если он положителен (кнопка OK) транзакция подтверждается и у всех настраиваемых элементов, которые были изменены вызывается метод EndEdit, оповещаяя их об успешном завершении редактирования. Если же DialogResult != true, то в этом случае транзакция откатывается и у всех настраиваемых элементов, которые были изменены вызывается метод CancelEdit, оповещаяя их об отмене редактирования.

Summary

Как всем этим пользоваться? Очень просто. Унаследуйте свой VM от TunedViewModel, переопределите свойства, реализующие интерфейсы, напишите стиль для рендеринга настроек, оберните настройки в Tune или TransactionalTune, положите их в ресурсы View (можно переопределить метод ViewModel.OnViewLoaded), добавьте ваш ViewModel в список настраиваемых элементов и отдайте его ConfigDialog. И все. И еще, поскольку и Tune и TransactionalTune наследуются от ObservableObject, вы можете подписаться на событие PropertyChanged и реагирвать на него прямо в процессе редактирования.

И если вы попросите хорошего дизайнера поработать над вашим xaml, то можете получиь что-нибудь вроде этого:



Удачи.
PS: У Josh'a Smith'a есть пост на схожую тему - сохранение настроек окна. Почитайте, интересно.

вторник, 4 марта 2008 г.

Business objects validation and UI integration

Случилось мне писать WPF Wizard. И было это сложно и интересно и много проблем порешал я.


Вот одна из них довольно интересная - валидация бизнес-объектов и управление состоянием UI в зависимости от результата валидации.

В прошлых своих постах я писал про паттерн проектирования и разработки клиентских UI приложений DM-V-VM. В моей реализации UI никак не следит за валидностью нижележащих данных, т.к. в архитектуре системы заложено, что редактированием бизнес-объектов занимается специально предназначенный для этого UI - wizard, а все остальные представления занимаются лишь отображением данных без возможности редактирования.

Если есть необходимость редактировать данные вне wizard'а, то здесь описан подход, как это можно реализовать.

Итак, что же нам нужно и что мы имеем?

Нужно:
  • автоматическое отслеживание представлениями состояния валидности объекта (кнопки Next, Save и т.п. должны быть отключены, если редактируемый объект в невалидном состоянии, и наоборот. Проще говоря, wizard не должен дать вам совершить ошибку);
  • прозрачное для разработчика валидирование - это не должна быть его забота и он не должен писать в code-behind никакой логики валидирования;

Что есть:

  • В WPF binding engine есть валидация. Она реализуется при помощи объектов классов наследников ValidationRule и статического класса Validation. Почему бы не использовать их и не угомониться на этом? Да потому что это валидация данных, лежащих в UI контролах, а не валидация бизнес объектов. Нужен унифицированный механизм, не завязанный на UI технологию, т.к. валидровать объект можно не только в UI. Но эту технологию можно использовать как точку расширения и подключения дополнительной логики в процессе валидирования данных wizard.
  • В Enterprise library (EL) есть Validation Application Block (VAB), который предназначен для решения задачи валидирования бизнес-объектов;
  • В предыдущих постах я писал об использовании комманд WPF, управляющих состоянием UI.
Итак, кажется все, что нужно есть, тогда приступим.

Для начала сценарий: wizard редактирует сущность посетитель/пользователь (как угодно).

  1. на первой странице заполняются ФИО, причем имя или фамилия поля обязательные.
  2. на второй странице заполняются настройки доступа - логин, пароль, подтверждение пароля
  3. на третьей странице заполняется и так далее... =)
В разработанном мною wizard'e редактируемая сущность "вешается" wizard'у на DataContext, таким образом все его визуальное дерево (страницы с контролами в них) наследует это свойство.

Каждому UI контролу, выполняющему какое-нибудь действие присваивается wpf команда. Каждая wpf команда, обернута уже знакомым нам классом CommandModel. Эти классы реализуют логику принятия решения о состоянии команды и логику выполнения команды. Делегируем этим классам валидирование бизнес-объекта. Но как она реализована?

Предоставим бизнес-объектам самим реализовывать свою логику валидирования. Для этого используем EL VAB. У David'а Hayden'а есть отличная серия статей по VAB. Валидация при помощи VAB настраивается 2мя путями, либо программно (при помощи аттрибутов), либо административно (при помощи конфигурационного файла). В нашем случае мы будем использовать аттрибуты, т.к. наши ограничения не меняются и заранее известны. Управлять подмножеством свойств для валидации можно при помощи ValidationRuleSet - идентификатора подмножества валидаторов. Так реализуется частичное валидирование. Т.к. страницы wizard'а редактируют только части объекта, то очевидно страница wizard'а - наилучшее место для размещения свойства validationRuleSet.

Теперь по ограничениям, т.к. наше приложение service ориентировано, то бизнес-объекты будут поставляться WCF сервисами и следовательно должны быть помечены атрибутами [DataContract] или хотя бы [Serializable] для того, чтобы сервис мог передать их через канал и десереализовать. Валидирование реализуем при помощи паттерна Template method (метод DoValidate() ), реализацию которого возложим на класс-наследник.


Диаграмма иерархии наследования классов домена.

На диаграмме приведена иерархия классов домена:


  1. ObservableObject - уже встречавшийся нам класс, реализующий InotifyPropertyChanged.
  2. DomainObject - базовый класс домена. Содержит шаблонный метод DoValidate, в котором наследниками должна будет реализовываться логика валидирования. Не является data contract, но помечен аттрибутом [Serializable]. Такое решение принято чтобы не вводить зависимость домена от технологии создания сервисов (WCF).
  3. DomainDTO - базовый класс для всех дата контрактов. Знает обо всех своих наследниках (при помощи аттрибута [KnownType]).
  4. DomainDTO, где Т - тип класса наследника. Реализует логику валидирования объекта.

Базовый класс домена.

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

Базовый дата контракт, реализующий шаблоный метод.

DomainDTO в статическом конструкторе собирает аттрибуты-валидаторы и сохраняет их. Точно также как базовый класс домена не включает в data contract список валидаторов, т.к. нет нужды передавать их через границу сервиса. Data contract'ы на стороне сервиса и клиента используются одни и те же, так что нет нужды импортировать wsdl сервиса и генерить классы контрактов, а потом вручную добавлять в них ту же логику.



Когда DomainDTO просят - валидирует себя по указанному подмножеству валидаторов (ruleSet).

Здесь он просит фасад VAB фабрики объектов создать композитный валидатор по указанному ruleSet подмножеству, а затем передает себя валидатору, который собирает, комбинирует и возвращает ответ от всех валидаторов, входящих в указанный validationRuleSet.

Наш класс посетителя мог бы выглядеть вот так:


В нем свойство Name помечено валидатором длины строки (от 1 до 200) с идентификатором подмножества CommanRuleSet, так что у страницы, редактирующей это же свойство ValidationRuleSet должно быть присвоено CommanRuleSet.

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

В wizard'е есть следующие команды:


  • NextPage - переход на следующую страницу;
  • BackPage - переход на предыдущую страницу;
  • GoToPage - переход на указаную страницу;
  • Save - сохранить редактируемый объект;
  • Cancel - отменить редактирование объекта;
  • Help - показать файл справки;
Решение проиллюстрируем при помощи команды NextPage. В OnQueryEnabled команда проверяет валиден ли объект по подмножеству validationRuleSet, заданном для текущей страницы wizard'а, не является ли текущая страница последней и не помечена ли текущая страница классом Validation как невалидная (именно это является точкой расширения, т.к. разработчик конкретного wizard'а в UI может добавить логику валидирования и сообщить команде, что по его мнению редактируемые данные невалидны):


Эта логика будет автоматически вызываться WPF framework и состояние редактируемого объекта будет управлять состоянием кнопки Next.

Extensibility point.

Для демонстрации этой возможности представьте себе следующий сценарий:

на странице wizard'а редактируется объект пользовательских данных для разграничения доступа (логин, пароль). На странице есть 1 TextBox - Login, и 2 PasswordBox'а - Password, PasswordConfirmation. В DataContext страницы wizard'а лежит бизнес-объект (пусть LoginCredential). Поле Text textbox'а Login привязано (binding) к свойству LoginCredential.Login, поле Text passwordbox'а Password в codebehind привязно к LoginCredential.Password. Для валидации данных, необходимо, чтобы пользователь повторил пароль, но в объекте LoginCredential нет поля повтор пароля. Если в passwordbox PasswordConfirmation не будет введен идентичный первому пароль все навигационные кнопки не должны быть активными. Как быть? Мы по-прежнему не хотим писать spaghetti-code.

Здесь и вступает в игру binding validation, ведь теперь мы валидируем не бизнес объект, а данные, которые лежат в UI, но сильно влияют на принятие решения о валидности объекта.

Для облегчения интеграции внешней логики в процесс валидации я написал класс ValidationHelper.



У класса есть attached-свойство Sink, которым можно пометить любой визуальный объект, наследованный от FrameworkElement и у которого задано свойство Name. В своем дефолтном стиле wizardPage присваивает привязывает ValidationHelper.Sink к себе самой, тем самым помечая себя для валидации.


Также у класса есть attached-свойство Bag, которое будет очень полезно объектам у которых нет dependency-свойств, чтобы привязывать какие-либо данные через WPF binding.

Далее, в нашем сценарии, разработчику необходимо привязать ValidationHelper.Bag у первого объекта PasswordBox к LoginCredential.Password, т.к. свойство Password PasswordBox'а не является dependency-свойством (странное решение).


Здесь вы видите в binding в коллекцию ValidationRules добавляется PasswordValidationRule, которое и является ключевым элементом интеграции. Из code-behind'а (именно, опять же из-за того что Password свойство обычное, а не dependency), при изменении пароля, получаем объект BindingExpression и просим его обновить Source, тем самым вызывая валидацию.


Сложным моментом здесь является передача объекту ValidationRule значений из UI, ведь ValidationRule не наследуется от DependencyObject и не может использовать binding. Решение этой проблемы нашел и описал наш любимы rock-star Josh Smith здесь. В кратце опишу суть - поскольку мы не можем унаследоваться от DependencyObject создадим суррогатный объект наследник DependencyObject, с единственным Dependency-свойством Value. Нужное нам значение из визуального дерева мы пересылаем в ресурсы объекту, который Josh очень лаконично назвал DataContextBridge, при помощи особого вида binding - OneWayToSource. А потом привязываем Value суррогатного объекта к DataContextBridge при помощи обычного binding. В нашей реализации таким образом объекту PasswordValidationRule передаются пароль и ссылка на страницу, в которой происходит редактирование.

Далее обязанность нашего правила сравнить пароли и пометить соответствующим образом страницу:



Логика очень проста - если пароли различны, то при помощи вспомогательного метода ValidationHelper.MarkInvalid страница помечается невалидной и в OnQueryEnabled будет принято решение о невалидности данных,т.к. страница невалидна. Если же введенные пароли идентичны, то страница помечается валидной вспомогательным методом ValidationHelper.ClearInvalid.
ValidationHelper.MarkInvalid и ValidationHelper.ClearInvalid являются всего лишь обертками методов класса System.Windows.Controls.Validation MarkInvalid и ClearInvalid, передавая им BindingExpression объекта на свойство Validation.Sink.
Совсем непростая реализация, но позволяет добиться ожидаемого поведения.
Также wizard предлагает более простую модель - реакция на события смены страниц, но это решение не обеспечивает нужного поведения UI элементов.

Удачи.
PS: перечитал пост и понял, что в последней части можно было бы обойтись и без столь сложного кода с ValidationRule - просто в code-behind сравнивать строки и помечать страницу валидной либо невалидной. подумал но исправлять ничего не стал - пусть послужит демонстрацией продвинутых возможностей WPF. Возможно где-нибудь в другом более сложном сценарии вам пригодится этот код. И обратите внимание на VirtualBranch прием от Josh'а.
PPS: а потом еще раз подумал и решил, что все правильно и так и надо делать, ибо при валидации в ValidationRule WPF автоматически отобразит результаты валидации в UI, надо лишь задать ErrorTemplate в классе Validation.