Рылся в кодах Prism и натолкнулся на вот такой код:
public event EventHandler
я так никогда раньше не писал, но данный паттерн позволяет совершенно не заботится о безопасности вызова и можно смело писать так:
Updated(null, new AccountPositionEventArgs());
вместо:
EventHandler
if (handler != null)
handler(null, new AccountPositionEventArgs());
так что на заметку.
Удачи!
четверг, 27 марта 2008 г.
Safe event pattern
среда, 26 марта 2008 г.
понедельник, 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 полей.
В конструктор декоратора IoC-контейнером инжектируются SEModel и CFProvider. Декоратор для обернутой модели устанавливает режим синхронного обновления и подписывается на событие изменения свойств.
Основная работа выполняется в методе DoUpdate. Декоратор сначала обновляет custom поля для индексированной по Id сущности, а затем делегирует обновление обернутой DataModel.
Единственное место, где к декоратору обращаются явно - custom xaml, который хранится в БД и создается в конфигураторе. В данном примере его будет поставлять ICustomContentModel. Xaml как я уже говорил отображается XamlViewer:
, а сам xaml может выглядеть вот так:
Все метаданные (список custom полей, xaml с данными - имя поля к которому надо привязываться (binding), порядок расположения полей, их имена в UI, отображаени/скрытие выбранных полей) конфигурируются в специальном приложении - Configurator.
Удачи.
четверг, 6 марта 2008 г.
WPF ConfigurationDialog
Сообщу вам новость - во всех приложениях есть настройки! И их надо (вы не поверите) настраивать! Так вот об этом и пойдет сегодня речь - создание конфигурационного диалога.
Во-первых определимся с дизайном:
- это должно быть окно;
- оно должно быть красивое (не ко мне в другой блог пожалуйста);
- должен быть какой-то унифицированный механизм рендеринга настроек в диалоге.
- свое появление
- уход в тень главного окна
- свое исчезновение
- переход на передний план главного окна
Обратите внимание на свойство SharedSizeGroup у ColumnDefinitions и IsSharedSizeScope у Grid - удивительная прозорливость разработчиков WPF - они предусмотрели такую возможность, как разделяемый размер столбцов или строк в Grid'е. Задавая им одинаковый идентификатор вы объединяете их в одну группу с разделяемым размером (одним общим размером, максимальным из всех необходимых). Таким образом, если не задавать явно нашим столбцам размеры, их ширина будет определяться лишь максимальной шириной кнопки, которая в свою очередь определяется контентом (длиной строки в нашем случае). В итоге мы имеем 2 кнопки одинаковой ширины, которые синхронно изменят свой размер, при смене языка, т.к. на другом языке строки могут быть длиннее или короче. Удивительная прозорливость, и удивительное отсутствие нормальных средств для локализации.
Далее, кнопка "Отмена" - IsCancel = true - принажатии на нее DialogResult автоматически = false
В триггере ControlTemplate следим за свойством Validation.HasError, - используя технику продемонстрированную в предыдущем посте, можно пометить диалог как невалидный и тогда кнопка "Ок" станет неактивной.
2. Beautiful window.
Цифрами на картинке я обозначил зоны диалога. 1,2,3 - зона контента, 4 - зона кнопок. Они определены в дефолтном стиле DialogBox. Наш класс ConfigDialog будет наследоваться от DialogBox, поэтому зоны 1,2,3 - это Content окна.
- Список настраиваемых элементов. Каждый элемент списка должен иметь картинку и короткий заголовок.
- Зона настроек все эти бла-бла-бла - настройки.
- Зона длинного заголовка текущего настраиваемого элемента.
3. Tunes.
Для списка создается CollectionView через xaml-proxy CollectionViewSource, к которому привязан список. Здесь мы имеем классический пример master-detail binding, все элементы привязываются в binding к CollectionView, который синхронизирован с ListView, таким образом все элементы отображают текущий выбранный элемент в списке. По поводу CollectionView - это можно сказать встроенная в библиортеку реализация ViewModel для отображения коллекций в UI.
Так вот эти самые настройки и предлагается редактировать в xaml. UI для редактирования настроек (Content ConfigDialog'а зона 2) передается в виде стиля WPF. Хранение его в виде стиля, а не например UserControl'а позволяет добиться следующих преимуществ:
- хранится в View в ResourceDictionary и будет создан единожды при загрузке View как синглтон.
- всю обработку событий этого UI code-behind View должен делегировать тому же VM классу, т.е. вся логика в VM.
- в стиле можно использовать MarkupExtensions как источники данных для StaticResource.
Если ваше представление имеет какие-либо настройки, оберните их в один из классов Tune
вторник, 4 марта 2008 г.
Business objects validation and UI integration
Случилось мне писать WPF Wizard. И было это сложно и интересно и много проблем порешал я.
- автоматическое отслеживание представлениями состояния валидности объекта (кнопки Next, Save и т.п. должны быть отключены, если редактируемый объект в невалидном состоянии, и наоборот. Проще говоря, wizard не должен дать вам совершить ошибку);
- прозрачное для разработчика валидирование - это не должна быть его забота и он не должен писать в code-behind никакой логики валидирования;
Что есть:
- В WPF binding engine есть валидация. Она реализуется при помощи объектов классов наследников ValidationRule и статического класса Validation. Почему бы не использовать их и не угомониться на этом? Да потому что это валидация данных, лежащих в UI контролах, а не валидация бизнес объектов. Нужен унифицированный механизм, не завязанный на UI технологию, т.к. валидровать объект можно не только в UI. Но эту технологию можно использовать как точку расширения и подключения дополнительной логики в процессе валидирования данных wizard.
- В Enterprise library (EL) есть Validation Application Block (VAB), который предназначен для решения задачи валидирования бизнес-объектов;
- В предыдущих постах я писал об использовании комманд WPF, управляющих состоянием UI.
- на первой странице заполняются ФИО, причем имя или фамилия поля обязательные.
- на второй странице заполняются настройки доступа - логин, пароль, подтверждение пароля
- на третьей странице заполняется и так далее... =)
- ObservableObject - уже встречавшийся нам класс, реализующий InotifyPropertyChanged.
- DomainObject - базовый класс домена. Содержит шаблонный метод DoValidate, в котором наследниками должна будет реализовываться логика валидирования. Не является data contract, но помечен аттрибутом [Serializable]. Такое решение принято чтобы не вводить зависимость домена от технологии создания сервисов (WCF).
- DomainDTO - базовый класс для всех дата контрактов. Знает обо всех своих наследниках (при помощи аттрибута [KnownType]).
- DomainDTO
, где Т - тип класса наследника. Реализует логику валидирования объекта.
Базовый класс домена.
Как видите класс не сериализует результаты валидирования, ибо нет нужды передавать их через границу сервиса.
Базовый дата контракт, реализующий шаблоный метод.
DomainDTO
Здесь он просит фасад VAB фабрики объектов создать композитный валидатор по указанному ruleSet подмножеству, а затем передает себя валидатору, который собирает, комбинирует и возвращает ответ от всех валидаторов, входящих в указанный validationRuleSet.
В wizard'е есть следующие команды:
- NextPage - переход на следующую страницу;
- BackPage - переход на предыдущую страницу;
- GoToPage - переход на указаную страницу;
- Save - сохранить редактируемый объект;
- Cancel - отменить редактирование объекта;
- Help - показать файл справки;
Для демонстрации этой возможности представьте себе следующий сценарий:
на странице 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, но сильно влияют на принятие решения о валидности объекта.
Также у класса есть attached-свойство Bag, которое будет очень полезно объектам у которых нет dependency-свойств, чтобы привязывать какие-либо данные через WPF binding.
Логика очень проста - если пароли различны, то при помощи вспомогательного метода ValidationHelper.MarkInvalid страница помечается невалидной и в OnQueryEnabled будет принято решение о невалидности данных,т.к. страница невалидна. Если же введенные пароли идентичны, то страница помечается валидной вспомогательным методом ValidationHelper.ClearInvalid.