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/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/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index aa27538abc..bf7c62f5fc 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -3,14 +3,13 @@ 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 string _classes; private bool _isExpanded; @@ -49,7 +48,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - public IAvaloniaReadOnlyList Children + public TreeNodeCollection Children { get; protected set; @@ -104,6 +103,11 @@ namespace Avalonia.Diagnostics.ViewModels } } + public void 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()); + } + } } }