Get rid of memory leaks in WPF

    imageAt DevExpress, we spend a lot of effort on business components for WPF and Silverlight . We have our own line of controls, the list of which recently included DXPivotGrid - a replacement of the PivotTable tool from Excel. In the process of developing new components, we try to make the most of existing code. For example, base classes from the version of PivotGrid for WinForms . Often this gives rise to problems that you have not encountered while developing under .NET 2.0. When I wrote PivotGrid for WPF, I had to solve problems with memory leaks due to subscribing (more precisely, “unsubscribing”) to events.


    Microsoft introduced the concept of a weak event in .NET 3.0: it is a variation of standard events that do not hold a direct link to the event handler. On the other hand, ordinary handlers are two references: one to the object, and the second to the method inside the object. Nothing new, but there is a nuance.

    The event handler will not be processed by the garbage collector until it unsubscribes from the event. Given that WPF does not use the IDisposable interface, this turns into a big problem.

    As a solution, Microsoft offers loosely coupled event handlers (weak events - Microsoft's weak event handlers ). The garbage collector can process objects subscribing to such events, even if the subscription still exists.

    There are two ways to make a weak event: use WeakEventManager or RoutedEvent.

    WeakEventManager


    The WeakEventManager class allows you to turn an existing event into a weak event. In my project, this was necessary to subscribe to events from the product core, which should be compatible with .NET 2.0.

    Weak events are created using two classes: WeakEventManager (dispatcher) and WeakEventListener (listener). The event dispatcher subscribes to it and transfers calls to listeners, i.e. is a layer between the source and the recipient, breaking the hard link.

    This is an event dispatcher template.

    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);
        }
    }


    And this is a template snippet for Visual Studio: https://gist.github.com/777559 . Hangs on the wem command.

    This template can be used for any weak event manager. You must change the source class name, the subscription method, and adjust the type of parameters in the EventHandler method.

    Every object that wants to subscribe to a weak event must implement the IWeakEventListener interface . This interface contains a single ReceiveWeakEvent method . You must check the type of event dispatcher, call the handler, and return true. If you cannot determine the type of dispatcher, you must return false. In this case, a System.ExecutionEngineException will be thrown .with some obscure text of the cause of the error. According to it, it becomes clear that there is an error in dispatchers or listeners.

    Here is the IWeakEventListener interface implementation template:

    class MyEventListener : IWeakEventListener {
        bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) {
            if(managerType == typeof(MyEventManager)) {
                // Обработайте событие
                return true;    // Уведомление, что событие корректно обработано
            }
            return false;   // Что-то пошло не так
        }
    }


    Pros and cons

    • This type of weak event is triggered almost instantly.
    • You can determine if there are listeners and not trigger an event if they are not. For me, this was useful in events that need to be called very often or for events with heavy arguments.
    • Can be used for non-UIElements. Useful if you want to use old code from WPF.
    • Very cumbersome to create - each event requires its own dispatcher.


    Routed Events


    Forwarded events are an infrastructure for events that are processed in XAML (for example, in EventTrigger ) or that pass through the visual tree.

    Their main advantages:

    • These are weak events.
    • They can call handlers at several levels of the logical tree.


    There is a good article on MSDN about them: Routed Events Overview and therefore I do not want to repeat them here. But I will mention two of their main disadvantages.

    A difficult call and no information on the number of subscribers

    This is part of the UIElement.RaiseEventImpl method that raises the routed event:

    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);
    }


    It looks fine until you look inside the BuildRouteHelper and InvokeHandlers methods , each of which is longer than 100 lines. And all this complexity to cause a single event. This complexity makes this approach inapplicable to frequently triggered events.

    They can only be added to descendants of the UIElement class.

    You cannot create a routed event for a simple date object that is not inherited from UIElement. If your technical requirements do not allow this, then this will be a problem for you.

    Eventually


    If you are not limited by the old code and the event calls are not numerous, then use RoutedEvents . If you have a lot of calls or you have code common with .NET 2.0, you will have to write WeakEventManager for everyone. Cumbersome, but necessary.

    Both of these methods will work in MediumTrust. If this requirement is not important for you, then wait for decision No. 3 in the next series.

    Also popular now: