15 Апрель 2009 г.

Designing managed API over "flat" native one or .Net SIP library

Попробую описать процесс проекции плоского API, написанного в процедурном стиле, в многомерное ООП пространство, на примере работы, которой было занято почти все мое свободное время за последний месяц.

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

Конечно обертка эта не покрывает все то богатство возможностей управления процессом, которое предоставляют различные модули pjsip, но предоставляет высокоуровневый .Net интерфейс к pjsua (user agent) API, необходимый для быстрого создания SIP клиентов.

  1. P/Invoke или как не утонуть в тоннах кода;
  2. Designing API;
  3. Testing API;
1. P/Invoke.

Я пропустил очень важную часть - выбор подходящей библиотеки, т.к. считаю, что к теме заметки это имеет мало отношения, но все-таки немного опишу мотивацию, которая диктовала выбор SIP библиотеки pjsip. Мне нужна библиотека:

  • реализующая последние RFC в наиболее полном объеме;
  • с поддержкой различных кодеков, в том числе широкополосных;
  • портируемая на разные платформы (на будущее);
  • с возможностью передачи видео-потока (на будущее);
  • open source с разумной лицензией, которая позволила бы использовать ее в коммерческих приложениях. (UPD3: GPL, под которой распространяется эта библиотека не такая уж и разумная, а, как сказал мой друг: "детище неадекватного проповедника Столмена". Следует заметить, что исходное выражение подверглось жесткой цензуре =). )
pjsip удовлетворяет всем этим условиям, и многим другим, неперечисленным здесь. Замечательная вобщем библиотека.

Итак, прежде всего нам надо скачать исходные коды и собрать dll. Скачать pjsip можно из SVN хранилища, как стабильную версию, так и текущий закоммиченый код.

Далее, забегая вперед, сразу же скажу, что при генерации interop-кода возникла проблема с callback функциями, т.к. C# поддерживает только stdcall callback функции (UPD: врешь ты все =) ), т.е. делегаты будут вызываться как stdcall, тогда как pjsip декларирует свои callback'и как cdecl, это проблема! Путей решения у нее несколько.
Если бы у нас не было доступа к исходному коду pjsip - у нас была бы проблема, которую пришлось бы решать нетривиальным образом, изменяя IL код, сгенерированный для interop кода. Этот хак описан здесь.
UPD: Да, и пока я искал ссылку, нашлось более простое решение, поставляемое самой платформой .Net -
UnmanagedFunctionPointer атрибут, с помощью которого можно пометить делегат и обозначить calling convention.
Последнее же решение, к которому прибегнул я, - пометил все нужные мне callback функции в pjsua __stdcall, благо их немного.

Итак, dll, экспортирующую функции высокоуровневого pjsua API я собрал, но даже для этого интерфейса (довольно скромного по сравнению с более низкоуровневыми) я бы писал interop код вручную недели две. Столько времени у меня нет, да это еще к тому же ужасно скучно, поэтому я нашел средство автоматической генерации interop кода - PInvoke Assistant. Достаточно лишь указать этой замечательной утилите всю необходимую информацию, и вуаля - 90% кода сгенерились автоматически. Надо сказать, что 10% недостающего кода эта_замечательная_утилита все-таки не осилила, но винить ее в этом я не намерен, т.к. код был действительно очень сложный для автоматического разбора. Вобщем 10%, написанные вручную, все-таки лучше, чем 100%.

UPD2: нашел еще одну утилиту для генерации interop кода: SWIG
. Выглядит она намного мощнее той, которую я использовал, достаточно посмотреть на список поддерживаемых языков.

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

2. Designing API.
Дизайн ООП обертки во многом продиктован структурой pjsua API (больше половины работы уже сделано за нас). pjsua логически и очень интуитивно разделяется на следующие группы:
Также логически и интуитивно я выделил следующие сущности:
  • SIPUserAgent - центральная фигура и главная точка доступа к API, отвечающая за создание/конфигурирование клиента;
  • Account Manager - интерфейс для управления аккаунтами (аналог - телефонный номер);
  • Call Manager - интерфейс для управления звонками (прием/создание новых звонков, управление состоянием);
  • Media Manager - интерфейс для управления медиа-параметрами системы (кодеки, звуковые устройства, конференция);
  • Conference Bridge - жалкое отражение мощного медиа-микшера pjmedia;
