В DevExpress мы тратим много сил на бизнес компоненты для WPF и Silverlight. У нас есть своя линейка контролов, в список которых недавно вошел DXPivotGrid – замена инструмента PivotTable из Excel. В процессе разработки новых компонентов, мы стараемся по-максимуму использовать существующий код. Например, базовые классы от версии PivotGrid для WinForms. Часто это рождает проблемы, с которыми ты не сталкивался, разрабатывая под .NET 2.0. Когда я писал PivotGrid для WPF, мне пришлось решить проблемы с утечками памяти из-за подписки (точнее, «неотписки») на события.
Компания Microsoft ввела понятие weak event'а в .NET 3.0: это вариация стандартных событий, которые не держат прямую ссылку на обработчик события. С другой стороны, обычные обработчики – это две ссылки: одна на объект, а вторая – на метод внутри объекта. Ничего нового, но есть нюанс.
Обработчик события не будет обработан сборщиком мусора, пока он не отпишется от события. Учитывая, что в WPF не применяется интерфейс IDisposable, это превращается в большую проблему.
Как решение, Microsoft предлагает слабо-связанные обработчики событий (weak events — «обработчики слабых событий» в переводе Microsoft). Сборщик мусора может обработать объекты, подписывающиеся на такие события, даже если подписка все ещё существует.
Есть два способа сделать слабое событие: использовать WeakEventManager или RoutedEvent.
Класс WeakEventManager позволяет превратить существующее событие в слабое событие. В моем проекте это было нужно для подписки на события из ядра продукта, которое должно быть совместимо с .NET 2.0.
Слабые события создаются при помощи двух классов: WeakEventManager (диспетчер) и WeakEventListener (прослушиватель). Диспетчер события подписывается на него и передает вызовы прослушивателям, т.е. является прослойкой между источником и получателем, разрывая жесткую связь.
Это шаблон диспетчера событий.
А это сниппет с шаблоном для Visual Studio: https://gist.github.com/777559. Вешается на команду «wem».
Этот шаблон можно использовать для любого диспетчера слабых событий. Вы должны изменить имя класса источника, способ подписки и скорректировать тип параметров в методе EventHandler.
Каждый объект, желающий подписаться на слабое событие, должен реализовать интерфейс IWeakEventListener. Этот интерфейс содержит единственный метод ReceiveWeakEvent. Вы должны проверить тип диспетчера событий, вызвать обработчик и вернуть true. Если вы не можете определить тип диспетчера, вы должны вернуть false. В этом случае будет вызвано исключение System.ExecutionEngineException с несколько непонятным текстом причины ошибки. По нему становится ясно, что в диспетчерах или прослушивателях есть ошибка.
Вот шаблон реализации интерфейса IWeakEventListener:
Перенаправленные события – это инфраструктура для событий, обрабатываемых в XAML (например, в EventTrigger) или проходящих по визуальному дереву.
В MSDN есть хорошая статья про них: Routed Events Overview и поэтому я не хочу повторять их здесь. Но упомяну два их основных недостатка.
Это часть метода UIElement.RaiseEventImpl, вызывающего перенаправленное событие:
Выглядит нормально, пока не взглянуть внутрь методов BuildRouteHelper и InvokeHandlers, каждый из которых длиннее 100 строк. И вся эта сложность для вызова единственного события. Такая сложность делает этот подход неприменимым для часто вызываемых событий.
Вы не сможете создать перенаправленное событие для простого дата объекта, не наследуемого от UIElement. Если ваши технические требования не позволяют этого, то это станет проблемой для вас.
Если вы не ограничены старым кодом и вызовы событий у вас не многочислены, то используйте RoutedEvents. Если вызовов много или у вас есть общий с .NET 2.0 код, то придется писать WeakEventManager для каждого. Громоздко, но придется.
Оба эти способа будут работать в MediumTrust. Если это требование для вас не важно, то ждите решения №3 в следующей серии.
Компания 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 в следующей серии.