diff --git a/src/Controls/src/Core/Interactivity/AttachedCollection.cs b/src/Controls/src/Core/Interactivity/AttachedCollection.cs index d16b09c73b73..2a53b1ddf4e5 100644 --- a/src/Controls/src/Core/Interactivity/AttachedCollection.cs +++ b/src/Controls/src/Core/Interactivity/AttachedCollection.cs @@ -7,10 +7,7 @@ namespace Microsoft.Maui.Controls { internal class AttachedCollection : ObservableCollection, ICollection, IAttachedObject where T : BindableObject, IAttachedObject { - readonly List _associatedObjects = new List(); - - const int CleanupTrigger = 128; - int _cleanupThreshold = CleanupTrigger; + readonly WeakList _associatedObjects = new(); public AttachedCollection() { @@ -38,13 +35,10 @@ public void DetachFrom(BindableObject bindable) protected override void ClearItems() { - foreach (WeakReference weakbindable in _associatedObjects) + foreach (var bindable in _associatedObjects) { foreach (T item in this) { - var bindable = weakbindable.Target as BindableObject; - if (bindable == null) - continue; item.DetachFrom(bindable); } } @@ -54,11 +48,8 @@ protected override void ClearItems() protected override void InsertItem(int index, T item) { base.InsertItem(index, item); - foreach (WeakReference weakbindable in _associatedObjects) + foreach (var bindable in _associatedObjects) { - var bindable = weakbindable.Target as BindableObject; - if (bindable == null) - continue; item.AttachTo(bindable); } } @@ -67,8 +58,7 @@ protected virtual void OnAttachedTo(BindableObject bindable) { lock (_associatedObjects) { - _associatedObjects.Add(new WeakReference(bindable)); - CleanUpWeakReferences(); + _associatedObjects.Add(bindable); } foreach (T item in this) item.AttachTo(bindable); @@ -80,27 +70,15 @@ protected virtual void OnDetachingFrom(BindableObject bindable) item.DetachFrom(bindable); lock (_associatedObjects) { - for (var i = 0; i < _associatedObjects.Count; i++) - { - object target = _associatedObjects[i].Target; - - if (target == null || target == bindable) - { - _associatedObjects.RemoveAt(i); - i--; - } - } + _associatedObjects.Remove(bindable); } } protected override void RemoveItem(int index) { T item = this[index]; - foreach (WeakReference weakbindable in _associatedObjects) + foreach (var bindable in _associatedObjects) { - var bindable = weakbindable.Target as BindableObject; - if (bindable == null) - continue; item.DetachFrom(bindable); } @@ -110,34 +88,17 @@ protected override void RemoveItem(int index) protected override void SetItem(int index, T item) { T old = this[index]; - foreach (WeakReference weakbindable in _associatedObjects) + foreach (var bindable in _associatedObjects) { - var bindable = weakbindable.Target as BindableObject; - if (bindable == null) - continue; old.DetachFrom(bindable); } base.SetItem(index, item); - foreach (WeakReference weakbindable in _associatedObjects) + foreach (var bindable in _associatedObjects) { - var bindable = weakbindable.Target as BindableObject; - if (bindable == null) - continue; item.AttachTo(bindable); } } - - void CleanUpWeakReferences() - { - if (_associatedObjects.Count < _cleanupThreshold) - { - return; - } - - _associatedObjects.RemoveAll(t => t == null || !t.IsAlive); - _cleanupThreshold = _associatedObjects.Count + CleanupTrigger; - } } } \ No newline at end of file diff --git a/src/Controls/src/Core/Style.cs b/src/Controls/src/Core/Style.cs index 0acd5448b2c7..01f0eb55bff6 100644 --- a/src/Controls/src/Core/Style.cs +++ b/src/Controls/src/Core/Style.cs @@ -12,13 +12,10 @@ public sealed class Style : IStyle { internal const string StyleClassPrefix = "Microsoft.Maui.Controls.StyleClass."; - const int CleanupTrigger = 128; - int _cleanupThreshold = CleanupTrigger; - readonly BindableProperty _basedOnResourceProperty = BindableProperty.CreateAttached("BasedOnResource", typeof(Style), typeof(Style), default(Style), propertyChanged: OnBasedOnResourceChanged); - readonly List> _targets = new List>(4); + readonly WeakList _targets = new(); Style _basedOnStyle; @@ -66,10 +63,8 @@ public string BaseResourceKey return; _baseResourceKey = value; //update all DynamicResources - foreach (WeakReference bindableWr in _targets) + foreach (var target in _targets) { - if (!bindableWr.TryGetTarget(out BindableObject target)) - continue; target.RemoveDynamicResource(_basedOnResourceProperty); if (value != null) target.SetDynamicResource(_basedOnResourceProperty, value); @@ -98,14 +93,12 @@ void IStyle.Apply(BindableObject bindable) { lock (_targets) { - _targets.Add(new WeakReference(bindable)); + _targets.Add(bindable); } if (BaseResourceKey != null) bindable.SetDynamicResource(_basedOnResourceProperty, BaseResourceKey); ApplyCore(bindable, BasedOn ?? GetBasedOnResource(bindable)); - - CleanUpWeakReferences(); } /// @@ -117,7 +110,7 @@ void IStyle.UnApply(BindableObject bindable) bindable.RemoveDynamicResource(_basedOnResourceProperty); lock (_targets) { - _targets.RemoveAll(wr => wr != null && wr.TryGetTarget(out BindableObject target) && target == bindable); + _targets.Remove(bindable); } } @@ -149,11 +142,8 @@ void ApplyCore(BindableObject bindable, Style basedOn) void BasedOnChanged(Style oldValue, Style newValue) { - foreach (WeakReference bindableRef in _targets) + foreach (var bindable in _targets) { - if (!bindableRef.TryGetTarget(out BindableObject bindable)) - continue; - UnApplyCore(bindable, oldValue); ApplyCore(bindable, newValue); } @@ -183,17 +173,5 @@ void UnApplyCore(BindableObject bindable, Style basedOn) bool ValidateBasedOn(Style value) => value is null || value.TargetType.IsAssignableFrom(TargetType); - - void CleanUpWeakReferences() - { - if (_targets.Count < _cleanupThreshold) - return; - - lock (_targets) - { - _targets.RemoveAll(t => t == null || !t.TryGetTarget(out _)); - _cleanupThreshold = _targets.Count + CleanupTrigger; - } - } } } \ No newline at end of file diff --git a/src/Core/src/HotReload/HotReloadHelper.cs b/src/Core/src/HotReload/HotReloadHelper.cs index 49b6c645d742..4de9c4ebc680 100644 --- a/src/Core/src/HotReload/HotReloadHelper.cs +++ b/src/Core/src/HotReload/HotReloadHelper.cs @@ -6,7 +6,6 @@ using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Maui.Hosting; -using Microsoft.Maui.Internal; namespace Microsoft.Maui.HotReload { @@ -155,7 +154,7 @@ static void RegisterHandler(KeyValuePair pair, Type newHandler) public static void TriggerReload() { - List? roots = null; + List? roots = null; while (roots == null) { try diff --git a/src/Core/src/HotReload/WeakList.cs b/src/Core/src/HotReload/WeakList.cs deleted file mode 100644 index 0c5a55ffd105..000000000000 --- a/src/Core/src/HotReload/WeakList.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Microsoft.Maui.Internal -{ - class WeakList : IList - { - readonly List _items = new(); - - public T? this[int index] - { - get => (T?)_items[index].Target; - set => throw new NotImplementedException(); - } - - public int Count => CleanseItems().Count; - - public bool IsReadOnly => false; - - public void Add(T? item) - { - if (item == null) - return; - - if (Contains(item)) - return; - - _items.Add(new WeakReference(item)); - } - - public void Clear() - { - CleanseItems(); - - //foreach (var item in views) - //{ - // (item.Target as IDisposable)?.Dispose(); - //} - - _items.Clear(); - } - - public bool Contains(T? item) - { - if (item == null) - return false; - - var items = new List(CleanseItems()); - foreach (var x in items) - { - if (x.IsAlive && EqualityComparer.Default.Equals(item, (T)x.Target!)) - return true; - } - - return false; - } - - public void CopyTo(T?[] array, int arrayIndex) => - throw new NotImplementedException(); - - public IEnumerator GetEnumerator() - { - var items = new List(CleanseItems()); - foreach (var x in items) - yield return (T)x.Target!; - } - - public int IndexOf(T? item) => - throw new NotImplementedException(); - - public void Insert(int index, T? item) => - throw new NotImplementedException(); - - public bool Remove(T? item) - { - if (item == null) - return false; - - var removed = _items.RemoveAll(x => EqualityComparer.Default.Equals(item, (T)x.Target!)); - - return removed > 0; - } - - public void RemoveAt(int index) => - throw new NotImplementedException(); - - public void ForEach(Action action) - { - var items = new List(CleanseItems()); - foreach (var item in items) - { - if (item.IsAlive) - action?.Invoke((T)item.Target!); - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - List CleanseItems() - { - _items.RemoveAll(x => !x.IsAlive); - return _items; - } - } -} \ No newline at end of file diff --git a/src/Core/src/Platform/Android/EnergySaverListenerManager.cs b/src/Core/src/Platform/Android/EnergySaverListenerManager.cs index e39c0f74c155..6e13d628fb1d 100644 --- a/src/Core/src/Platform/Android/EnergySaverListenerManager.cs +++ b/src/Core/src/Platform/Android/EnergySaverListenerManager.cs @@ -1,6 +1,5 @@ using System; using Microsoft.Maui.Devices; -using Microsoft.Maui.Internal; namespace Microsoft.Maui.Platform { diff --git a/src/Core/src/WeakList.cs b/src/Core/src/WeakList.cs new file mode 100644 index 000000000000..974676a2a476 --- /dev/null +++ b/src/Core/src/WeakList.cs @@ -0,0 +1,76 @@ +#nullable enable +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Maui; + +/// +/// A List type for holding WeakReference's +/// It clears the underlying List based on a threshold of operations: Add() or GetEnumerator() +/// +class WeakList : IEnumerable where T : class +{ + readonly List> _list = new(); + int _operations = 0; + + public int Count => _list.Count; + + public int CleanupThreshold { get; set; } = 32; + + public void Add(T item) + { + CleanupIfNeeded(); + _list.Add(new WeakReference(item)); + } + + public void Remove(T item) + { + WeakReference w; + + // A reverse for-loop, means we can call RemoveAt(i) inside the loop + for (int i = _list.Count - 1; i >= 0; i--) + { + w = _list[i]; + if (w.TryGetTarget(out T? target)) + { + if (target == item) + { + _list.RemoveAt(i); + break; + } + } + else + { + // Remove if we found one that is not alive + _list.RemoveAt(i); + } + } + } + + public void Clear() + { + _list.Clear(); + _operations = 0; + } + + public IEnumerator GetEnumerator() + { + CleanupIfNeeded(); + + foreach (var w in _list) + if (w.TryGetTarget(out T? item)) + yield return item; + } + + void CleanupIfNeeded() + { + if (++_operations > CleanupThreshold) + { + _operations = 0; + _list.RemoveAll(w => !w.TryGetTarget(out _)); + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/Core/tests/UnitTests/WeakListTests.cs b/src/Core/tests/UnitTests/WeakListTests.cs new file mode 100644 index 000000000000..a604245cf755 --- /dev/null +++ b/src/Core/tests/UnitTests/WeakListTests.cs @@ -0,0 +1,63 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Maui.UnitTests +{ + public class WeakListTests + { + [Fact] + public async Task ObjectsAreEvicted_GetEnumerator() + { + var expected = new object(); + var list = new WeakList { expected, new object() }; + list.CleanupThreshold = 1; + + await Task.Yield(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + + foreach (var item in list) + { + // Do nothing + } + + Assert.Equal(1, list.Count); + Assert.Equal(expected, list.First()); + } + + [Fact] + public async Task ObjectsAreEvicted_Remove() + { + var expected = new object(); + var list = new WeakList { expected, new object() }; + list.CleanupThreshold = 1; + + await Task.Yield(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + + list.Remove(expected); + + Assert.Equal(0, list.Count); + } + + [Fact] + public async Task ObjectsAreEvicted_Add() + { + var expected = new object(); + var list = new WeakList { expected, new object() }; + list.CleanupThreshold = 1; + + await Task.Yield(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + + list.Add(new object()); + + Assert.Equal(2, list.Count); + Assert.Equal(expected, list.First()); + } + } +}