Итак вот он взгляд на систему с высоты:

Поскольку большинство объектов являются обертками над нативными структурами, выделяющимися в неуправляемой памяти, то нам нужен механизм управления освобождением последних. Такой механизм в .Net изобретен давно и называется он Disposable pattern. Очень кстати полезно помнить про этот паттерн, так как он .Net specific, а про такие MS любит спрашивать на собеседовании. Фундамент для Disposable pattern - базовый класс Resource, с одной лишь разницей, что он не реализует IDisposable, т.к. я не хочу отдавать контроль над временем жизни всех объектов, потому что это может нарушить работу системы. Например: нельзя дать пользователю этого API освободить ресурсы VoIPTransport'а во время звонка, или даже пока в системе есть зарегистрированные аккаунты. Следить же за всеми зависимостями просто невозможно, и вообще это есть нарушение всех принципов хорошего дизайна. Поэтому в Resource вместо публичной реализации IDisposable есть internal метод InternalDispose. В случае, когда время жизни объекта может управляться извне, то класс наследник Resource явно реализует интерфейс IDisposable.

Одна из основ дизайна - инициализируемые объекты. В pjsip прежде чем что-то использовать, это что-то сначала надо сконфигурировать и проинициализировать. Причем некоторые объекты поддерживают повторную реинициализацию, а некоторые - нет. О проблемах и решениях инициализации объектов я писал в предыдущей заметке, как раз в то время, когда создавался дизайн всей системы. Каждый инициализируемый класс, проводит валидацию установленных параметров по окончании сессии инициализации.

SIPUserAgent (UA) - центральная точка доступа к API. UA - инициализируемый объект и прежде чем вы получите доступ к различным частям системы, его необходимо... правильно! проинициализировать. По окончании сессии UA проверит все введенные данные на валидность и, если все в порядке, создаст необходимые структуры pjsua. UA поддерживает конфигурирование в config файле, но по умолчанию не использует его, хотя и имеет для этого явный интерфейс - метод ReadConfiguration(), который может быть вызван только во время сессии инициализации. Имеется и зеркальный метод WriteConfiguration(), который следует вызвать до того, как UA будет уничтожен. Кстати, UA это пример объекта, временем жизни которого можно управлять пользователям API, поэтому он реализует интерфейс IDisposable и как дополнение, для большей наглядности метод Destroy(), который по смыслу ничем не отличается от Dispose().
SIPUserAgent, AccountManager, CallManager, MediaManager - singleton'ы и они существуют всегда в единичном экземпляре. Это означает, что в приложении вы не сможете создать больше одного SIPUserAgent'а, да оно и не требуется, т.к. в системе есть понятие аккаунтов.

Transport интерфейс pjsua инкапсулирован в классе VoIPTransport.
Это пожалуй самая маленькая часть системы, предоставляющая интерфейс для создания и конфигурирования конкретного типа SIP транспорта (UDP, TCP или TLS). В UA поумолчанию создается UDPTransport для SIP, который может быть заменен либо при загрузке конфигурации, либо явно одним из наследников VoIPTransport во время сессии инициаллизации. К сожалению в текущем релизе pjsip поддерживает только UDP транспорт для RTP, но в дальнейших планах стоит TCP транспорт для RTP, что авторы сопровождают пометкой "анекдот", им виднее. Поэтому на всякий случай VoIPTransport, а не SIPTransport.

Многие объекты системы имеют различные состояния, которые прекрасно описываются state machine. Состояния эти передаются через callback'и из pjsip и обрабатываются соответствующими менеджерами, так за обработку callback'ов сессии регистрации аккаунта отвечает AccountManager, который затем сообщает state machine, что ее состояние изменилось. Поначалу state machines были частью сущности, состояние которой они описывали, но при более детальном взгляде оказалось, что объект (например звонок [Call] имеет Invite-сессию и медиа-сессию) может иметь несколько никак не связанных состояний, которые лучше даже назвать сессиями. Поэтому для моделирования этих сессий я написал малюсенький набор классов, состоящий из StateMachine - базового класса для сессии; и AbstractState - базового класса для состояния. Классы состояний управляют логикой обработки событий (этакий workflow), а классы сессий просто хранят атрибуты состояния. Я даже рассматривал возможность использования WF движка для моделирования сессий, но понял, что это был бы overkill для задачи такого масштаба.

