Показаны сообщения с ярлыком XAML. Показать все сообщения
Показаны сообщения с ярлыком XAML. Показать все сообщения

четверг, 28 августа 2008 г.

Dependency Injection in xaml

Здесь я опишу свой подход к DI в Xaml. Особенность решения состоит в том, что я использую CAB как платформу для построения моих smartclient приложений, но, надеюсь, за реализацией вы увидите идею, которая позволит вам абстрагироваться от конкретного IoC framework'а.

Итак, довольно часто я создаю объекты в непосредственно в Xaml, и мне бы хотелось при этом не нарушать основные принципы, с которыми я разрабатываю приложение, например, использовать, также как и в коде, IoC-контейнеры для уменьшения связанности системы.



Требования к решению как обычно сводятся к следующим:

  • возможность использовать в Binding;
  • возможность применять к POCO объектам, создаваемым в Xaml.
Решение состоит из двух частей:
  • IoCProvider - custom DataSourceProvider (можно использовать в Binding);
  • набор MarkupExtensions для использования с POCO;
IoCProvider

Этот провайдер - ядро решения. Он занимается выведением зависимостей используя стандартный для CAB IoC контейнер - WorkItem.



Поскольку созданием этого объекта будет управлять WPF engine, то и "впрыснуть" в него зависимости не удастся, но можно использовать доступный отовсюду реестр. В случае WPF это объект Application, а поскольку мой Application обязательно реализует интерфейс IRootApplication, единственное назначение которого публиковать корневой WorkItem, то задача получить IoC контейнер не составляет более труда (см. конструктор IoCProvider).

Основную работу по выведению зависимостей Provider вы полняет в методе BeginQuery, вызываемом WPF binding engine. Инициировать запрос можно также и вручную просто дернув метод Refresh() базового класса DataSourceProvider.



Теперь этот провайдер можно использовать в Binding. Но при попытке привязать поле POCO объекта при помощи Binding к выведенной зависимости, вы получите InvalidOperationException, сообщающее вам о том, что Binding можно использовать только с Dependency Property. И точно, а как же быть?

Markup Extension.

Вот выход - использовать markup extension, ведь его можно использовать практически везде в xaml. Как пример, приведу расширение для "впрыскивания" зависимых сервисов WorkItem - ServiceDependencyMarkupExtension.



И вот результат - мы создаем объект и "впрыскиваем" его зависимости прямо в Xaml.


Надеюсь, теперь вы видите, с какой легкостью можно реализовать Dependency Injection в Xaml, и абстрагировать framework от реализации IoC контейнера. Можно, например, заменить WorkItem на IContainerFacade, как это сделано в Prism.

Вот несколько статей, которые помогут вам в реализации:
creating-a-custom-datasourceprovider
Injecting Xaml with Unity Application Block using Markup Extensions

Удачи!

четверг, 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.

среда, 20 февраля 2008 г.

Enhanced ObservableObjectWrapper

Сюжет моего следующего поста навеян моими техническими приключениями. Многие думают, что работа программиста это рутина, но для меня это ежедневные искрометные схватки! Чем сложнее проблема, тем приятнее ее решать =).

Проект, который я веду, "is standing on the shoulders" проекта-предка, и в связи с этим мне приходится использовать функциональность предыдущего (или может правильнее параллельного проекта) в базовой функциональности которого не заложена совместимость с новыми технологиями, да собственно и не должна быть.

Речь здесь идет в общем-то о простых вещах - поддержка интерфейсов в объектах для data binding. Так вот, используя объекты "старого движка", назовем это так, и даже наследуясь от них пропадает та легкость и элегантность, кода, которая присуща в Binding Oriented Programming (Paul Stauvell, можно почитать здесь и здесь). Так что же делать, если я не хочу писать классы обертки каждый раз как мне надо будет использовать класс из старой функциональности, или не хочу писать spaghetti-code?

Тут и пришла идея использовать прием описанный WPF rock-star Josh'ем Smith'ом здесь.

Прием был разработан Josh'ем немного для другой ситуации - для нормального обновления UI, когда binding происходит на сам объект с интерфейсом INotifyPropertyChanged (INPC), а не на его свойства, и они (эти свойства) обновляются.

Суть решения состоит в следующем - объект подменяется оберткой(proxy, wrapper), которая реализует интерфейс INPC, и генерирует событие PropertyChanged при доступе и изменении свойств оборачиваемого объекта. Оборачиваемый объект же выставляется наружу через bindng converter.

В нашем случае интерфейс INPC может быть не реализован или реализован частично! Для этого случая и была расширена функциональность BuisnessObjectHolder от Josh'а. Интерфейс класса:


Класс наследуется от базового класса, реализующего интерфейс INPC - ObservableObject, который опять же был навеян постами Josh'а здесь. В интерфейс класса добавлены 2 internal метода - SetValueProperty и GetValueProperty. Их использует binding converter, о котором чуть позже.

