diff --git a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs index d915887e4c..b52829a60f 100644 --- a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs +++ b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs @@ -140,6 +140,7 @@ namespace Avalonia.Collections } } + [Obsolete("Causes memory leaks. Use DynamicData or similar instead.")] public static IAvaloniaReadOnlyList CreateDerivedList( this IAvaloniaReadOnlyList collection, Func select) diff --git a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs index 9b6444fc66..9614d079d9 100644 --- a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs +++ b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq; namespace Avalonia.Controls.Utils @@ -15,12 +16,14 @@ namespace Avalonia.Controls.Utils { if (items != null) { - var collection = items as ICollection; - - if (collection != null) + if (items is ICollection collection) { return collection.Count; } + else if (items is IReadOnlyCollection readOnly) + { + return readOnly.Count; + } else { return Enumerable.Count(items.Cast()); diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index b3a8f4745e..4899be2955 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -45,7 +45,15 @@ namespace Avalonia.Diagnostics window.Closed += DevToolsClosed; s_open.Add(root, window); - window.Show(); + + if (root is Window inspectedWindow) + { + window.Show(inspectedWindow); + } + else + { + window.Show(); + } } return Disposable.Create(() => window?.Close()); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs index a7c2997346..38788ef8ee 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.LogicalTree; @@ -9,7 +10,7 @@ namespace Avalonia.Diagnostics.ViewModels public LogicalTreeNode(ILogical logical, TreeNode parent) : base((Control)logical, parent) { - Children = logical.LogicalChildren.CreateDerivedList(x => new LogicalTreeNode(x, this)); + Children = new LogicalTreeNodeCollection(this, logical); } public static LogicalTreeNode[] Create(object control) @@ -17,5 +18,31 @@ namespace Avalonia.Diagnostics.ViewModels var logical = control as ILogical; return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null; } + + internal class LogicalTreeNodeCollection : TreeNodeCollection + { + private readonly ILogical _control; + private IDisposable _subscription; + + public LogicalTreeNodeCollection(TreeNode owner, ILogical control) + : base(owner) + { + _control = control; + } + + public override void Dispose() + { + base.Dispose(); + _subscription?.Dispose(); + } + + protected override void Initialize(AvaloniaList nodes) + { + _subscription = _control.LogicalChildren.ForEachItem( + (i, item) => nodes.Insert(i, new LogicalTreeNode(item, Owner)), + (i, item) => nodes.RemoveAt(i), + () => nodes.Clear()); + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 1d19e1a346..acc3ef16c2 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Avalonia.Controls; using Avalonia.Diagnostics.Models; using Avalonia.Input; @@ -12,6 +13,7 @@ namespace Avalonia.Diagnostics.ViewModels private readonly TreePageViewModel _logicalTree; private readonly TreePageViewModel _visualTree; private readonly EventsPageViewModel _events; + private readonly IDisposable _pointerOverSubscription; private ViewModelBase _content; private int _selectedTab; private string _focusedControl; @@ -25,16 +27,9 @@ namespace Avalonia.Diagnostics.ViewModels _events = new EventsPageViewModel(root); UpdateFocusedControl(); - KeyboardDevice.Instance.PropertyChanged += (s, e) => - { - if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) - { - UpdateFocusedControl(); - } - }; - + KeyboardDevice.Instance.PropertyChanged += KeyboardPropertyChanged; SelectedTab = 0; - root.GetObservable(TopLevel.PointerOverElementProperty) + _pointerOverSubscription = root.GetObservable(TopLevel.PointerOverElementProperty) .Subscribe(x => PointerOverElement = x?.GetType().Name); Console = new ConsoleViewModel(UpdateConsoleContext); } @@ -129,6 +124,8 @@ namespace Avalonia.Diagnostics.ViewModels public void Dispose() { + KeyboardDevice.Instance.PropertyChanged -= KeyboardPropertyChanged; + _pointerOverSubscription.Dispose(); _logicalTree.Dispose(); _visualTree.Dispose(); } @@ -137,5 +134,13 @@ namespace Avalonia.Diagnostics.ViewModels { FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name; } + + private void KeyboardPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) + { + UpdateFocusedControl(); + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index aa27538abc..cb5f5b1fda 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -3,15 +3,15 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; -using Avalonia.Collections; using Avalonia.Controls; using Avalonia.LogicalTree; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { - internal class TreeNode : ViewModelBase + internal class TreeNode : ViewModelBase, IDisposable { + private IDisposable _classesSubscription; private string _classes; private bool _isExpanded; @@ -33,7 +33,7 @@ namespace Avalonia.Diagnostics.ViewModels x => control.Classes.CollectionChanged -= x) .TakeUntil(removed); - classesChanged.Select(_ => Unit.Default) + _classesSubscription = classesChanged.Select(_ => Unit.Default) .StartWith(Unit.Default) .Subscribe(_ => { @@ -49,7 +49,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - public IAvaloniaReadOnlyList Children + public TreeNodeCollection Children { get; protected set; @@ -104,6 +104,12 @@ namespace Avalonia.Diagnostics.ViewModels } } + public void Dispose() + { + _classesSubscription.Dispose(); + Children.Dispose(); + } + private static int IndexOf(IReadOnlyList collection, TreeNode item) { var count = collection.Count; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs new file mode 100644 index 0000000000..8b4f03bd23 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using Avalonia.Collections; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList, IDisposable + { + private AvaloniaList _inner; + + public TreeNodeCollection(TreeNode owner) => Owner = owner; + + public TreeNode this[int index] + { + get + { + EnsureInitialized(); + return _inner[index]; + } + } + + public int Count + { + get + { + EnsureInitialized(); + return _inner.Count; + } + } + + protected TreeNode Owner { get; } + + public event NotifyCollectionChangedEventHandler CollectionChanged + { + add => _inner.CollectionChanged += value; + remove => _inner.CollectionChanged -= value; + } + + public event PropertyChangedEventHandler PropertyChanged + { + add => _inner.PropertyChanged += value; + remove => _inner.PropertyChanged -= value; + } + + public virtual void Dispose() + { + if (_inner is object) + { + foreach (var node in _inner) + { + node.Dispose(); + } + } + } + + public IEnumerator GetEnumerator() + { + EnsureInitialized(); + return _inner.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + protected abstract void Initialize(AvaloniaList nodes); + + private void EnsureInitialized() + { + if (_inner is null) + { + _inner = new AvaloniaList(); + Initialize(_inner); + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index 38ac88a83c..ec48cff399 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -62,7 +62,15 @@ namespace Avalonia.Diagnostics.ViewModels } } - public void Dispose() => _details?.Dispose(); + public void Dispose() + { + foreach (var node in Nodes) + { + node.Dispose(); + } + + _details?.Dispose(); + } public TreeNode FindNode(IControl control) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index 5383cb2b68..bc40edf477 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Collections; using Avalonia.Styling; using Avalonia.VisualTree; @@ -9,16 +10,7 @@ namespace Avalonia.Diagnostics.ViewModels public VisualTreeNode(IVisual visual, TreeNode parent) : base(visual, parent) { - var host = visual as IVisualTreeHost; - - if (host?.Root == null) - { - Children = visual.VisualChildren.CreateDerivedList(x => new VisualTreeNode(x, this)); - } - else - { - Children = new AvaloniaList(new[] { new VisualTreeNode(host.Root, this) }); - } + Children = new VisualTreeNodeCollection(this, visual); if ((Visual is IStyleable styleable)) { @@ -33,5 +25,30 @@ namespace Avalonia.Diagnostics.ViewModels var visual = control as IVisual; return visual != null ? new[] { new VisualTreeNode(visual, null) } : null; } + + internal class VisualTreeNodeCollection : TreeNodeCollection + { + private readonly IVisual _control; + private IDisposable _subscription; + + public VisualTreeNodeCollection(TreeNode owner, IVisual control) + : base(owner) + { + _control = control; + } + + public override void Dispose() + { + _subscription?.Dispose(); + } + + protected override void Initialize(AvaloniaList nodes) + { + _subscription = _control.VisualChildren.ForEachItem( + (i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)), + (i, item) => nodes.RemoveAt(i), + () => nodes.Clear()); + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 3abdb5034a..10861538ae 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -14,8 +14,8 @@ namespace Avalonia.Diagnostics.Views { internal class MainWindow : Window, IStyleHost { + private readonly IDisposable _keySubscription; private TopLevel _root; - private IDisposable _keySubscription; public MainWindow() { @@ -33,8 +33,22 @@ namespace Avalonia.Diagnostics.Views { if (_root != value) { + if (_root != null) + { + _root.Closed -= RootClosed; + } + _root = value; - DataContext = new MainViewModel(value); + + if (_root != null) + { + _root.Closed += RootClosed; + DataContext = new MainViewModel(value); + } + else + { + DataContext = null; + } } } } @@ -45,6 +59,8 @@ namespace Avalonia.Diagnostics.Views { base.OnClosed(e); _keySubscription.Dispose(); + _root.Closed -= RootClosed; + _root = null; ((MainViewModel)DataContext)?.Dispose(); } @@ -70,5 +86,7 @@ namespace Avalonia.Diagnostics.Views } } } + + private void RootClosed(object sender, EventArgs e) => Close(); } }