Accounts


Тут все просто - пользователь создает аккаунт, инициализирует, передает AccountManager'у, который позаботится о его дальнейшей судьбе. В принципе уже здесь можно выкинуть ссылку на аккаунт, т.к. он автоматически попадает в коллекцию аккаунтов AccountManager'ов, из которой будет удален только когда будет вызван метод AccountManagerUnregisterAccount(account).
AccountManager при старте системы создает внутренний локальный аккаунт, так что система сразу готова к приему и отправке звонков.
У аккаунта есть сессия регистрации, управляющая его состоянием. В принципе число возможных состояний аккаунта равно числу кодов статуса SIP протокола (около 100). Поэтому, чтобы не плодить кучу классов, описывающих состояния, в которые аккаунт неизвестно может ли перейти вообще, я описал лишь те состояния, в которых при тестировании аккаунт побывал однозначно (registering, timedout, registered), а для всех остальных написал общий класс UnknownStatusState, который, получая код статуса, пишет в Trace строку с описанием этого статуса.
Об изменениях в состоянии и Account и AccountManager информируют пользователей генерируя соответствующие события.
В классе Account великое множество свойств, из которых можно почерпнуть информацию о текущем состоянии аккаунта.

Calls

Здесь, конечно, дела обстоят посложнее, но не для пользователя системы, разумеется. С его точки зрения все также просто: чтобы позвонить куда-то нужно лишь вызвать метод MakeCall() CallManager'а, указав последнему куда позвонить и с какого аккаунта, чтобы принять звонок - надо подписаться на событие IncomingCall и вызвать метод Answer() переданного звонка, и все. Звонок также будет помещен в коллекцию звонков, и дальнейшей его судьбой будет управлять CallManager, ну и конечно же пользователь может явно закончить звонок, вызвав Call.Hangup(). Под капотом же все намного сложнее.

Начнем с того, что у звонка две сессии: Invite сессия, управляемая SIP протоколом, и медиа сессия, управляемая RTP протоколом.

С Invite сессией все более менее понятно - все через нее проходят каждый раз как набирают номер на мобильном и ждут соединения, а вот медиа-сессия более интересная тема. Дело в том, что звонок может иметь более одного слушателя, равно как и вещателя, т.е. быть частью конференции. Но здесь возникает небольшое расхождение с плоской иерархией классов состояний, описывающих эту сессию, т.к. звонок может быть активным и не участвовать в конференции, или участвовать, но не может быть неактивным и участвовать в конференции, т.к. это абсурд. Для описания этой ситуации я применил Decorator - ConferenceMediaStateDecorator, оборачивающий ActiveMediaState.

Между аккаунтом и звонками есть зависимость один-ко-многим, т.е. несколько звонков могут использовать один и тот же аккаунт. Следовательнго система должна каким-то образом следить за этими зависимостями. Можно создать множество и хранить в нем ассоциации, а можно воспользоваться механизмом подсчета ссылок, как это было сделано в COM. Именно этой техникой в сочетании с описанным приемом smart-pointer для .Net я и воспользовался для реализации это й задачи в loosely-coupled манере.


Каждый звонок при создании получает ссылку на аккаунт, с которым он ассоциирован. Он не хранит ее, а блокирует аккаунт (его невозможно будет удалить) вызовом метода Lock(), который возвращает IDisposable (smart-pointer), который при создании увеличивает число ссылок на аккаунт, а при явном уничтожении (Dispose) уменьшает. Этот smart-pointer освобождается при очистке ресурсов звонка.

Media

Медиа менеджер позволяет установить устройства воспроизведения и захвата звука, посмотреть коллекцию кодеков и посмотреть состояние конференц-моста.
Конференц-мост - это часть системы, выполняющая роль микшера медиа потоков. Звонки, подключающиеся к конференции, соединяются со всеми узлами двунаправленного графа, уже участвующих в конференции звонков. В системе может быть только одна активная конференция.
Подключение к конференции может происходить как автоматически - если установлен флаг UA AutoConference (тогда ActiveMediaState будет автоматически оборачиваться ConferenceStateDecorator'ом, который и подключает звонок к конференц мосту), либо явно при помощи метода Call.ConnectToConference() (в этом случае сначала будет произведена проверка активна ли медиа сессия и, если да, то ActiveMedaiState будет обернут ConferenceStateDecorator'ом с тем же результатом). За подключение звонков между собой отвечает ConferenceBridge.

