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

WPF command & CAB command - run into one another

В прошлый раз я писал про DM-V-VM "framework", как расширение к PnP smart client software factory (SCSF) и SCSFContrib. Одним из интересных моментов в snippet'e был класс ActionViewModel, в котором содержится список команд View, и который автоматически привязывает команды к UI. Там мы использовали класс CommandModel, являвшийся оберткой над WPF командой. Но CAB также предлагает совю реализацию паттерна Command. И как же быть? какую реализацию использовать?

CAB реализация имеет преимущества доступа к IoС-контейнеру, а следовательно и ко всем ресурсам (локальные сервисы, ресурсы, виды) composite приложения, разработанного при помощи CAB. А WPF команда тесно интегрирована с WPF framework'ом и следовательно вам не надо будет заботиться о логике опроса команды и управления состоянием UI. Как использовать преимущества одной и другой реализаций?

Здесь я собираюсь рассказать о своем решении, являющемся слиянием 2ух сущностей - WPF command и CAB command.
На самом деле в SCSFContrib (интеграция WPF и CAB) Kent Boogaart уже все сделал - WrappedCabCommand, но у его решения есть серьезный недостаток - невозможность передачи параметра команде. Эту проблему я решил, развив его идею.
IActionCatalogService

Основное усовершенствование WrappedCabCommand заключается в использовании IActionCatalogService - локального сервиса SCSF, как основного обработчика команды и ее состояния. Здесь есть хорошее описание данного локального сервиса SCSF. Сервис этот связывает действия(actions) с методами, помеченными атрибутом [Action(ActionName)],


регистрирует условия(conditions) - код реализующий логику принятия решения о возможности выполнения конкретного действия,

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


Методы, помеченные атрибутом [Action(ActionName)] должны быть публичными и соответствовать сигнатуре делегата ActionDelegate. Подпиской методов управляет ActionStrategy из SCSF, так что для присоединения метода к действию, надо либо создать класс, в котором этот метод определен, с помощью IoC-контейнера CAB, либо просто добавить его в коллекцию WorkItem.Items.


Условия - классы, реализующие интерфейс IActionCondition. Автоматически вызываются сервисом IActionCatalogService перед выполнением действия, либо явно вызываются клиентом через тот же сервис. Сервис строит из условий цепочку и конечный результат является произведением всех отдельных результатов опроса IActionCondition.


СAB Command.

В CAB команды представляются коллекцией объектов Command, хранящихся в WorkItem, доступ к которым осуществляется по уникальному строковому идентификатору. Сам CAB работает с командами через наследников класса CommandAdapter, управляя подписчиками, триггерами и т.п. Command'ы создаются CommandStrategy ObjectBuilder'a (OB), когда в процессе создания объекта OB встречает публичные методы-обработчики комманды, соответсвующие сигнатуре EventHandler'a и помеченные атрибутом [CommandHandler(CommandName)]. CommandAdapter содержит внутреннюю таблицу триггеров(объектов вызывающих выполнение команды) и подписчиков на указанные сообщения. И когда происходит событие в объекте-триггере, указанное при привязке триггера к команде, WorkItem вызывает всех обработчиков подписанных при помощи [CommandHandler(CommandName)].


Добавление UI-control'a в список триггеров команды ActivateOverview.

Метод-обработчик команды ActivateOverview. Будет автоматически добавлен в список методов-обработчиков CommandAdapter'a для комманды ActivateOverview.

WPF Command.

В WPF команды представляются объектами, реализующими интерфейс ICommand. В библиотеке есть 2 класса реализующих этот интерфейс - RoutedCommand, RoutedUICommand, которые маршрутизируют события CanExecute и Executed по всему визуальному дереву. Сами команды по умолчанию не содержат никакой логики, а предлагают реализовать ее любому объекту, подписавшемуся на эти комманды через CommandBinding где-либо в визуальном дереве. UI элементы, с которыми можно связать комманду, реализуют интерфейс ICommandSource и содержат свойства: Command - собственно комманда (объект с интерфейсом ICommand), CommandParameter - параметр, который передается команде (необязательное свойство) и CommandTarget - цель, на которой эта команда должна выполняться (необязательное свойство).

