
Компания Microsoft ввела понятие weak event'а в .NET 3.0: это вариация стандартных событий, которые не держат прямую ссылку на обработчик события. С другой стороны, обычные обработчики – это две ссылки: одна на объект, а вторая – на метод внутри объекта. Ничего нового, но есть нюанс.
Обработчик события не будет обработан сборщиком мусора, пока он не отпишется от события. Учитывая, что в WPF не применяется интерфейс IDisposable, это превращается в большую проблему.
Как решение, Microsoft предлагает слабо-связанные обработчики событий (weak events — «обработчики слабых событий» в переводе Microsoft). Сборщик мусора может обработать объекты, подписывающиеся на такие события, даже если подписка все ещё существует.
Есть два способа сделать слабое событие: использовать WeakEventManager или RoutedEvent.
WeakEventManager
Класс WeakEventManager позволяет превратить существующее событие в слабое событие. В моем проекте это было нужно для подписки на события из ядра продукта, которое должно быть совместимо с .NET 2.0.
Слабые события создаются при помощи двух классов: WeakEventManager (диспетчер) и WeakEventListener (прослушиватель). Диспетчер события подписывается на него и передает вызовы прослушивателям, т.е. является прослойкой между источником и получателем, разрывая жесткую связь.
Это шаблон диспетчера событий.
public class MyEventManager : WeakEventManager {
static MyEventManager CurrentManager {
get {
// Создание статического диспетчера событий
Type managerType = typeof(MyEventManager);
MyEventManager currentManager =
(MyEventManager)WeakEventManager.GetCurrentManager(managerType);
if(currentManager == null) {
currentManager = new MyEventManager();
WeakEventManager.SetCurrentManager(managerType, currentManager);
}
return currentManager;
}
}
MyEventManager() { }
// Измените "T" на действительный тип источника событий
public static void AddListener(T source, IWeakEventListener listener) {
CurrentManager.ProtectedAddListener(source, listener);
}
// Измените "T" на действительный тип источника событий
public static void RemoveListener(T source, IWeakEventListener listener) {
CurrentManager.ProtectedRemoveListener(source, listener);
}
protected override void StartListening(object source) {
// Подпишитесь на событие
// Например, ((T)source).Event += EventHandler;
}
protected override void StopListening(object source) {
// Отпишитесь от события
// Например, ((T)source).Event -= EventHandler;
}
// Обработчик события – измените тип аргумента
// на корректный для вашего события
void EventHandler(object sender, EventArgs e) {
base.DeliverEvent(sender, e);
}
}
А это сниппет с шаблоном для Visual Studio: https://gist.github.com/777559. Вешается на команду «wem».
Этот шаблон можно использовать для любого диспетчера слабых событий. Вы должны изменить имя класса источника, способ подписки и скорректировать тип параметров в методе EventHandler.
Каждый объект, желающий подписаться на слабое событие, должен реализовать интерфейс IWeakEventListener. Этот интерфейс содержит единственный метод ReceiveWeakEvent. Вы должны проверить тип диспетчера событий, вызвать обработчик и вернуть true. Если вы не можете определить тип диспетчера, вы должны вернуть false. В этом случае будет вызвано исключение System.ExecutionEngineException с несколько непонятным текстом причины ошибки. По нему становится ясно, что в диспетчерах или прослушивателях есть ошибка.
Вот шаблон реализации интерфейса IWeakEventListener:
class MyEventListener : IWeakEventListener {
bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) {
if(managerType == typeof(MyEventManager)) {
// Обработайте событие
return true; // Уведомление, что событие корректно обработано
}
return false; // Что-то пошло не так
}
}
Плюсы и минусы
- Этот тип слабых событий вызывается практически мгновенно
- Вы можете определить есть ли прослушиватели и не вызывать событие, если их нет. Для меня это было полезно в событиях, которые нужно вызывать очень часто или для событий с тяжелыми аргументами.
- Могут использоваться для не UIElements. Полезно, если вы хотите использовать старый код из WPF.
- Очень громоздки в создании – каждое событие требует своего диспетчера.
Routed Events (Перенаправленные события)
Перенаправленные события – это инфраструктура для событий, обрабатываемых в XAML (например, в EventTrigger) или проходящих по визуальному дереву.
Их главные преимущества:
- Это слабые события.
- Они могут вызывать обработчики на нескольких уровнях логического дерева.
В MSDN есть хорошая статья про них: Routed Events Overview и поэтому я не хочу повторять их здесь. Но упомяну два их основных недостатка.
Тяжелый вызов и нет информации о количестве подписчиков
Это часть метода UIElement.RaiseEventImpl, вызывающего перенаправленное событие:
internal static void RaiseEventImpl(DependencyObject sender, RoutedEventArgs args)
{
EventRoute route = EventRouteFactory.FetchObject(args.RoutedEvent);
if (TraceRoutedEvent.IsEnabled)
{
TraceRoutedEvent.Trace(TraceEventType.Start, TraceRoutedEvent.RaiseEvent, new object[] { args.RoutedEvent, sender, args, args.Handled });
}
try
{
args.Source = sender;
BuildRouteHelper(sender, route, args);
route.InvokeHandlers(sender, args);
args.Source = args.OriginalSource;
}
finally
{
if (TraceRoutedEvent.IsEnabled)
{
TraceRoutedEvent.Trace(TraceEventType.Stop, TraceRoutedEvent.RaiseEvent, new object[] { args.RoutedEvent, sender, args, args.Handled });
}
}
EventRouteFactory.RecycleObject(route);
}
Выглядит нормально, пока не взглянуть внутрь методов BuildRouteHelper и InvokeHandlers, каждый из которых длиннее 100 строк. И вся эта сложность для вызова единственного события. Такая сложность делает этот подход неприменимым для часто вызываемых событий.
Они могут быть добавлены только в наследников класса UIElement.
Вы не сможете создать перенаправленное событие для простого дата объекта, не наследуемого от UIElement. Если ваши технические требования не позволяют этого, то это станет проблемой для вас.
В итоге
Если вы не ограничены старым кодом и вызовы событий у вас не многочислены, то используйте RoutedEvents. Если вызовов много или у вас есть общий с .NET 2.0 код, то придется писать WeakEventManager для каждого. Громоздко, но придется.
Оба эти способа будут работать в MediumTrust. Если это требование для вас не важно, то ждите решения №3 в следующей серии.