3. Testing API.
Сейчас скажу крамолу - очень важно тестировать код! Поэтому в процессе разработки постоянно писалось тестовое консольное приложение. Оно позволило вычистить API, найти частично ошибки. Далее я решил залезть в чужую шкуру - стать пользователем своей же библиотеки и написать GUI приложение. И в процессе этой работы выловил еще несколько проблем. Например при уничтожении UA возникали racing conditions, связанные с отсутствием синхронизации между процессом уничтожения всех звонков и очисткой аккаунтов, или ошибки связанные с публикацией событий не в UI потоке, т.к. pjsip обрабатывает различные транзакции в различных потоках, то для упрощения клиентского кода, все менеджеры публикуют события через SyncronizationContext, буде таковой существует. Их как правило устанавливают GUI framework'и WinForms или WPF.

Conclusion
Возможно вы заметили, что я покрыл не все части pjsua API и не все возможности уже покрытых групп реализованы - это потому что моя обертка все еще в разработке!
Надеюсь чтение про мои страдания было интересным и поучительным.

PS: Код будет выложен позже.

Всем удачи!

16 Март 2009 г.

Initializable pattern

Представьте, что вы разрабатываете API, в котором объекты, являющиеся частью системы, должны быть проинициализированы перед использованием.
Есть несколько способов простимулировать будущих пользователей этого API завершать инициализацию перед использованием:

  1. Ввести метод Initialize в каждом объекте, который нужно проинициаллизировать;
  2. Более "правильный" и следующий дизайну .Net framework - реализовать в классе ISupportInitialize;
  3. Пойти еще дальше и автоматизировать запуск и закрытие сессии инициализации;
Что значит автоматизировать запуск и закрытие сессии инициализации? Скажем, возложить эту обязанность на framerwork. Как это сделать? Об этом и заметка! Вспомните мой пост про удобную обертку над Monitor'ом, с помощью которой можно узнать есть ли в очереди на доступ к ресурсу заблокированные потоки? Так вот для пущего удобства и сохранения стиля выполнения блокировок с поддержкой языковых конструкций (скобочек =) ) там использовался .Net-специфичный паттерн Disposable. Почему бы не воспользоваться тем же приемом для обрамления сессии инициализации? Сказано, сделано. Прошу любить и жаловать - .Net-specific Initializable pattern. Состоит это решение из 2х частей:
  • собственно объект, который необходимо проинициаллизировать (реализует ISupportInitialize);
  • обертка, с помощью которой и происходит неявное управление сессией инициаллизации;
Давайте взглянем на эту обертку поближе:

Это маленькая обертка, которая при создании получает ссылку на инициаллизируемый объект и запускает сессию, вызовом метода ISupportInitialize.BeginInit(). И при детерминированном освобождении закрывает сессию вызовом метода ISupportInitialize.EndInit(). Я намеренно не стал реализовывать в этом классе Disposable паттерн целиком с тем, чтобы сессия закрывалась только явно, либо вызовом Dispose() обертки, либо EndInit() самого объекта.

Вот как может выглядеть API:



и использование его в коде:



Easy-peasy
!
А вот как выглядел бы код без Initializer:

мое чувство прекрасного плачет =).

Все это конечно не отменяет проверок на наличие сессии в самом объекте, т.к. он должен разрешать редактировать себя только на протяжение последней, а в остальном мне кажется несколько упрощает код, избавляя его от 2х строчек =), зато визуально ограничивает рамки сессии, что на мой взгляд весьма удобно и лаконично.

UPD: поступила вполне обоснованная критика - трудно читаемый код, тяжело разобраться зачем создавать аккаунт и сразу же "диспозить" его. На это могу лишь ответить, что API в данном случае (и вообще, если применяется этот паттерн) должно подсказывать ЧТО оно возвращает, другими словами метод называется не AddAccount, a InitializeAndAddAccount. А еще лучше если бы он назывался CreateAccountInitializer =).

UPD#2:
Придумал как избавиться от неразберихи и нарушения принципов, сохранив красоту идеи.

