From 394a5be4026afcf41202d876fb29dcf4beb8ab7c Mon Sep 17 00:00:00 2001 From: Lubomir Tetak Date: Fri, 17 Dec 2021 14:52:22 +0100 Subject: [PATCH 01/76] OSX handle CMD+key up combinations in Avalonia --- native/Avalonia.Native/src/OSX/app.mm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 79175d9ff1..05b129baca 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -73,6 +73,11 @@ ComPtr _events; _isHandlingSendEvent = true; @try { [super sendEvent: event]; + if ([event type] == NSEventTypeKeyUp && ([event modifierFlags] & NSEventModifierFlagCommand)) + { + [[self keyWindow] sendEvent:event]; + } + } @finally { _isHandlingSendEvent = oldHandling; } From 213cc3429b7d91a2e84c1c2978a28d82ccb2f1ab Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 19:27:34 +0300 Subject: [PATCH 02/76] Visual now uses WeakEvent too --- src/Avalonia.Visuals/Visual.cs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 324b253a0f..41c9f76dbc 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -91,11 +91,17 @@ namespace Avalonia /// public static readonly StyledProperty ZIndexProperty = AvaloniaProperty.Register(nameof(ZIndex)); + + private static readonly WeakEvent InvalidatedWeakEvent = + WeakEvent.Register( + (s, h) => s.Invalidated += h, + (s, h) => s.Invalidated -= h); private Rect _bounds; private TransformedBounds? _transformedBounds; private IRenderRoot? _visualRoot; private IVisual? _visualParent; + private WeakEventSubscriber? _affectsRenderWeakSubscriber; /// /// Initializes static members of the class. @@ -352,12 +358,21 @@ namespace Avalonia { if (e.OldValue is IAffectsRender oldValue) { - WeakEventHandlerManager.Unsubscribe(oldValue, nameof(oldValue.Invalidated), sender.AffectsRenderInvalidated); + if (sender._affectsRenderWeakSubscriber != null) + InvalidatedWeakEvent.Unsubscribe(oldValue, sender._affectsRenderWeakSubscriber); } if (e.NewValue is IAffectsRender newValue) { - WeakEventHandlerManager.Subscribe(newValue, nameof(newValue.Invalidated), sender.AffectsRenderInvalidated); + if (sender._affectsRenderWeakSubscriber == null) + { + sender._affectsRenderWeakSubscriber = new WeakEventSubscriber(); + sender._affectsRenderWeakSubscriber.Event += delegate + { + sender.InvalidateVisual(); + }; + } + InvalidatedWeakEvent.Subscribe(newValue, sender._affectsRenderWeakSubscriber); } sender.InvalidateVisual(); @@ -608,8 +623,6 @@ namespace Avalonia OnVisualParentChanged(old, value); } - private void AffectsRenderInvalidated(object? sender, EventArgs e) => InvalidateVisual(); - /// /// Called when the collection changes. /// From 7e1d9dbc72eb9c6371f8bf33800caab19916cf20 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 19:28:49 +0300 Subject: [PATCH 03/76] Optimized WeakEvent (9332ms to 5ms using #6660 bench) --- src/Avalonia.Base/Utilities/WeakEvent.cs | 85 ++++++++++++++++++++---- 1 file changed, 72 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index 0b32015a8a..21c165afae 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; @@ -36,7 +37,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { if (!_subscriptions.TryGetValue(target, out var subscription)) _subscriptions.Add(target, subscription = new Subscription(this, target)); - subscription.Add(new WeakReference>(subscriber)); + subscription.Add(subscriber); } public void Unsubscribe(TSender target, IWeakEventSubscriber subscriber) @@ -51,11 +52,61 @@ public class WeakEvent : WeakEvent where TEventArgs : Event private readonly TSender _target; private readonly Action _compact; - private WeakReference>?[] _data = - new WeakReference>[16]; + struct Entry + { + WeakReference>? _reference; + int _hashCode; + + public Entry(IWeakEventSubscriber r) + { + if (r == null) + { + _reference = null; + _hashCode = 0; + return; + } + + _hashCode = r.GetHashCode(); + _reference = new WeakReference>(r); + } + + public bool IsEmpty + { + get + { + if (_reference == null) + return false; + if (_reference.TryGetTarget(out var target)) + return true; + _reference = null; + return false; + } + } + + public bool TryGetTarget([MaybeNullWhen(false)]out IWeakEventSubscriber target) + { + if (_reference == null) + { + target = null!; + return false; + } + return _reference.TryGetTarget(out target); + } + + public bool Equals(IWeakEventSubscriber r) + { + if (_reference == null || r.GetHashCode() != _hashCode) + return false; + return _reference.TryGetTarget(out var target) && target == r; + } + } + + private Entry[] _data = + new Entry[16]; private int _count; private readonly Action _unsubscribe; private bool _compactScheduled; + private int _removedSinceLastCompact; public Subscription(WeakEvent ev, TSender target) { @@ -71,17 +122,17 @@ public class WeakEvent : WeakEvent where TEventArgs : Event _ev._subscriptions.Remove(_target); } - public void Add(WeakReference> s) + public void Add(IWeakEventSubscriber s) { if (_count == _data.Length) { //Extend capacity - var extendedData = new WeakReference>?[_data.Length * 2]; + var extendedData = new Entry[_data.Length * 2]; Array.Copy(_data, extendedData, _data.Length); _data = extendedData; } - _data[_count] = s; + _data[_count] = new(s); _count++; } @@ -93,16 +144,21 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { var reference = _data[c]; - if (reference != null && reference.TryGetTarget(out var instance) && instance == s) + if (reference.Equals(s)) { - _data[c] = null; + _data[c] = default; removed = true; + break; } } if (removed) { + _removedSinceLastCompact++; ScheduleCompact(); + + if (_removedSinceLastCompact > 500) + Compact(); } } @@ -116,18 +172,21 @@ public class WeakEvent : WeakEvent where TEventArgs : Event void Compact() { + if(!_compactScheduled || _removedSinceLastCompact == 0) + return; _compactScheduled = false; + _removedSinceLastCompact = 0; int empty = -1; for (var c = 0; c < _count; c++) { - var r = _data[c]; + ref var r = ref _data[c]; //Mark current index as first empty - if (r == null && empty == -1) + if (r.IsEmpty && empty == -1) empty = c; //If current element isn't null and we have an empty one - if (r != null && empty != -1) + if (!r.IsEmpty && empty != -1) { - _data[c] = null; + _data[c] = default; _data[empty] = r; empty++; } @@ -145,7 +204,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event for (var c = 0; c < _count; c++) { var r = _data[c]; - if (r?.TryGetTarget(out var sub) == true) + if (r.TryGetTarget(out var sub)) sub!.OnEvent(_target, _ev, eventArgs); else needCompact = true; From dca2ca5940f0c768529f97b89535ed172ef44403 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 19:29:14 +0300 Subject: [PATCH 04/76] Don't implement IWeakEventSubscriber directly on public types --- .../Utilities/IWeakEventSubscriber.cs | 10 +++++++ .../Repeater/ItemsRepeater.cs | 30 ++++++++++--------- src/Avalonia.Controls/TopLevel.cs | 16 +++++----- src/Avalonia.Visuals/Media/Pen.cs | 25 +++++++++------- 4 files changed, 49 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs index e48c0cb111..2a24376592 100644 --- a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs +++ b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs @@ -9,4 +9,14 @@ namespace Avalonia.Utilities; public interface IWeakEventSubscriber where TEventArgs : EventArgs { void OnEvent(object? sender, WeakEvent ev, TEventArgs e); +} + +public class WeakEventSubscriber : IWeakEventSubscriber where TEventArgs : EventArgs +{ + public event Action? Event; + + void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, TEventArgs e) + { + + } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index b40cf26df5..6d2f4144eb 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls /// Represents a data-driven collection control that incorporates a flexible layout system, /// custom views, and virtualization. /// - public class ItemsRepeater : Panel, IChildIndexProvider, IWeakEventSubscriber + public class ItemsRepeater : Panel, IChildIndexProvider { /// /// Defines the property. @@ -68,6 +68,7 @@ namespace Avalonia.Controls private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs; private ItemsRepeaterElementClearingEventArgs _elementClearingArgs; private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs; + private WeakEventSubscriber _layoutWeakSubscriber = new(); /// /// Initializes a new instance of the class. @@ -77,6 +78,15 @@ namespace Avalonia.Controls _viewManager = new ViewManager(this); _viewportManager = new ViewportManager(this); KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once); + + _layoutWeakSubscriber.Event += (_, ev, e) => + { + if (ev == AttachedLayout.ArrangeInvalidatedWeakEvent) + InvalidateArrange(); + else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent) + InvalidateMeasure(); + }; + OnLayoutChanged(null, Layout); } @@ -723,8 +733,8 @@ namespace Avalonia.Controls { oldValue.UninitializeForContext(LayoutContext); - AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, this); - AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, this); + AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, _layoutWeakSubscriber); + AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, _layoutWeakSubscriber); // Walk through all the elements and make sure they are cleared foreach (var element in Children) @@ -742,8 +752,8 @@ namespace Avalonia.Controls { newValue.InitializeForContext(LayoutContext); - AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, this); - AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, this); + AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, _layoutWeakSubscriber); + AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, _layoutWeakSubscriber); } bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; @@ -793,15 +803,7 @@ namespace Avalonia.Controls { _viewportManager.OnBringIntoViewRequested(e); } - - void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) - { - if(ev == AttachedLayout.ArrangeInvalidatedWeakEvent) - InvalidateArrange(); - else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent) - InvalidateMeasure(); - } - + private VirtualizingLayoutContext GetLayoutContext() { if (_layoutContext == null) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index eaee5bdb50..76de113290 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -33,8 +33,7 @@ namespace Avalonia.Controls ICloseable, IStyleHost, ILogicalRoot, - ITextInputMethodRoot, - IWeakEventSubscriber + ITextInputMethodRoot { /// /// Defines the property. @@ -90,6 +89,7 @@ namespace Avalonia.Controls private WindowTransparencyLevel _actualTransparencyLevel; private ILayoutManager _layoutManager; private Border _transparencyFallbackBorder; + private WeakEventSubscriber _resourcesChangesSubscriber; /// /// Initializes static members of the class. @@ -184,7 +184,12 @@ namespace Avalonia.Controls if (((IStyleHost)this).StylingParent is IResourceHost applicationResources) { - ResourcesChangedWeakEvent.Subscribe(applicationResources, this); + _resourcesChangesSubscriber = new(); + _resourcesChangesSubscriber.Event += (_, __, e) => + { + ((ILogical)this).NotifyResourcesChanged(e); + }; + ResourcesChangedWeakEvent.Subscribe(applicationResources, _resourcesChangesSubscriber); } impl.LostFocus += PlatformImpl_LostFocus; @@ -289,11 +294,6 @@ namespace Avalonia.Controls /// IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice; - void IWeakEventSubscriber.OnEvent(object sender, WeakEvent ev, ResourcesChangedEventArgs e) - { - ((ILogical)this).NotifyResourcesChanged(e); - } - /// /// Gets or sets a value indicating whether access keys are shown in the window. /// diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index 65ba851100..f0a0d24248 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -7,7 +7,7 @@ namespace Avalonia.Media /// /// Describes how a stroke is drawn. /// - public sealed class Pen : AvaloniaObject, IPen, IWeakEventSubscriber + public sealed class Pen : AvaloniaObject, IPen { /// /// Defines the property. @@ -48,7 +48,8 @@ namespace Avalonia.Media private EventHandler? _invalidated; private IAffectsRender? _subscribedToBrush; private IAffectsRender? _subscribedToDashes; - + private WeakEventSubscriber? _weakSubscriber; + /// /// Initializes a new instance of the class. /// @@ -207,13 +208,23 @@ namespace Avalonia.Media { if ((_invalidated == null || field != value) && field != null) { - InvalidatedWeakEvent.Unsubscribe(field, this); + if (_weakSubscriber != null) + InvalidatedWeakEvent.Unsubscribe(field, _weakSubscriber); field = null; } if (_invalidated != null && field != value && value is IAffectsRender affectsRender) { - InvalidatedWeakEvent.Subscribe(affectsRender, this); + if (_weakSubscriber == null) + { + _weakSubscriber = new WeakEventSubscriber(); + _weakSubscriber.Event += (_, ev, __) => + { + if (ev == InvalidatedWeakEvent) + _invalidated?.Invoke(this, EventArgs.Empty); + }; + } + InvalidatedWeakEvent.Subscribe(affectsRender, _weakSubscriber); field = affectsRender; } } @@ -223,11 +234,5 @@ namespace Avalonia.Media UpdateSubscription(ref _subscribedToBrush, Brush); UpdateSubscription(ref _subscribedToDashes, DashStyle); } - - void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) - { - if (ev == InvalidatedWeakEvent) - _invalidated?.Invoke(this, EventArgs.Empty); - } } } From bb9f5e75f33f460dcb729678b2c3a8a41fc81fb0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 19:55:46 +0300 Subject: [PATCH 05/76] Fixed WeakEvent Compact --- src/Avalonia.Base/Utilities/WeakEvent.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index 21c165afae..5d5a1cb55b 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -75,7 +75,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event get { if (_reference == null) - return false; + return true; if (_reference.TryGetTarget(out var target)) return true; _reference = null; @@ -179,15 +179,15 @@ public class WeakEvent : WeakEvent where TEventArgs : Event int empty = -1; for (var c = 0; c < _count; c++) { - ref var r = ref _data[c]; + ref var currentRef = ref _data[c]; //Mark current index as first empty - if (r.IsEmpty && empty == -1) + if (currentRef.IsEmpty && empty == -1) empty = c; //If current element isn't null and we have an empty one - if (!r.IsEmpty && empty != -1) + if (!currentRef.IsEmpty && empty != -1) { - _data[c] = default; - _data[empty] = r; + _data[empty] = currentRef; + currentRef = default; empty++; } } From c73fcd25a9cfcaa17e7295b6b01080c585fe3c78 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 20:09:24 +0300 Subject: [PATCH 06/76] Fixed tests --- src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs | 2 +- src/Avalonia.Base/Utilities/WeakEvent.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs index 2a24376592..57060853c8 100644 --- a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs +++ b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs @@ -17,6 +17,6 @@ public class WeakEventSubscriber : IWeakEventSubscriber void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, TEventArgs e) { - + Event?.Invoke(sender, ev, e); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index 5d5a1cb55b..bcee7f89f7 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -76,10 +76,10 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { if (_reference == null) return true; - if (_reference.TryGetTarget(out var target)) - return true; + if (_reference.TryGetTarget(out _)) + return false; _reference = null; - return false; + return true; } } From a4fa74977f2a9c7c8cb6fc1f8b86c88f00543463 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 20:17:56 +0300 Subject: [PATCH 07/76] Removed immediate compact since it makes things worse --- src/Avalonia.Base/Utilities/WeakEvent.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index bcee7f89f7..b27ea9f455 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -106,7 +106,6 @@ public class WeakEvent : WeakEvent where TEventArgs : Event private int _count; private readonly Action _unsubscribe; private bool _compactScheduled; - private int _removedSinceLastCompact; public Subscription(WeakEvent ev, TSender target) { @@ -154,11 +153,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event if (removed) { - _removedSinceLastCompact++; ScheduleCompact(); - - if (_removedSinceLastCompact > 500) - Compact(); } } @@ -172,10 +167,9 @@ public class WeakEvent : WeakEvent where TEventArgs : Event void Compact() { - if(!_compactScheduled || _removedSinceLastCompact == 0) + if(!_compactScheduled) return; _compactScheduled = false; - _removedSinceLastCompact = 0; int empty = -1; for (var c = 0; c < _count; c++) { From 8103f2a0b1c7dbe5f0c620229442ce8a7fe619d3 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 23:47:12 +0300 Subject: [PATCH 08/76] Use Dictionary for more than 8 WeakEvent subscribers --- src/Avalonia.Base/Utilities/WeakEvent.cs | 85 ++----- src/Avalonia.Base/Utilities/WeakHashList.cs | 236 ++++++++++++++++++++ 2 files changed, 258 insertions(+), 63 deletions(-) create mode 100644 src/Avalonia.Base/Utilities/WeakHashList.cs diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index b27ea9f455..1335d7e9b8 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -101,11 +101,10 @@ public class WeakEvent : WeakEvent where TEventArgs : Event } } - private Entry[] _data = - new Entry[16]; - private int _count; private readonly Action _unsubscribe; + private readonly WeakHashList> _list = new(); private bool _compactScheduled; + private bool _destroyed; public Subscription(WeakEvent ev, TSender target) { @@ -117,49 +116,27 @@ public class WeakEvent : WeakEvent where TEventArgs : Event void Destroy() { + if(_destroyed) + return; + _destroyed = true; _unsubscribe(); _ev._subscriptions.Remove(_target); } - public void Add(IWeakEventSubscriber s) - { - if (_count == _data.Length) - { - //Extend capacity - var extendedData = new Entry[_data.Length * 2]; - Array.Copy(_data, extendedData, _data.Length); - _data = extendedData; - } - - _data[_count] = new(s); - _count++; - } + public void Add(IWeakEventSubscriber s) => _list.Add(s); public void Remove(IWeakEventSubscriber s) { - var removed = false; - - for (int c = 0; c < _count; ++c) - { - var reference = _data[c]; - - if (reference.Equals(s)) - { - _data[c] = default; - removed = true; - break; - } - } - - if (removed) - { + _list.Remove(s); + if(_list.IsEmpty) + Destroy(); + else if(_list.NeedCompact && _compactScheduled) ScheduleCompact(); - } } void ScheduleCompact() { - if(_compactScheduled) + if(_compactScheduled || _destroyed) return; _compactScheduled = true; Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background); @@ -170,42 +147,24 @@ public class WeakEvent : WeakEvent where TEventArgs : Event if(!_compactScheduled) return; _compactScheduled = false; - int empty = -1; - for (var c = 0; c < _count; c++) - { - ref var currentRef = ref _data[c]; - //Mark current index as first empty - if (currentRef.IsEmpty && empty == -1) - empty = c; - //If current element isn't null and we have an empty one - if (!currentRef.IsEmpty && empty != -1) - { - _data[empty] = currentRef; - currentRef = default; - empty++; - } - } - - if (empty != -1) - _count = empty; - if (_count == 0) + _list.Compact(); + if (_list.IsEmpty) Destroy(); } void OnEvent(object? sender, TEventArgs eventArgs) { - var needCompact = false; - for (var c = 0; c < _count; c++) + var alive = _list.GetAlive(); + if(alive == null) + Destroy(); + else { - var r = _data[c]; - if (r.TryGetTarget(out var sub)) - sub!.OnEvent(_target, _ev, eventArgs); - else - needCompact = true; + foreach(var item in alive) + item.OnEvent(_target, _ev, eventArgs); + WeakHashList>.ReturnToSharedPool(alive); + if(_list.NeedCompact && !_compactScheduled) + ScheduleCompact(); } - - if (needCompact) - ScheduleCompact(); } } diff --git a/src/Avalonia.Base/Utilities/WeakHashList.cs b/src/Avalonia.Base/Utilities/WeakHashList.cs new file mode 100644 index 0000000000..32668872da --- /dev/null +++ b/src/Avalonia.Base/Utilities/WeakHashList.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using Avalonia.Collections.Pooled; + +namespace Avalonia.Utilities; + +internal class WeakHashList where T : class +{ + private struct Key + { + public WeakReference? Weak; + public T? Strong; + public int HashCode; + + public static Key MakeStrong(T r) => new() + { + HashCode = r.GetHashCode(), + Strong = r + }; + + public static Key MakeWeak(T r) => new() + { + HashCode = r.GetHashCode(), + Weak = new WeakReference(r) + }; + + public override int GetHashCode() => HashCode; + } + + class KeyComparer : IEqualityComparer + { + public bool Equals(Key x, Key y) + { + if (x.HashCode != y.HashCode) + return false; + if (x.Strong != null) + { + if (y.Strong != null) + return x.Strong == y.Strong; + if (y.Weak == null) + return false; + return y.Weak.TryGetTarget(out var weakTarget) && weakTarget == x.Strong; + } + else if (y.Strong != null) + { + if (x.Weak == null) + return false; + return x.Weak.TryGetTarget(out var weakTarget) && weakTarget == y.Strong; + } + else + { + if (x.Weak == null || x.Weak.TryGetTarget(out var xTarget) == false) + return y.Weak?.TryGetTarget(out _) != true; + return y.Weak?.TryGetTarget(out var yTarget) == true && xTarget == yTarget; + } + } + + public int GetHashCode(Key obj) => obj.HashCode; + public static KeyComparer Instance = new(); + } + + Dictionary? _dic; + WeakReference?[]? _arr; + int _arrCount; + + public bool IsEmpty => _dic == null || _dic.Count == 0; + public bool NeedCompact { get; private set; } + + public void Add(T item) + { + if (_dic != null) + { + var strongKey = Key.MakeStrong(item); + if (_dic.TryGetValue(strongKey, out var cnt)) + _dic[strongKey] = cnt + 1; + else + _dic[Key.MakeWeak(item)] = 1; + return; + } + + if (_arr == null) + _arr = new WeakReference[8]; + + if (_arrCount < _arr.Length) + { + _arr[_arrCount] = new WeakReference(item); + _arrCount++; + return; + } + + // Check if something is dead + for (var c = 0; c < _arrCount; c++) + { + if (_arr[c]!.TryGetTarget(out _) == false) + { + _arr[c] = new WeakReference(item); + return; + } + } + + _dic = new Dictionary(KeyComparer.Instance); + foreach (var existing in _arr) + { + if (existing!.TryGetTarget(out var target)) + Add(target); + } + _arr = null; + + } + + public void Remove(T item) + { + if (_arr != null) + { + for (var c = 0; c < _arr.Length; c++) + { + if (_arr[c]?.TryGetTarget(out var target) == true && target == item) + { + _arr[c] = null; + Compact(); + return; + } + } + } + else if (_dic != null) + { + var strongKey = Key.MakeStrong(item); + + if (_dic.TryGetValue(strongKey, out var cnt)) + { + if (cnt > 1) + { + _dic[strongKey] = cnt - 1; + return; + } + } + + _dic.Remove(strongKey); + } + } + + private void ArrCompact() + { + if (_arr != null) + { + int empty = -1; + for (var c = 0; c < _arrCount; c++) + { + var r = _arr[c]; + //Mark current index as first empty + if (r == null && empty == -1) + empty = c; + //If current element isn't null and we have an empty one + if (r != null && empty != -1) + { + _arr[c] = null; + _arr[empty] = r; + empty++; + } + } + + if (empty != -1) + _arrCount = empty; + } + } + + public void Compact() + { + if (_dic != null) + { + PooledList? toRemove = null; + foreach (var kvp in _dic) + { + if (kvp.Key.Weak?.TryGetTarget(out _) != true) + (toRemove ??= new PooledList()).Add(kvp.Key); + } + + if (toRemove != null) + { + foreach (var k in toRemove) + _dic.Remove(k); + toRemove.Dispose(); + } + } + } + + private static readonly Stack> s_listPool = new(); + + public static void ReturnToSharedPool(PooledList list) + { + list.Clear(); + s_listPool.Push(list); + } + + public PooledList? GetAlive(Func>? factory = null) + { + PooledList? pooled = null; + if (_arr != null) + { + bool needCompact = false; + for (var c = 0; c < _arrCount; c++) + { + if (_arr[c]?.TryGetTarget(out var target) == true) + (pooled ??= factory?.Invoke() + ?? (s_listPool.Count > 0 + ? s_listPool.Pop() + : new PooledList())).Add(target!); + else + { + _arr[c] = null; + needCompact = true; + } + } + if(needCompact) + ArrCompact(); + return pooled; + } + if (_dic != null) + { + + foreach (var kvp in _dic) + { + if (kvp.Key.Weak?.TryGetTarget(out var target) == true) + (pooled ??= factory?.Invoke() + ?? (s_listPool.Count > 0 + ? s_listPool.Pop() + : new PooledList())) + .Add(target!); + else + NeedCompact = true; + } + } + + return pooled; + } +} \ No newline at end of file From 27ecb055086d437516ec6685fc22016d77ebed94 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 17 Jan 2022 11:38:20 +0300 Subject: [PATCH 09/76] Use PooledList.Span for enumeration --- src/Avalonia.Base/Utilities/WeakEvent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index 1335d7e9b8..e72606bf70 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -159,7 +159,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event Destroy(); else { - foreach(var item in alive) + foreach(var item in alive.Span) item.OnEvent(_target, _ev, eventArgs); WeakHashList>.ReturnToSharedPool(alive); if(_list.NeedCompact && !_compactScheduled) From b2b4014b1c6e5805e7c2330582124aa2f73f34ca Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 3 Feb 2022 14:58:28 +0300 Subject: [PATCH 10/76] Make DispatcherPriority to be a struct with static readonly field values In future versions this will allow us to extend priority lists without breaking binary compatibility --- src/Avalonia.Base/ApiCompatBaseline.txt | 3 +- .../Threading/AvaloniaScheduler.cs | 2 +- src/Avalonia.Base/Threading/Dispatcher.cs | 12 +-- .../Threading/DispatcherPriority.cs | 97 ++++++++++++++----- .../Threading/DispatcherTimer.cs | 4 +- src/Avalonia.Base/Threading/IDispatcher.cs | 12 +-- .../Diagnostics/ViewModels/MainViewModel.cs | 3 +- .../Avalonia.UnitTests/ImmediateDispatcher.cs | 12 +-- 8 files changed, 96 insertions(+), 49 deletions(-) diff --git a/src/Avalonia.Base/ApiCompatBaseline.txt b/src/Avalonia.Base/ApiCompatBaseline.txt index 4701a83175..7f378d2f65 100644 --- a/src/Avalonia.Base/ApiCompatBaseline.txt +++ b/src/Avalonia.Base/ApiCompatBaseline.txt @@ -1,3 +1,4 @@ Compat issues with assembly Avalonia.Base: +MembersMustExist : Member 'public System.Int32 System.Int32 Avalonia.Threading.DispatcherPriority.value__' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Threading.IDispatcher.Post(System.Action, T, Avalonia.Threading.DispatcherPriority)' is present in the implementation but not in the contract. -Total Issues: 1 +Total Issues: 2 diff --git a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs index 397826df53..6423d86e7c 100644 --- a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs +++ b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs @@ -46,7 +46,7 @@ namespace Avalonia.Threading { composite.Add(action(this, state)); } - }, DispatcherPriority.DataBind); + }, DispatcherPriority.Background); composite.Add(cancellation); diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index 49cee441d0..2eb2e7c01f 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -83,42 +83,42 @@ namespace Avalonia.Threading _jobRunner.HasJobsWithPriority(minimumPriority); /// - public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Action action, DispatcherPriority priority = default) { _ = action ?? throw new ArgumentNullException(nameof(action)); return _jobRunner.InvokeAsync(action, priority); } /// - public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func function, DispatcherPriority priority = default) { _ = function ?? throw new ArgumentNullException(nameof(function)); return _jobRunner.InvokeAsync(function, priority); } /// - public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func function, DispatcherPriority priority = default) { _ = function ?? throw new ArgumentNullException(nameof(function)); return _jobRunner.InvokeAsync(function, priority).Unwrap(); } /// - public Task InvokeAsync(Func> function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func> function, DispatcherPriority priority = default) { _ = function ?? throw new ArgumentNullException(nameof(function)); return _jobRunner.InvokeAsync(function, priority).Unwrap(); } /// - public void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal) + public void Post(Action action, DispatcherPriority priority = default) { _ = action ?? throw new ArgumentNullException(nameof(action)); _jobRunner.Post(action, priority); } /// - public void Post(Action action, T arg, DispatcherPriority priority = DispatcherPriority.Normal) + public void Post(Action action, T arg, DispatcherPriority priority = default) { _ = action ?? throw new ArgumentNullException(nameof(action)); _jobRunner.Post(action, arg, priority); diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index a2b4b86bac..a93e4f406d 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -1,74 +1,121 @@ +using System; + namespace Avalonia.Threading { /// /// Defines the priorities with which jobs can be invoked on a . /// - // TODO: These are copied from WPF - many won't apply to Avalonia. - public enum DispatcherPriority + public readonly struct DispatcherPriority : IEquatable, IComparable { + /// + /// The integer value of the priority + /// + public int Value { get; } + + private DispatcherPriority(int value) + { + Value = value; + } + /// /// Minimum possible priority /// - MinValue = 1, - + public static readonly DispatcherPriority MinValue = new(0); + /// /// The job will be processed when the system is idle. /// - SystemIdle = 1, + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority SystemIdle = MinValue; /// /// The job will be processed when the application is idle. /// - ApplicationIdle = 2, + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ApplicationIdle = MinValue; /// /// The job will be processed after background operations have completed. /// - ContextIdle = 3, + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ContextIdle = MinValue; /// - /// The job will be processed after other non-idle operations have completed. + /// The job will be processed with normal priority. /// - Background = 4, + public static readonly DispatcherPriority Normal = MinValue; /// - /// The job will be processed with the same priority as input. + /// The job will be processed after other non-idle operations have completed. /// - Input = 5, + public static readonly DispatcherPriority Background = new(1); /// - /// The job will be processed after layout and render but before input. + /// The job will be processed with the same priority as input. /// - Loaded = 6, + public static readonly DispatcherPriority Input = new(2); /// - /// The job will be processed with the same priority as render. + /// The job will be processed after layout and render but before input. /// - Render = 7, + public static readonly DispatcherPriority Loaded = new(3); /// /// The job will be processed with the same priority as render. /// - Layout = 8, - + public static readonly DispatcherPriority Render = new(5); + /// - /// The job will be processed with the same priority as data binding. + /// The job will be processed with the same priority as render. /// - DataBind = 9, + public static readonly DispatcherPriority Layout = new(6); /// - /// The job will be processed with normal priority. + /// The job will be processed with the same priority as data binding. /// - Normal = 10, + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority DataBind = MinValue; /// /// The job will be processed before other asynchronous operations. /// - Send = 11, - + public static readonly DispatcherPriority Send = new(7); + /// /// Maximum possible priority /// - MaxValue = 11 + public static readonly DispatcherPriority MaxValue = Send; + + // Note: unlike ctor this one is validating + public static DispatcherPriority FromValue(int value) + { + if (value < MinValue.Value || value > MaxValue.Value) + throw new ArgumentOutOfRangeException(nameof(value)); + return new DispatcherPriority(value); + } + + public static implicit operator int(DispatcherPriority priority) => priority.Value; + + public static implicit operator DispatcherPriority(int value) => FromValue(value); + + /// + public bool Equals(DispatcherPriority other) => Value == other.Value; + + /// + public override bool Equals(object? obj) => obj is DispatcherPriority other && Equals(other); + + /// + public override int GetHashCode() => Value.GetHashCode(); + + public static bool operator ==(DispatcherPriority left, DispatcherPriority right) => left.Value == right.Value; + + public static bool operator !=(DispatcherPriority left, DispatcherPriority right) => left.Value != right.Value; + + public static bool operator <(DispatcherPriority left, DispatcherPriority right) => left.Value < right.Value; + + public static bool operator >(DispatcherPriority left, DispatcherPriority right) => left.Value > right.Value; + + public static bool operator <=(DispatcherPriority left, DispatcherPriority right) => left.Value <= right.Value; + + public static bool operator >=(DispatcherPriority left, DispatcherPriority right) => left.Value >= right.Value; + + /// + public int CompareTo(DispatcherPriority other) => Value.CompareTo(other.Value); } -} +} \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index 93023b90a5..0c25d89722 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -123,7 +123,7 @@ namespace Avalonia.Threading /// The interval at which to tick. /// The priority to use. /// An used to cancel the timer. - public static IDisposable Run(Func action, TimeSpan interval, DispatcherPriority priority = DispatcherPriority.Normal) + public static IDisposable Run(Func action, TimeSpan interval, DispatcherPriority priority = default) { var timer = new DispatcherTimer(priority) { Interval = interval }; @@ -152,7 +152,7 @@ namespace Avalonia.Threading public static IDisposable RunOnce( Action action, TimeSpan interval, - DispatcherPriority priority = DispatcherPriority.Normal) + DispatcherPriority priority = default) { interval = (interval != TimeSpan.Zero) ? interval : TimeSpan.FromTicks(1); diff --git a/src/Avalonia.Base/Threading/IDispatcher.cs b/src/Avalonia.Base/Threading/IDispatcher.cs index cd5add70d4..eccd42bd4e 100644 --- a/src/Avalonia.Base/Threading/IDispatcher.cs +++ b/src/Avalonia.Base/Threading/IDispatcher.cs @@ -24,7 +24,7 @@ namespace Avalonia.Threading /// /// The method. /// The priority with which to invoke the method. - void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal); + void Post(Action action, DispatcherPriority priority = default); /// /// Posts an action that will be invoked on the dispatcher thread. @@ -33,7 +33,7 @@ namespace Avalonia.Threading /// The method to call. /// The argument of method to call. /// The priority with which to invoke the method. - void Post(Action action, T arg, DispatcherPriority priority = DispatcherPriority.Normal); + void Post(Action action, T arg, DispatcherPriority priority = default); /// /// Invokes a action on the dispatcher thread. @@ -41,7 +41,7 @@ namespace Avalonia.Threading /// The method. /// The priority with which to invoke the method. /// A task that can be used to track the method's execution. - Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal); + Task InvokeAsync(Action action, DispatcherPriority priority = default); /// /// Invokes a method on the dispatcher thread. @@ -49,7 +49,7 @@ namespace Avalonia.Threading /// The method. /// The priority with which to invoke the method. /// A task that can be used to track the method's execution. - Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal); + Task InvokeAsync(Func function, DispatcherPriority priority = default); /// /// Queues the specified work to run on the dispatcher thread and returns a proxy for the @@ -58,7 +58,7 @@ namespace Avalonia.Threading /// The work to execute asynchronously. /// The priority with which to invoke the method. /// A task that represents a proxy for the task returned by . - Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal); + Task InvokeAsync(Func function, DispatcherPriority priority = default); /// /// Queues the specified work to run on the dispatcher thread and returns a proxy for the @@ -67,6 +67,6 @@ namespace Avalonia.Threading /// The work to execute asynchronously. /// The priority with which to invoke the method. /// A task that represents a proxy for the task returned by . - Task InvokeAsync(Func> function, DispatcherPriority priority = DispatcherPriority.Normal); + Task InvokeAsync(Func> function, DispatcherPriority priority = default); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index e08c5bc8dd..140515eb40 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -163,8 +163,7 @@ namespace Avalonia.Diagnostics.ViewModels } catch { } }, - TimeSpan.FromMilliseconds(0), - DispatcherPriority.ApplicationIdle); + TimeSpan.FromMilliseconds(0)); } RaiseAndSetIfChanged(ref _content, value); diff --git a/tests/Avalonia.UnitTests/ImmediateDispatcher.cs b/tests/Avalonia.UnitTests/ImmediateDispatcher.cs index 5f0d41590f..03c89732f3 100644 --- a/tests/Avalonia.UnitTests/ImmediateDispatcher.cs +++ b/tests/Avalonia.UnitTests/ImmediateDispatcher.cs @@ -16,39 +16,39 @@ namespace Avalonia.UnitTests } /// - public void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal) + public void Post(Action action, DispatcherPriority priority) { action(); } /// - public void Post(Action action, T arg, DispatcherPriority priority = DispatcherPriority.Normal) + public void Post(Action action, T arg, DispatcherPriority priority) { action(arg); } /// - public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Action action, DispatcherPriority priority) { action(); return Task.CompletedTask; } /// - public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func function, DispatcherPriority priority) { var result = function(); return Task.FromResult(result); } /// - public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func function, DispatcherPriority priority) { return function(); } /// - public Task InvokeAsync(Func> function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func> function, DispatcherPriority priority) { return function(); } From 5669a640d019ab5425775a80339aa73a32662dcd Mon Sep 17 00:00:00 2001 From: Kibnet Philosoff Date: Wed, 9 Feb 2022 13:13:27 +0300 Subject: [PATCH 11/76] Fix #7567 - TreeView crashes the app when trying to display the same viewmodel twice --- .../Generators/TreeContainerIndex.cs | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs index da13416700..eb60fca367 100644 --- a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs +++ b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs @@ -15,6 +15,7 @@ namespace Avalonia.Controls.Generators /// public class TreeContainerIndex { + private readonly Dictionary> _itemToContainerSet = new Dictionary>(); private readonly Dictionary _itemToContainer = new Dictionary(); private readonly Dictionary _containerToItem = new Dictionary(); @@ -45,14 +46,45 @@ namespace Avalonia.Controls.Generators /// The item container. public void Add(object item, IControl container) { - _itemToContainer.Add(item, container); + _itemToContainer[item] = container; + if (_itemToContainerSet.TryGetValue(item, out var set)) + { + set.Add(container); + } + else + { + _itemToContainerSet.Add(item, new HashSet { container }); + } + _containerToItem.Add(container, item); Materialized?.Invoke( - this, + this, new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0))); } + /// + /// Removes a container from private collections. + /// + /// The item container. + /// The DataContext object + private void RemoveContainer(IControl container, object item) + { + if (_itemToContainerSet.TryGetValue(item, out var set)) + { + set.Remove(container); + if (set.Count == 0) + { + _itemToContainerSet.Remove(item); + _itemToContainer.Remove(item); + } + else + { + _itemToContainer[item] = set.First(); + } + } + } + /// /// Removes a container from the index. /// @@ -61,10 +93,10 @@ namespace Avalonia.Controls.Generators { var item = _containerToItem[container]; _containerToItem.Remove(container); - _itemToContainer.Remove(item); + RemoveContainer(container, item); Dematerialized?.Invoke( - this, + this, new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0))); } @@ -79,7 +111,7 @@ namespace Avalonia.Controls.Generators { var item = _containerToItem[container.ContainerControl]; _containerToItem.Remove(container.ContainerControl); - _itemToContainer.Remove(item); + RemoveContainer(container.ContainerControl, item); } Dematerialized?.Invoke( @@ -97,6 +129,14 @@ namespace Avalonia.Controls.Generators if (item != null) { _itemToContainer.TryGetValue(item, out var result); + if (result == null) + { + _itemToContainerSet.TryGetValue(item, out var set); + if (set?.Count > 0) + { + return set.FirstOrDefault(); + } + } return result; } @@ -113,6 +153,10 @@ namespace Avalonia.Controls.Generators if (container != null) { _containerToItem.TryGetValue(container, out var result); + if (result != null) + { + _itemToContainer[result] = container; + } return result; } From bcc2be8d645275a41cb83c0f8d15955b56894af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Tue, 29 Mar 2022 01:09:47 +0100 Subject: [PATCH 12/76] Fixed ScrollViewer Padding. --- src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml b/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml index b357446bfa..aab1b76259 100644 --- a/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml @@ -13,7 +13,7 @@ CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" Content="{TemplateBinding Content}" Extent="{TemplateBinding Extent, Mode=TwoWay}" - Margin="{TemplateBinding Padding}" + Padding="{TemplateBinding Padding}" Offset="{TemplateBinding Offset, Mode=TwoWay}" Viewport="{TemplateBinding Viewport, Mode=TwoWay}" IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}"> diff --git a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml index 53a1b721d1..b7addcb61e 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml @@ -32,7 +32,7 @@ CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" Content="{TemplateBinding Content}" Extent="{TemplateBinding Extent, Mode=TwoWay}" - Margin="{TemplateBinding Padding}" + Padding="{TemplateBinding Padding}" Offset="{TemplateBinding Offset, Mode=TwoWay}" Viewport="{TemplateBinding Viewport, Mode=TwoWay}" IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}"> From 847e20150c993c7431b8a145a161c82107d6297c Mon Sep 17 00:00:00 2001 From: Jade Macho Date: Tue, 29 Mar 2022 21:08:47 +0200 Subject: [PATCH 13/76] Fix IsRegisteredAsAnchorCandidate desync, mainly affecting recycled anchors --- .../Repeater/ItemsRepeater.cs | 8 ++--- .../Repeater/ViewportManager.cs | 29 +++++++++++++++---- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 09c0e58332..21b677af66 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -391,11 +391,7 @@ namespace Avalonia.Controls var newBounds = element.Bounds; virtInfo.ArrangeBounds = newBounds; - if (!virtInfo.IsRegisteredAsAnchorCandidate) - { - _viewportManager.RegisterScrollAnchorCandidate(element); - virtInfo.IsRegisteredAsAnchorCandidate = true; - } + _viewportManager.RegisterScrollAnchorCandidate(element, virtInfo); } } @@ -480,7 +476,7 @@ namespace Avalonia.Controls _processingItemsSourceChange.Action == NotifyCollectionChangedAction.Reset); _viewManager.ClearElement(element, isClearedDueToCollectionChange); - _viewportManager.OnElementCleared(element); + _viewportManager.OnElementCleared(element, GetVirtualizationInfo(element)); } private int GetElementIndexImpl(IControl element) diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index ec25fcb265..7f03cc575e 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -249,9 +249,10 @@ namespace Avalonia.Controls virtInfo.IsRegisteredAsAnchorCandidate = false; } - public void OnElementCleared(IControl element) + public void OnElementCleared(IControl element, VirtualizationInfo virtInfo) { _scroller?.UnregisterAnchorCandidate(element); + virtInfo.IsRegisteredAsAnchorCandidate = false; } public void OnOwnerMeasuring() @@ -358,9 +359,12 @@ namespace Avalonia.Controls { foreach (var child in _owner.Children) { - if (child != targetChild) + var info = ItemsRepeater.GetVirtualizationInfo(child); + + if (child != targetChild && info.IsRegisteredAsAnchorCandidate) { _scroller.UnregisterAnchorCandidate(child); + info.IsRegisteredAsAnchorCandidate = false; } } } @@ -377,9 +381,13 @@ namespace Avalonia.Controls } } - public void RegisterScrollAnchorCandidate(IControl element) + public void RegisterScrollAnchorCandidate(IControl element, VirtualizationInfo virtInfo) { - _scroller?.RegisterAnchorCandidate(element); + if (!virtInfo.IsRegisteredAsAnchorCandidate) + { + _scroller?.RegisterAnchorCandidate(element); + virtInfo.IsRegisteredAsAnchorCandidate = true; + } } private IControl? GetImmediateChildOfRepeater(IControl descendant) @@ -405,15 +413,18 @@ namespace Avalonia.Controls _isBringIntoViewInProgress = false; _makeAnchorElement = null; + // Undo the anchor deregistrations done by OnBringIntoViewRequested. if (_scroller is object) { foreach (var child in _owner.Children) { var info = ItemsRepeater.GetVirtualizationInfo(child); - if (info.IsRealized && info.IsHeldByLayout) + // The item brought into view is still registered - don't register it more than once. + if (info.IsRealized && info.IsHeldByLayout && !info.IsRegisteredAsAnchorCandidate) { _scroller.RegisterAnchorCandidate(child); + info.IsRegisteredAsAnchorCandidate = true; } } } @@ -430,7 +441,13 @@ namespace Avalonia.Controls { foreach (var child in _owner.Children) { - _scroller.UnregisterAnchorCandidate(child); + var info = ItemsRepeater.GetVirtualizationInfo(child); + + if (info.IsRegisteredAsAnchorCandidate) + { + _scroller.UnregisterAnchorCandidate(child); + info.IsRegisteredAsAnchorCandidate = false; + } } _scroller = null; From c8f2548f058841e27ab49431a847e07056dbb437 Mon Sep 17 00:00:00 2001 From: Jade Macho Date: Tue, 29 Mar 2022 22:44:32 +0200 Subject: [PATCH 14/76] Add "Scroll to Selected" and "Remove Item" to control catalog items repeater --- samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml | 2 ++ .../ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs | 10 ++++++++++ .../ViewModels/ItemsRepeaterPageViewModel.cs | 13 +++++++++++++ 3 files changed, 25 insertions(+) diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 8305d72d1f..1d42b92096 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -53,10 +53,12 @@ UniformGrid - Horizontal + + ("scroller"); _scrollToLast = this.FindControl