Как удалить подписки в UserControl, если вы можете изменить VisualParent - PullRequest
0 голосов
/ 19 февраля 2020

У меня есть FooUserControl, который подписывается на LoadedEvent. Этот UserControl может быть размещен в другом месте на вашем gui (на любом Window или внутри любого Control). Чтобы избежать утечек, я реализовал некоторую утилизацию.

Проблема с этим решением:

Если вы поставите FooUserControl на TabItem TabControl и измените вкладки, вызывается OnVisualParentChanged() и подписка удаляется. Если я не добавлю этот метод, и вы закроете TabItem, подписка все еще будет работать в фоновом режиме, хотя UserControl можно удалить. Та же проблема возникнет с 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();
    }
}

1 Ответ

0 голосов
/ 19 февраля 2020

Я наконец нашел решение. В событии UnLoaded я сканирую Logical/VisualTree, если экземпляр все еще присутствует или нет.

Поскольку в wpf нет реального механизма disposing, я принял это решение. Я открыт для лучшего решения!

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