Не надо делать никаких "conventions" в API. Надо дать возможность желающим писать так как они хотят и привыкли, а извращенцам вроде меня дать возможность "юзать" такие "извраты":


и пример:



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


PS: И еще одно дополнение. Вся эта возня вокруг Disposable паттерна - это ничто иное как smart pointer для .Net! Те из вас, кто писал на С++ поймут о чем я.

Удачи!

2 Февраль 2009 г.

Much of muchness. Other readings.

This is a short note on other possible solutions of forementioned scenarios that these series of articles were devoted to.

Actually there is only one writer that had discovered the same theme. His name is Kent Boogaart. You might know him for his SCSF.WPF Contrib project, that I for one actively use in my projects.

Discovering MAF-enabled solution he breakes it to following pieces:

The most interesting parts here are Event Hub and Service Provider implementations.
Kent's techinque is fully based on MAF advantages (it means he doesn't use any other .Net libraries as I did with WCF in my EventBroker implementation) finishing with the fully independent addins and hosts, that is not true in my case, where host and addin are coupled by service contracts and service provider libraries.

P.S. other articles in this series
P.P.S. Actually there is nothing preventing you from generating proxies to subscription and publication services in addin's and host's projects separately, so there is only one dependency left - my ServiceProvider library. Not a big deal =)

Good luck!

27 Январь 2009 г.

Much of muchness. Part 6. XAppDomain EventBroker.

Алилуйя! Мы добрались до заключительной (и самой интересной) части.
Здесь я расскажу, свой подход к реализации кросс-доменного EventAgregator'а.

Необходимость создания этого монстра диктует плагинная архитектура - различные изолированные модули, ничего не знающие об остальных плагинах, должны уметь оповещать окружающих о каком-то важном событии, причем получатели сообщения, обязательно будут жить в другом, изолированном домене.
Фундаментом моей платформы является CAB - composite application block, содержащим реализацию EventAggregator'а. Как сделать так, чтобы он не был ограничен одним доменом? Никак. Дело в том, что в CAB композиты публикуют события и подписываются на них при помощи ObjectBuilder pipeline, в котором специальная стратегия EventAggregator'а аккумулирует всю эту информацию. В нашем же случае все объекты плагинов будут создаваться MAF pipeline'ом, и доступа к ним у нас не будет. Так как же быть? Использовать WCF - Windows Communication Foundation.

В WCF из коробки нет реализации механизма Publish-Subscribe, но есть все средства для его реализации. Подходов может быть несколько:

За основу решения я взял последний framework от Juval Lowy и расширил его, сделав его contract&topic based, с тем, чтобы интегрировать его с CAB EventAggregator (topic based). Но обо всем по порядку.

Publish-Subscribe framework (С) Juval Lowy IDesign.

Дабы не повторяться, не буду описывать механизмы работы последнего, посему настоятельно рекомендую ознакомиться со статьей, в которой он описан.

Прежде всего небольшим изменениям подвергся контракт сервиса подписки. Теперь ему передается не имя метода, которое надо вызвать у подписчика, а некий объект, выражающий интерес в подписке, который содержит имя метода (contract based) и имя topic (topic based), по которым EventAggregator сможет определить заинтересованность того или иного подписчика в неком событии.





Интерфейс сервиса подписки кросс-доменного EventAggregator'а для моей платформы выглядит следующим образом:

основное его назначение - сообщать WCF инфраструктуре о том, что IEventNotifyCallback - это callback contract для нашего кросс-доменного EventAggregator'а. DomainDTO - это суперкласс для моих DTO.

Сам сервис реализован в классе XAppDomainEventBrokerSubscription.

Он выполняет функции сервиса подписки и связывает инфраструктуру CAB с нашим механизмом. Последнее осуществляется аналогичным CAB'овскому образом - поиск публикаторов событий (по соответствующим аттрибутам) и аккумуляция этих публикаторов в CAB EventAggregator с одной лишь разницой, что вместо подписчика подставляется суррогатный объект, который при получении события опубликует его в нашей кросс-доменной системе (фактически просто пробросит дальше). Интегрируется этот сервис в CAB при помощи специальной стратегии ObjectBuilder'а XAppDomainEventBrokerStrategy.