При создании wrapper'a в конструктор ему передается оборачиваемый объект. Из него при помощи TypeDescriptor'а мы получаем список public PropertyDescriptor'ов и кешируем их, и, если класс реализует INPC, подписываемся на PropertyChanged.



Вот здесь на сцену выходит GenericObservableObjectConverter - binding converter, который будет выставлять наружу обернутый объект, или его свойства, имена которых, передаются в Binding через ConverterParameter.


Конвертер работает следующим образом:

  • При передаче ему объекта от binding source проверяет создан ли wrapper, если нет создает его, и в зависимости от переданного параметра возвращает в UI значения: ConverterParameter = null - wrapper; ConverterParameter = propertyName - своство обернутого объекта с propertyName, вызвав у wrapper'а GetValueProperty();


  • В обратном процессе при передаче значений от binding target к binding source конвертер проверяет значение ConverterParameter и если оно отлично от null вызывает SetValueProperty() wrapper'а.


SetValueProperty ObservableObjectWrapper'а вызывает Value_PropertyChanged, заставляя UI обновить весь объект.


Обратите внимание, что сначала объект обнуляется, это связано с тем, что wpf binding engine получив PropertyChanged увидит, что объект не изменился и проигнорирует обновление.

Теперь, когда решение практически готово, встает вопрос, а как без code-behind, создавать generic-классы в xaml? Ведь нам надо будет инстанцировать generic-класс
GenericObservableConverter. Тут опять не обошлось без гуру и навыручку пришел пост от Mike'а Hilberg'a о поддержке generic'ов в xaml. Для инстанцирования generic классов он предлагает использовать markup extension - GenericExtension. Вот слегка модифицированный код главного метода класса:


Решение в сборе выглядит следующим образом:

Недостатки данного решения: ссылаться через StaticResource на класс инстанцированный при помощи markup extension можно только из стиля в ресурсах. В других же случаях Converter необходимо будет создавать в каждом Binding.
Удачи.

понедельник, 18 февраля 2008 г.

Xaml Viewer

Продолжу.
В прошлом посте я описал свой подход к локализации Xaml. При разработке локализуемого GUI в WPF с использованием данного подхода есть один сценарий, в котором вышеописанное решение не сработает - это текст внутри TextBlock'а, отформатированный при помощи модели документов WPF. Для иллюстрации приведу пример:





-, который в другой локали может выглядеть например вот так:




как вы понимаете в данном случае сами строки в ресурсы уже не положишь, т.к. они не содержат информации о форматировании. Поэтому в случае необходимости локализации форматированного текста было решено в ресурсах хранить Xaml. Но как его отображать, не переписывая в code-behind бесконечное количество раз код загрузки Xaml? Ответ - custom control. Его функция - отображение loose-xaml (xaml в runtime).

Собственно абсолютно ничего сложного в этом контроле нет и наверное он и не заслужил бы упоминания, если бы не всплыл сценарий локализации форматированного текста.

Работает он следующим образом:
  • у контрола есть 2 режима работы: Host (наш случай - статичное отборажение отрендеренного Xaml) и Edit (вариант, в котором Xaml можно редактировать и просматривать результат "на лету" - в этом случае у него сверху появится TextBox для редактирования Xaml).
  • свойству Xaml присваивается строковое значение;

  • code-behind парсит и инстанцирует объекты (в нашем случае TextBlock с коллекцией Inline'ов) из свойства Xaml при помощи класса XamlReader и полученный объект присвает своему свойству Content;

  • Content привязан к ScrollViewer'у объявленному в default style;

  • Если в проссе разбора xaml произойдет XamlParseException, то контрол схватит его и отобразит сообщение ( что очень полезно при ручном наборе xaml =)), этот сценарий управляется триггером default стиля повешенного на изменение свойства IsInvalidXaml.

Xaml в ресурсах же выглядит вот так:


И вот так все будет выглядеть "в сборе":

Чего нехватает: xaml хранится в том виде, в котором передан ( может быть без форматирования, что жутко неудобно).

Почитать про модель документов WPF: http://msdn2.microsoft.com/en-us/library/ms748388.aspx?ref=

Удачи.

четверг, 7 февраля 2008 г.

XAML localization

Начну пожалуй.

В процессе работы над проектом столкнулся с проблемой локализации, и моя святая вера в то что язык в приложении, которое собираются продавать в разных странах должен быть английским и никаким другим (что удивительно совпадает с мнением чуваков из MS =) ), разбилась. да разбилась о неколебимую твердь и т.д. и т.п.
Короче. Есть проблема и есть несколько способов ее решения. На codeproject'e есть несколько статей посвященных данной проблеме:

  1. http://www.codeproject.com/KB/WPF/WPFUsingLocbaml.aspx
  2. http://www.codeproject.com/KB/WPF/Localization_in_WPF.aspx
  3. http://www.codeproject.com/KB/WPF/WPFLocalize.aspx
  4. http://www.codeproject.com/KB/WPF/WPF-Mulit-Lingual.aspx
, а также статья в MSDN на тему WPF Globalization and Localization.