Come together!

За основу для объединения сущностей CAB и WPF комманд возьмем класс CommandModule.


Реализацией идеи является класс ModuleCommand:

Интерфейс класса ModuleCommand. IActionCatalogService инжектируется IoC-контейнером CAB при создании.

В отличие от WrappedCabCommand этот класс не требует передачи ему объекта CAB комманды. CAB комманду он находит сам, для этого ему лишь требуется имя - CommandName.

Для уникальной строковой константы CommandName принято соглашение - она идентифицирует следующие сущности: действия IActionCatalogService, Cab команды: собственно команду CommandName, и если определены строковые константы CommandName с префиксами Before[CommandName] и After[CommandName] - команды-тригеры до и после выполнения действия.


Переопределенный виртуальный метод OnQueryEnabled автоматически вызывается WPF framework. В нем производится опрос команды и ActionCondition'ов можно ли выполнять команду, вынося всю специфичную логику наружу (вызов CanExecute сервиса), и в зависимости от результата UI соответственно отображает состояние - enabled или disabled.

В интерфейсе класса есть свойство ActionTarget - дополнительный параметр, который команда будет автоматически передавать в метод-подписчик на действие.
CAB комманда учавствует в логике управления состояния комманды через свое свойство State. Оно влияет на принятие решения в OnQueryEnabled класса ModuleCommand.


Переопределенный виртуальный метод OnExecute вызывается WPF framework при перехвате события от UI элемента, связанонго с командой и вызывающего ее срабатывание, и делегирует выполнение кода триггеров и собственно команды IActionCatalogService и CAB командам.
Таким образом путем делегирования логики принятия решения о состоянии комманды и логики выполнения комманды IActionCatalogService решается проблема слияния CAB и WPF сущностей комманды. CommandBinding связывает комманду с UI, а IActionCatalogService связывает комманду с инфраструктурой CAB. Использование IActionCatalogService также позволило решить проблему передачи параметра в команду, т.к. CAB реализация команды такой возможности не предусматривает.

CAB command по-прежнему может использоваться в своем первозданном виде, когда логика валидирования комманды проста и не требует сложных вычислений или сравнений для определения состояния комманды.

I said work!

Дабы не быть голословным и продемонстрировать решение в действии давайте модифицируем пример из прошлого поста. Основные изменения коснутся FooViewModel. Теперь коллекция его комманд будет состоять не из двух объектов различных классов с жестко зашитой в их код логикой, а из двух объектов одного типа (ModuleCommand), логика которых будет делегирована IActionCatalogService.


+ в проекте добавятся 2 класса:
обработчик действий


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


+ список идентификаторов:

Удачи!

PS: Интересный подход к реализации логики комманд демонстрирует Josh Smith в своей статье. Суть его предложения в следующем - сделать RoutedCommand "умнее", добавив ей самой возможность определять логику. Он наследуется от RoutedCommand и в классе наследнике вводит attached-свойство, помечающее UI-объект как обработчик комманды и вводящий 2 абстрактных метода OnCanExecuteCore и OnExecuteCore для реализации логики в классах наследниках. Таким образом можно CommandModel заменить на SmartRoutedCommand, но в таком случае, усложнится xaml UI, т.к. там надо будет прописывать CommandSink'и каждой команды вручную.
PPS: вспомнил, что видел еще одну реализацию основанную на использовании CAB EventBroker. Работает, и кстати в реализации проще, но считаю, что зря чувак смешал сущности.
UPD: выложил код для скачивания здесь.

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

Model-View-ViewModel for WPF