Публикацией ведает XAppDomainEventBrokerPublication - наследник слегка измененного PublicationService сервиса из framework'а Juval Lowy. Первый реализует callback контракт как контракт сервиса, а последний на основании вызываемого метода и topic находит и вызывает соответствующий метод у соответствующего подписчика.


Но возникает резонный вопрос - а где, собственно, этому сервису передается topic? И куда его вообще воткнуть, если это contract-based framework? Ответ прост. Засунуть в заголовок
сообщения и занимаются этим клиенты сервиса. Их только надо унаследовать от PublisherServiceClient - часть моей инфраструктуры WCF (может быть опишу попозже).


Обратите внимание - у базового клиента сервиса подписки есть свойство EventTopic, с помощью которого мы можем передать это значение в WCF pipeline через расширение канала EventTopicChannelExtension, с одним единственным свойством - EventTopic.
Помните суррогатные объекты, добавлявшиеся в EventBroker CAB? Так вот они публикуют события через EventPublisherClient, передавая ему значение EventTopic из аттрибута.
Как EventTopicChannelExtension попал в канал? Все тот же базовый клиент при создании фабрики каналов добавляет EventTopicFlowBehavior, выполняющий всю работу.




Собственно всю работу делает даже не он, а следующие два джентельмена:



Первый, как следует из его названия, инициаллизирует канал, добавляя в него уже упоминавшееся выше расширение EventTopicChannelExtension для передачи EventTopic второму джентельмену, который собственно добавляет этот topic в заголовок передаваемого сообщения.
Из этого самого заголовка, слегка модифицированный мной, PublicationService вычленяет EventTopic и фильтрует по нему подписчиков.

Вот и все! Теперь все, что нужно сделать по шагам:
1. Создать прокси к сервису подписки и подписаться на интересуемое событие, указав при желании метод контракта, который следует вызвать.
2. Создать прокси к сервису публикации и вызвать, мимикрирующий под контракт подписчика сервис публикации, передав при желании topic.

Не очень надеюсь вы не запутались, потому что сам запутывался не раз.

P.S. ссылки на предыдущие посты в этой серии
Удачи!

5 Декабрь 2008 г.

Much of muchness. Part 5. Handling unhandled exceptions in isolated addin domains, gadget host.


Итак, мы приближаемся к концу серии! Что же нас ждет в этой части?

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

Sabotage! Разрабатывая приложение, открытое для расширения при помощи плагинов, вы можете столкнуться с ситуацией, в которой плагины, разработанные сторонними и не очень добросовестными производителями, будут рушить ваше приложение. Вы спросите зачем нам что-то делать, если они и так уже изолированы в sandbox. Но сложность заключается в том, что если в одном из доменов плагинов произойдет необработанное исключение, то кверху лапками полетит все приложение. Решение мое, конечно же основано на рекоммендациях комманды MAF, о том что делать в таких случаях: И состоит это решение из 2ух частей:
  • UnhandledExceptionHelper - вспомогательный класс для обработки исключительных ситуаций, как следует из его названия;
  • GadgetHost - хост-обертка для гаджетов, машина состояний, изменяющая своё представление в зависимости от состояния плагина/гаджета;
UnhandledExceptionHelper
Интерфейс этого незаменимого помощника выглядит следующим образом:



Следуя названию, его призвание обрабатывать исключительные ситуации в плагинах и не давать при этом основному приложению умереть. Как он это делает? Он обманывает CLR, так что его скорее надо было бы назвать UnhandledExceptionLiar =). Обман заключается в перехвате события необработанного исключения из потока в другом домене и блокировка этого потока до завершения приложения. Это единственный способ, который я нашел для безопасного перехвата исключения в домене плагина без последующего закрытия всего приложения. Следует заметить, что если исключение произойдет в UI потоке, даже не смотря на то, что код выполняется в другом домене (ибо потоки умеют пересекать границы доменов), то приложение рухнет. Это может произойти, например, при настройке параметров плагина или в логике gadget'а. Блокировка умирающего потока происходит при помощи метода Lock(), в котором другой поток (как правило UI), захватывает эксклюзивную блокировку. Кто же вызывает этот метод? Неужели это должен делать разработчик? Нет. Об этом и о многом другом позаботится GadgetHost.

GadgetHost.