Во всех этих источниках предлагается готовое решение, но каждый из подходов обладает своими достоинствами и недостатками (с моей точки зрения), которые я вкартце перечислю:

  1. Локализация, используя resx файлы и custom code generator (его применение обусловлено ограничением binding engine wpf - binding создается только лишь для public свойств public класс'ов) (статья 1): из достоинств - поддержка design-time и легкость в использовании (всего лишь создать ObjectDataProvider натравить его на сгенерированный public класс с ресурсами и спокойно Bind'ить строки к DEPENDENCY свойствам объектов в xaml). К недостаткам прежде всего следует отнести необходимость дополнительной инсталляции к VS, и самое главное таким способом можно локализовать только dependency свойства! В других же сценариях данный способ недейственен.
  2. Локализация используя LocBaml (упрощенный вариант - создание ResourceDictionary со строками и использование их через MergedDictionaries и DynamicResource)(статья 1): урощенный да не очень. Все равно используя LocBaml сборка будет проходить в 2 этапа.
  3. Локализация используя LocBaml (полный вариант - следуя официальным руководствам от MS локализуется сам XAML, точнее BAML)(статья 1, 2): см. п.2.
  4. Подход описанный в статье №3 очень похож на 1 метод в статье 1, но отличается от него тем, что для каждого класса ресурсов автор предлагает писать класс обертку вручную, а у меня в 45 проектов и почти в каждом из них есть ресурсы! (на заметку: если использовать ссылку на файл то может быть удалось бы избежать многократного копирования, надо проверить); в design-time будут отображаться default ресурсы. Из достоинств - возможность переключать культуры в runtime.
  5. Локализация используя xml файлы, XmlDataProvider, XPath запросы в Binding(статья 4): на мой взгляд отличный способ - и поддержка design-time и runtime переключение культур, но единственный недостаток - сложность поддержания множества файлов с локализованными ресурсами.

Итак, проанализировав предложенные подходы я выделил следующие требования, которым должно удовлетворять искомое решение:

  • отсутствие ограничения на локализуемые свойства (не только dependency но и обычные clr свойства);
  • легкость локализации и поддержания ресурсов;
  • отсутсвие необходимости писать классы обертки или использовать custom tools;
  • минимальная поддержка designtime - главное чтобы UI открывался в Blend.

Слив все в один котел и немного помешав, со дна всплыло решение - markup extension.

Идея подхода следующая: все ресурсы хранятся в resx файлах, что дает нам замечательные возможности хранить в VSS, редактировать и добавлять ресурсы прямо в VS, т.к. локализация в Net 2.0 действительно была сделана великолепно; в xaml вместо строк вы подставляете этот extension и передаете ему Id строки в файле ресурсов, а extension в момент распарсивания xaml (runtime) подставляет полученную строку или сообщение об ошибке. Выглядит это примерно так:

<TextBlock Text={me:CultureResource resourceId}/>

, где me: - объявление пространства имен, в котором существует класс CultureResourceExtension. Данный подход позволяет локализовать не только dependency свойства объектов, но вообще любое строковое свойство любого объекта, создаваемого в XAML. Из недостатков же - отсутствие поддержки design-time, точнее весьма ограниченная поддержка ( все открывается в Blend, но вместо искомых строк - "ResourceStr", - что, как показала практика, некритично).

Еще из недостатков необходимость явно задавать имя класса ресурсов, в случае если:

  • имя сборки отличается от пространства имен по умолчанию;
  • файл с ресурсами не лежит в папке /Properties;
  • и то и другое вместе.

Для решения этой проблемы в класс введено свойство BaseName, в котором явно прописывается имя класса с ресурсами в виде строки. Для упрощения синтаксиса, я ввел класс BaseBinder с наследуемым по визуальному дереву attached свойством Base. С помощью него можно существенно сократить семантику записи данного класса, задавая это свойство лишь один раз корневому объектау визуального дерева. И вот тут уже будет действовать ограничение на DependencyObject - таким образом упростить запись можно только в визуальном дереве WPF, для обычных же clr объектов придется писать BaseName при каждом появлении CultureResource.

Самым сложным сценарием оказался текст форматированный с помощью модели документов WPF! Но и этот сценарий удалось решить, загружая в ресурсы не строки, а Xaml (сомнительное решение согласен, но работает). Просмотр же этого самого loose-xaml осуществляет custom control XamlViewer о котором я напишу, пожалуй, в следующий раз.

Код класса с attached свойством для упрощения записи:
public static class ResourceBaseBinder
{
public static string GetBase(DependencyObject obj)
{
return (string)obj.GetValue(BaseProperty);
}

public static void SetBase(DependencyObject obj, string value)
{
obj.SetValue(BaseProperty, value);
}

public static readonly DependencyProperty BaseProperty =
DependencyProperty.RegisterAttached("Base", typeof(string), typeof(CultureResourceExtension),
new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.Inherits));
}

Главный метод класса расширения:


Удачи.