Search code examples
c#wpfdisposesubscription

How to Dispose subscriptions in a UserControl if you can change the VisualParent


I have a FooUserControl which subscribes on it's LoadedEvent. This UserControl can be placed else where on your gui (on any Window or inside of any Control). To avoid leaks, I have implemented some kind of disposing.

The problem with this solution:

If you put the FooUserControl on a TabItem of a TabControl and change the tabs, the OnVisualParentChanged() is called and the subscription is disposed. If I wouldn't add this method, and you close the TabItem the subscription is still alive in background, although the UserControl can be disposed. The same problem will occur with a page

public class FooUserControl : UserControl
{
    private IDisposable _Subscription;
    public FooUserControl()
    {
        Loaded += _OnLoaded;
    }

    private void _OnLoaded(object sender, RoutedEventArgs e)
    {
        // avoid multiple subscribing
        Loaded -= _OnLoaded;

        // add hook to parent window to dispose subscription
        var parentWindow = Window.GetWindow(this);
        if(parentWindow != null)
            parentWindow.Closed += _ParentWindowOnClosed;

        _Subscription = MyObservableInstance.Subscribe(...);
    }

    private void _ParentWindowOnClosed(object? sender, EventArgs e)
    {
        _Dispose();
    }

    // check if the parent visual has been changed
    // can happen if you use the control on a page
    protected override void OnVisualParentChanged(DependencyObject oldParent)
    {
        if (oldParent != null)
        {
            _Dispose();
        }
        base.OnVisualParentChanged(oldParent);
    }

    private void _Dispose()
    {
        _Subscription?.Dispose();
    }
}

Solution

  • I finally found a solution. In the UnLoaded event, I scan the Logical/VisualTree if there is still an instance present or not.

    Since there is no real disposing mechanism in wpf, I have adopted this solution. I'm open for a better solution!

    FooUserControl

    public class FooUserControl : UserControl
    {
        private IDisposable _Subscription;
        private Window _ParentWindow;
    
    
        public FooUserControl()
        {
            Loaded += _OnLoaded;
            Unloaded += _OnUnloaded;
        }
    
        private void _OnLoaded(object sender, RoutedEventArgs e)
        {
            // avoid multiple subscribing
            Loaded -= _OnLoaded;
    
            // add hook to parent window to dispose subscription
            _ParentWindow = Window.GetWindow(this);
            _ParentWindow.Closed += _ParentWindowOnClosed;
    
            _Subscription = MyObservableInstance.Subscribe(...);
        }
    
        private void _OnUnloaded(object sender, RoutedEventArgs e)
        {
            // look in logical and visual tree if the control has been removed
            if (_ParentWindow.FindChildByUid<NLogViewer>(Uid) == null)
            {
                _Dispose();
            }
        }
    
        private void _ParentWindowOnClosed(object? sender, EventArgs e)
        {
            _Dispose();
        }
    
        private void _Dispose()
        {
            _Subscription?.Dispose();
        }
    }
    

    DependencyObjectExtensions

    public static class DependencyObjectExtensions
    {
        /// <summary>
        /// Analyzes both visual and logical tree in order to find all elements of a given
        /// type that are descendants of the <paramref name="source"/> item.
        /// </summary>
        /// <typeparam name="T">The type of the queried items.</typeparam>
        /// <param name="source">The root element that marks the source of the search. If the
        /// source is already of the requested type, it will not be included in the result.</param>
        /// <param name="uid">The UID of the <see cref="UIElement"/></param>
        /// <returns>All descendants of <paramref name="source"/> that match the requested type.</returns>
        public static T FindChildByUid<T>(this DependencyObject source, string uid) where T : UIElement
        {
            if (source != null)
            {
                var childs = GetChildObjects(source);
                foreach (DependencyObject child in childs)
                {
                    //analyze if children match the requested type
                    if (child != null && child is T dependencyObject && dependencyObject.Uid.Equals(uid))
                    {
                        return dependencyObject;
                    }
    
                    var descendant = FindChildByUid<T>(child, uid);
                    if (descendant != null)
                        return descendant;
                }
            }
    
            return null;
        }
    
        /// <summary>
        /// This method is an alternative to WPF's
        /// <see cref="VisualTreeHelper.GetChild"/> method, which also
        /// supports content elements. Keep in mind that for content elements,
        /// this method falls back to the logical tree of the element.
        /// </summary>
        /// <param name="parent">The item to be processed.</param>
        /// <returns>The submitted item's child elements, if available.</returns>
        public static IEnumerable<DependencyObject> GetChildObjects(this DependencyObject parent)
        {
            if (parent == null) yield break;
    
            if (parent is ContentElement || parent is FrameworkElement)
            {
                //use the logical tree for content / framework elements
                foreach (object obj in LogicalTreeHelper.GetChildren(parent))
                {
                    var depObj = obj as DependencyObject;
                    if (depObj != null) yield return (DependencyObject) obj;
                }
            }
            else
            {
                //use the visual tree per default
                int count = VisualTreeHelper.GetChildrenCount(parent);
                for (int i = 0; i < count; i++)
                {
                    yield return VisualTreeHelper.GetChild(parent, i);
                }
            }
        }
    }