GadgetHost это контрол, с помощью которого можно управлять состоянием нашего Gadget'a. Он принимает на вход AddInToken, инициаллизирует Gadet в background потоке, а затем, по вызове функции ShowGadget, как следует из названия, создает гаджет и размещает его как свой контент в UI.



При получении AddInToken GadgetHost создает экземпляр UnhandledExceptionHelper'а, который оповещает последнего о поломке и не дает приложению упасть. При поломке надо срочно привлечь внимание пользователя! И с этой функцией справляется мой FaultedGadgetPlaceholder - просто UserControl с эффектной картинкой (полчаса в Paint.Net =) ).

Вкратце так. Не пишите падающие гаджеты! =)

Удачи!

12 Ноябрь 2008 г.

Much of muchness. Part 4. Publishing MAF addin settings.

Различные виды кризисов задержали продолжение этой серии. Считаю, что надо ускориться и покончить с ней.

Сегодня опишу механизм публикации настроек плагинов. Построен он на инфраструктуре, которую я описывал ранее. Рекомендую для начала ознакомиться с ней, для того чтобы понимать о чем будет идти речь. Вкратце, решение состоит из нескольких интерфейсов, определяющих функциональность, пары классов для хранения настроек (примитивных типов данных, строк и т.п.) и конфигурационного диалога, который управляет отображением всего этого добра. Для плагинов потребовалась небольшая адаптация в виду того, что загружаются они в изолированые sandbox'ы.

Итак, может так статься, что вам понадобится управлять какими-то параметрами плагина. Есть несколько способов это сделать: поместить в UI плагина какой-то элемент
, например кнопку, которая будет запускать форму настроек; а можно делать это централизовано, в единой форме настроек хоста, что лично мне кажется более правильным.

Cоздавая плагины на основе MAF, мы имеем возможность хостить их в отдельных доменах приложения и даже в отдельных процессах, а значит, для того чтобы код хоста мог работать с настройками плагинов, их надо подготовить к передаче через эти границы. Так как настроечные интерфейсы представляют из себя совокупность предоставляемой функциональности, то, очевидно, передача по значению в данном случае не подходит, и наши настроечные объекты должны передаваться по ссылке. Эти требования форсировали разработку новых интерфейсов и классов.



Здесь IAddInTuned - интерфейс, который должны реализовывать классы настроек плагинов. Его дополнительная функция, помимо управления настройками, это передача через границу домена несереализуемого объекта изображения WPF, что реализуется в базовом классе AddInTuned при помощи кодирования изображения в массив байтов и последующего восстановления на стороне хоста.



Большинство свойств и функций, реализуемых этим классом интерфейсов, объявлены виртуальными, для того чтобы конкретные наследники класса могли их переопределить. Задачей представления данного объекта на стороне хоста занимается уже встречавшийся нам в одной из первых частей класс AddInTunedHostWrapper. Он не только восстанавливает изображения после передачи через границу домена, но и пользовательский интерфейс, предназначенный для представления и редактирования настроек.



Передается этот объект настроек и пользовательское представление, как вы наверное помните представлению объекта хоста на стороне плагина HostObject.



который затем по цепочке адаптеров передает эти объекты и в адаптере стороны хоста создает обертку AddInTunedHostWrapper для прозрачного представления настроек хосту.



AddInTunedHostWrapper делегирует все входящие вызовы внутреннему переданному TransparentProxy, который инициирует вызов методов объекта AddInTuned в домене плагина через remoting.

Вот так вкратце выглядит решение для передачи настроек плагинов на сторону хоста.

Ну и приведу пример кода, дабы было понятнее.
Сторона плагина.
Сам плагин.

Настройки плагина.


Представление настроек.

Результат.


Надеюсь я вас не разочаровал убогим примером.


Удачи!

17 Октябрь 2008 г.

System

J.D. Meyer опубликовал Cheat-Sheet - каталог паттернов и guidance'ов, выпущеных Microsoft P'n'P. Рекомендую добавить в избранное, т.к. это отличная возможность систематизировать знания, тем более, что J.D. уже сделал это для нас! Вот линк: http://blogs.msdn.com/jmeier/archive/2008/10/09/cheat-sheet-patterns-practices-catalog-at-a-glance-posted-to-codeplex.aspx

Удачи!