Вот одна из них довольно интересная - валидация бизнес-объектов и управление состоянием 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 редактирует сущность посетитель/пользователь (как угодно).
- на первой странице заполняются ФИО, причем имя или фамилия поля обязательные.
- на второй странице заполняются настройки доступа - логин, пароль, подтверждение пароля
- на третьей странице заполняется и так далее... =)
В разработанном мною 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() ), реализацию которого возложим на класс-наследник. Диаграмма иерархии наследования классов домена.
На диаграмме приведена иерархия классов домена:
- ObservableObject - уже встречавшийся нам класс, реализующий InotifyPropertyChanged.
- DomainObject - базовый класс домена. Содержит шаблонный метод DoValidate, в котором наследниками должна будет реализовываться логика валидирования. Не является data contract, но помечен аттрибутом [Serializable]. Такое решение принято чтобы не вводить зависимость домена от технологии создания сервисов (WCF).
- DomainDTO - базовый класс для всех дата контрактов. Знает обо всех своих наследниках (при помощи аттрибута [KnownType]).
- 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-свойством (странное решение).