В моем проекте весь UI построен на очень простом и одновременно очень мощном паттерне проектирования клиентских приложений DM-V-VM (DataModel-View-ViewModel), для реализации которого я написал простой framework (модное слово не удержался), с огромной помощью серии статей Dan'a Crevier(см. ссылку выше).
Основной технологией для разработки клиентских приложений в проекте был выбран WPF, в комбинации с Composite Application Block (CAB). В этом framework'е (CAB) активно используются и стимулируется разработчиком использование 2 паттернов для клиентских приложений: Model-View-Controller (M-V-C) и Model-View-Presenter (M-V-P). Мой же "Framework" является расширением и дополнением к CAB и SmartClient Software Factory (SCSF) - фабрики построенной на Composite Application Block), и использует часть ее функциональности, а в частности IoC-контейнер CAB для выведения зависимостей классов.

Паттерн DM-V-VM является частным случаем другого более общего паттерна написания UI кода - M-V-P, и в терминах Фаулера может называться PresentationModel. Есть с ним одна проблема - он шикарно вписывается в модель программирования с использованием WPF, но не позволит с легкостью переключиться на другую UI технологию, как это позволяет M-V-P (можно почитать у Dr.WPF). И, если вы готовы с этим мириться (а я очень даже готов, т.к. не планирую менять UI технологию), то эта модель в самый раз. Да, и, возможно, в сложных UI (например отображение и редактирование данных из многих, > 5 источников данных) поддержка ее также окажется черезчур сложной. Здесь я бы посоветовал использовать M-V-P, но на практике с такими случаями я пока не встречался.
Начну же препарировать данный паттерн.
DataModel(DM)

Классы, которые поставляют данные (бизнесс объекты). Их обязанность поставлять бизнес-объекты для отображения и редактирования в UI в WPF-дружественной манере (например, реализуя INotifyPropertyChanged). Все обращения к свойствам этих классов должны производиться в UI потоке и ни в коем случае не должны блокировать его.

На мой взгляд, DM отлично вписывается в SOA архитектуру, поставляя данные для Presentation слоя из SOA инфраструктуры, являясь промежуточным шлюзом данных.

На самом деле данные DM может получать из самых различных источников, что реализуется через делегирование общения с поставщиком данных Provider'у (можно было бы назвать и стратегией). Многое в реализации базового класса взято из DM Dan'a Crveier, поэтому не буду приводить здесь весь код, а сосредоточусь на усовершенствованиях его класса.

  1. Первое усовершенствование - введение базового интерфейса для DM и введение generic-интерфейса для наследников, параметризуемых Provider'ом.

  2. Введение типизированного провайдера и создание generic интерфейса DM с ограничением на поставляемый тип данных.


  3. Базовый класс DM параметризован интерфесом поставщика данных, а также содержит ряд других усовершенствований: асинхронные методы для оповещении об окончании загрузки, для асинхронной установки значения поля, оповещении об исключении, которое произошло в ходе обновления DM, - и ряд виртуальных методов для перегрузки в классах-наследниках.

Введение generic интерфейса для Provider'а позволило написать generic класс DM параметризуемый интерфейсом поставщика и типом данных SEModel (SingleEntityModel SEDM), избавив тем самым от написания многих классов наследников, занимающихся только тем, что поставляют разные типы данных.



Обратите внимание, что в классе у свойства Entity присутствуют публичные get и set методы, а в интерфейсе только get. Это сделано для того, чтобы обеспечить возможность асинхронной установки значения свойства по его имени через PropertyDescriptor, инкапсуляция же обеспечивается через публикацию только лишь интерфейса ISEDM, где Entity - свойство только для чтения, т.е. клиенты взаимодействуют только через интерфейс, который может "инжектироваться" как зависимость в клиенты IoC-контейнером.
View

VM про класс View ничего не должен знать, и общается с ним через интерфейс IView. Как я уже упоминал выше, DM-V-VM не позволяет с легкостью менять UI технологию, не переписывая кода VM (так как в VM вынесены такие technology-specific понятия как WPF command), то я просто для упрощения дальнейшего кода ввел интерфейс IWPFView, в котором опубликовал специфичные для WPF свойства. Такое вот волевое решение. =)


