diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_DependencyObjectCollection.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_DependencyObjectCollection.cs new file mode 100644 index 000000000000..2ea54933aa30 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_DependencyObjectCollection.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices.ObjectiveC; +using Microsoft.UI.Xaml; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml; + +[TestClass] +public class Given_DependencyObjectCollection +{ + [TestMethod] + public void When_Add_Multiple_And_Invoke() + { + DependencyObjectCollection c = new(); + + List list = []; + + void One(object sender, object args) => list.Add("One"); + void Two(object sender, object args) => list.Add("Two"); + + c.VectorChanged += One; + c.VectorChanged += Two; + c.VectorChanged += One; + c.VectorChanged -= One; + + c.Add(c); + + Assert.IsTrue(list.SequenceEqual(["One", "Two"])); + } +} diff --git a/src/Uno.UI/UI/Xaml/DependencyObjectCollection.cs b/src/Uno.UI/UI/Xaml/DependencyObjectCollection.cs index 70003d443970..976cef393567 100644 --- a/src/Uno.UI/UI/Xaml/DependencyObjectCollection.cs +++ b/src/Uno.UI/UI/Xaml/DependencyObjectCollection.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -25,7 +26,37 @@ public partial class DependencyObjectCollectionBase : DependencyObject public partial class DependencyObjectCollection : DependencyObjectCollectionBase, IList, IEnumerable, IEnumerable, IObservableVector where T : DependencyObject { - public event VectorChangedEventHandler VectorChanged; + private object _vectorChangedHandlersLock = new(); + + // Explicit handlers list to avoid the cost of generic multicast + // delegates handling on mono's AOT. + private List> _vectorChangedHandlers; + + public event VectorChangedEventHandler VectorChanged + { + add + { + lock (_vectorChangedHandlersLock) + { + (_vectorChangedHandlers ??= new()).Add(value); + } + } + + remove + { + lock (_vectorChangedHandlersLock) + { + var list = _vectorChangedHandlers ??= new(); + + var lastIndex = list.LastIndexOf(value); + + if (lastIndex != -1) + { + list.RemoveAt(lastIndex); + } + } + } + } private readonly List _list = new List(); @@ -205,7 +236,63 @@ internal List.Enumerator GetEnumeratorFast() => _list.GetEnumerator(); private void RaiseVectorChanged(CollectionChange change, int index) - => VectorChanged?.Invoke(this, new VectorChangedEventArgs(change, (uint)index)); + { + // Gets an executable list that does not need to be locked + int GetInvocationList(out VectorChangedEventHandler single, out VectorChangedEventHandler[] array) + { + lock (_vectorChangedHandlersLock) + { + if (_vectorChangedHandlers is { Count: > 0 }) + { + if (_vectorChangedHandlers.Count == 1) + { + single = _vectorChangedHandlers[0]; + array = null; + return 1; + } + else + { + single = null; + + array = ArrayPool>.Shared.Rent(_vectorChangedHandlers.Count); + _vectorChangedHandlers.CopyTo(array, 0); + + return _vectorChangedHandlers.Count; + } + } + } + + single = null; + array = null; + return 0; + } + + var count = GetInvocationList(out var single, out var array); + + if (count > 0) + { + var args = new VectorChangedEventArgs(change, (uint)index); + + if (count == 1) + { + single.Invoke(this, args); + } + else + { + for (int i = 0; i < count; i++) + { + ref var handler = ref array[i]; + handler.Invoke(this, args); + + // Clear the handle immediately, so we don't + // call ArrayPool.Return with clear. + handler = null; + } + + ArrayPool>.Shared.Return(array); + } + } + } private protected virtual void OnAdded(T d) {