From 2bbef79872be0e8ede6d0a89368933d5aa62f69c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 23 Dec 2021 17:31:48 +0100 Subject: [PATCH] Added PanelChildren. After this change the `Panel.Children` collection is also used as the logical/visual child store, preventing the need to store the children of a panel 3 times. --- .../Controls/LogicalVisualChildren.cs | 6 - src/Avalonia.Controls/Grid.cs | 7 +- src/Avalonia.Controls/Panel.cs | 64 +----- src/Avalonia.Controls/PanelChildren.cs | 191 ++++++++++++++++++ .../Repeater/ItemsRepeater.cs | 2 +- .../VirtualizingStackPanel.cs | 11 +- 6 files changed, 213 insertions(+), 68 deletions(-) create mode 100644 src/Avalonia.Controls/PanelChildren.cs diff --git a/src/Avalonia.Base/Controls/LogicalVisualChildren.cs b/src/Avalonia.Base/Controls/LogicalVisualChildren.cs index 933b1971e4..bcbd21adbe 100644 --- a/src/Avalonia.Base/Controls/LogicalVisualChildren.cs +++ b/src/Avalonia.Base/Controls/LogicalVisualChildren.cs @@ -119,12 +119,6 @@ namespace Avalonia.Controls case NotifyCollectionChangedAction.Reset: throw new NotSupportedException("Reset should not be signaled on LogicalChildren collection"); } - - // HACK: Remove this. - if (_owner is IVisual v) - { - v?.GetVisualRoot()?.Renderer?.RecalculateChildren(v); - } } private void OnVisualCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 564cc93a39..268e0115ef 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -570,13 +570,10 @@ namespace Avalonia.Controls return (arrangeSize); } - /// - /// - /// - protected override void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) + protected internal override void InvalidateOnChildrenChanged() { CellsStructureDirty = true; - base.ChildrenChanged(sender, e); + base.InvalidateOnChildrenChanged(); } /// diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 58a45cdceb..dfc87a64ff 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -1,13 +1,7 @@ using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using Avalonia.Collections; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; -using Avalonia.Styling; -using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -41,14 +35,13 @@ namespace Avalonia.Controls /// public Panel() { - Children.CollectionChanged += ChildrenChanged; } /// /// Gets the children of the . /// [Content] - public new Controls Children { get; } = new Controls(); + public new Controls Children => (Controls)base.Children; /// /// Gets or Sets Panel background brush. @@ -81,6 +74,8 @@ namespace Avalonia.Controls base.Render(context); } + protected override ILogicalVisualChildren CreateChildren() => new PanelChildren(this); + /// /// Marks a property on a child as affecting the parent panel's arrangement. /// @@ -108,56 +103,19 @@ namespace Avalonia.Controls } /// - /// Called when the collection changes. + /// Called in response to the collection changing in order to allow + /// the panel to carry out any needed invalidation. /// - /// The event sender. - /// The event args. - protected virtual void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) + protected internal virtual void InvalidateOnChildrenChanged() { - List controls; - var logicalChildren = (IAvaloniaList)LogicalChildren; - var visualChildren = (IAvaloniaList)VisualChildren; - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - controls = e.NewItems.OfType().ToList(); - logicalChildren.InsertRange(e.NewStartingIndex, controls); - visualChildren.InsertRange(e.NewStartingIndex, e.NewItems.OfType()); - break; - - case NotifyCollectionChangedAction.Move: - logicalChildren.MoveRange(e.OldStartingIndex, e.OldItems.Count, e.NewStartingIndex); - visualChildren.MoveRange(e.OldStartingIndex, e.OldItems.Count, e.NewStartingIndex); - break; - - case NotifyCollectionChangedAction.Remove: - controls = e.OldItems.OfType().ToList(); - logicalChildren.RemoveAll(controls); - visualChildren.RemoveAll(e.OldItems.OfType()); - break; - - case NotifyCollectionChangedAction.Replace: - for (var i = 0; i < e.OldItems.Count; ++i) - { - var index = i + e.OldStartingIndex; - var child = (IControl)e.NewItems[i]; - LogicalChildren[index] = child; - VisualChildren[index] = child; - } - break; - - case NotifyCollectionChangedAction.Reset: - throw new NotSupportedException(); - } - - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); - InvalidateMeasureOnChildrenChanged(); + OnChildIndexChanged(new ChildIndexChangedEventArgs()); + InvalidateMeasure(); + VisualRoot?.Renderer?.RecalculateChildren(this); } - private protected virtual void InvalidateMeasureOnChildrenChanged() + protected void OnChildIndexChanged(ChildIndexChangedEventArgs e) { - InvalidateMeasure(); + _childIndexChanged?.Invoke(this, e); } private static void AffectsParentArrangeInvalidate(AvaloniaPropertyChangedEventArgs e) diff --git a/src/Avalonia.Controls/PanelChildren.cs b/src/Avalonia.Controls/PanelChildren.cs new file mode 100644 index 0000000000..0672660562 --- /dev/null +++ b/src/Avalonia.Controls/PanelChildren.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.LogicalTree; +using Avalonia.VisualTree; + +#nullable enable + +namespace Avalonia.Controls +{ + /// + /// A collection held by a . + /// + internal class PanelChildren : Controls, + ILogicalVisualChildren, + IList, + IList + { + private readonly Panel _owner; + + public PanelChildren(Panel owner) + { + _owner = owner; + Validate = ValidateItem; + } + + public override IControl this[int index] + { + get => base[index]; + set + { + var oldValue = base[index]; + + if (oldValue != value) + { + ClearParent(oldValue); + base[index] = value; + } + } + } + + public override void Add(IControl item) + { + base.Add(item); + _owner.InvalidateOnChildrenChanged(); + } + + public override void Insert(int index, IControl item) + { + base.Insert(index, item); + _owner.InvalidateOnChildrenChanged(); + } + + public override void InsertRange(int index, IEnumerable items) + { + base.InsertRange(index, items); + _owner.InvalidateOnChildrenChanged(); + } + + public override bool Remove(IControl item) + { + var result = base.Remove(item); + + if (result) + { + ClearParent(item); + _owner.InvalidateOnChildrenChanged(); + } + + return result; + } + + public override void RemoveAt(int index) + { + ClearParent(this[index]); + base.RemoveAt(index); + _owner.InvalidateOnChildrenChanged(); + } + + public override void RemoveRange(int index, int count) + { + if (count > 0) + { + base.RemoveRange(index, count); + _owner.InvalidateOnChildrenChanged(); + } + } + + public override void Move(int oldIndex, int newIndex) + { + base.Move(oldIndex, newIndex); + _owner.InvalidateOnChildrenChanged(); + } + + public override void MoveRange(int oldIndex, int count, int newIndex) + { + base.MoveRange(oldIndex, count, newIndex); + _owner.InvalidateOnChildrenChanged(); + } + + public override void Clear() + { + foreach (var item in this) + { + if (item.LogicalParent == _owner) + ((ISetLogicalParent)item).SetParent(null); + ((ISetVisualParent)item).SetParent(null); + } + + base.Clear(); + _owner.InvalidateOnChildrenChanged(); + } + + ILogical IList.this[int index] + { + get => this[index]; + set => this[index] = (IControl)value; + } + + IVisual IList.this[int index] + { + get => this[index]; + set => this[index] = (IControl)value; + } + + IReadOnlyList ILogicalVisualChildren.Logical => this; + IReadOnlyList ILogicalVisualChildren.Visual => this; + IList ILogicalVisualChildren.LogicalMutable => this; + IList ILogicalVisualChildren.VisualMutable => this; + bool ICollection.IsReadOnly => true; + bool ICollection.IsReadOnly => true; + + void ILogicalVisualChildren.AddLogicalChildrenChangedHandler(NotifyCollectionChangedEventHandler handler) + { + CollectionChanged += handler; + } + + void ILogicalVisualChildren.RemoveLogicalChildrenChangedHandler(NotifyCollectionChangedEventHandler handler) + { + CollectionChanged -= handler; + } + + void ILogicalVisualChildren.AddVisualChildrenChangedHandler(NotifyCollectionChangedEventHandler handler) + { + CollectionChanged += handler; + } + + void ILogicalVisualChildren.RemoveVisualChildrenChangedHandler(NotifyCollectionChangedEventHandler handler) + { + CollectionChanged -= handler; + } + + void ICollection.Add(ILogical item) => Add((IControl)item); + void ICollection.Add(IVisual item) => Add((IControl)item); + bool ICollection.Contains(ILogical item) => item is IControl c ? Contains(c) : false; + bool ICollection.Contains(IVisual item) => item is IControl c ? Contains(c) : false; + void ICollection.CopyTo(ILogical[] array, int arrayIndex) => ((ICollection)this).CopyTo(array, arrayIndex); + void ICollection.CopyTo(IVisual[] array, int arrayIndex) => ((ICollection)this).CopyTo(array, arrayIndex); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + int IList.IndexOf(ILogical item) => item is IControl c ? IndexOf(c) : -1; + int IList.IndexOf(IVisual item) => item is IControl c ? IndexOf(c) : -1; + void IList.Insert(int index, ILogical item) => Insert(index, (IControl)item); + void IList.Insert(int index, IVisual item) => Insert(index, (IControl)item); + bool ICollection.Remove(ILogical item) => Remove((IControl)item); + bool ICollection.Remove(IVisual item) => Remove((IControl)item); + + private void ClearParent(IControl? c) + { + if (c is null) + return; + if (c.LogicalParent == _owner) + ((ISetLogicalParent)c).SetParent(null); + ((ISetVisualParent)c).SetParent(null); + } + + private void ValidateItem(IControl c) + { + _ = c ?? throw new ArgumentException("Cannot add null to Panel.Children."); + if (c.VisualParent is not null) + throw new InvalidOperationException("The control already has a visual parent."); + + // It's a bit naughty to do this during validation, but saves iterating the + // added items multiple times in the case of an AddRange. + if (c.LogicalParent is null) + ((ISetLogicalParent)c).SetParent(_owner); + ((ISetVisualParent)c).SetParent(_owner); + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index ecc0fa3a48..edcf4eab13 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -288,7 +288,7 @@ namespace Avalonia.Controls return result; } - private protected override void InvalidateMeasureOnChildrenChanged() + protected internal override void InvalidateOnChildrenChanged() { // Don't invalidate measure when children change. } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index da8bbe4fcf..4602dca73c 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -78,6 +78,13 @@ namespace Avalonia.Controls _forceRemeasure = true; } + protected override ILogicalVisualChildren CreateChildren() + { + var result = base.CreateChildren(); + result.AddLogicalChildrenChangedHandler(ChildrenChanged); + return result; + } + protected override Size MeasureOverride(Size availableSize) { if (_forceRemeasure || availableSize != ((ILayoutable)this).PreviousMeasure) @@ -103,10 +110,8 @@ namespace Avalonia.Controls return result; } - protected override void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) + private void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) { - base.ChildrenChanged(sender, e); - switch (e.Action) { case NotifyCollectionChangedAction.Add: