среда, 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.
Удачи.

1 комментарий:

miles комментирует...

мусчина я в этот раз понял прочел больше чем в прошлый но ни черта опять не понял : )
но все равно буду заходить и оставлять бесполезные комменты : )