ViewModel

И, наконец, главный участник троицы - ViewModel. Этот класс должен содержать всю логику UI. VM инстанцируется при создании View IoC-контейнером и выводит все его зависимости. Базовый класс ViewModelBase параметризуется интерфейсом View, а его наследник в дополнение еще параметризуется интерфейсом DM.


Эти классы вводят ряд полезных методов для перегрузки, вызываемых при установке/удалении вида/модели, при перехвате событий от модели о успешном обновлении/ об ошибке. Также VM при получении View подписывается на его события Loaded/Unloaded и предлагает виртуальные методы для перегрузки вызываемые при перехвате этих событий. В реализации по умолчанию VM в методе OnViewSet присваивает IView.DataContext указатель на себя, с тем, чтобы IView мог обновляться самостоятельно при перехвате событий от VM, DM и бизнес-объектов, поставленных DM. Таким образом в терминах Фаулера View является ActiveView.

На диаграмме классов также можно заметить класс ActionsViewModel. Этот класс содержит список CommandModels (также очень удобная конструкция от Dan'a Crevier инкапсулирующая WPF комманды), на каждую из комманд которого ActionsViewModel автоматически создает в View CommandBinding.

ViewModel в данном его виде не запрещает редактировать данные, поставленные DM и никаким образом не следит за валидностью и состоянием объекта (в процессе редактирования он просто может быть приведен в невалидное состояние, что затем вызовет цепочку ошибок). Во избежание этого в моем проекте все объекты редактируются в Wizard'ах, которые производят валидацию этих объектов и не дают сохранять объекты в промежуточном или невалидном состоянии (об этом в следующем посте). Если же есть необходимость редактировать бизнес-объект прямо в View, то рекомендую прочитать вот этот пост от Pete W.

DM-V-VM

Соберем теперь пример. Пусть у нас будет некий объект Foo, который будет поставляться SEModel (вот где пригодилась функциональность generic класса - не надо писать новый класс!) при помощи FooProvider.





Вся логика содержится в FooViewModel. Этот класс наследуется от ActionsViewModel и содержит 2 CommandModel: IncCommandModel и DecCommandModel. IncCommandModel увеличивает значение Foo.Bar, а DecCommandModel уменьшает. При загрузке View ViewModel просит DM обновиться.




Теперь очередь View.

В code-behind при создании View создается ViewModel.



И, поскольку ViewModel уже установил View.DataContext на себя, то можно в View поместить ContentControl, "забайндить" его свойство Content на DataContext (он используется поумолчанию, если в Binding не указан путь) и написать темплейт для рендеринга ViewModel.



И здесь мы столкнемся с единственным злом generic класса DM - binding далеко не всегда может найти у этого класса указанный путь, поэтому здесь мы явно при перехвате успешного обновления DM в FooViewModel сохраняем объект Foo в ресурсах View.



Да, если не использовать IoC-контейнер, то все [CreateNew] и [ServiceDependency] надо заменить на вызов конструкторов.

Вот, что у нас получилось:


Удачи.
PS: В ближайшем будущем выходит новый проект от PnP group - Prism. Он призван послужить заменой CAB в процессе разработки LOB приложений на WPF. Жду.
Upd: выложил код примера здесь.

среда, 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));
}

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


Удачи.


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

Начало

Обещаю марать тебя блог
записями о найденных мной или не мной
решениях в области высочайших технологий
о WPF WCF WF SOA и прочих магических заклинаниях
буде таковые найду или сами найдутся!

Писать обещаю нечасто ибо у меня от мозга до языка путь неблизкий
так что представьте сколько мысль до пальцев идет =)
хотя/ведь именно ими (мозгами/пальцами а не пальцами/мозгами) я в основном работаю =)/=)