From 1fac4a99cfde5ac731ef8eb752058ebf697932cf Mon Sep 17 00:00:00 2001 From: jbe2277 Date: Sun, 3 Dec 2023 09:55:49 +0100 Subject: [PATCH] WAF WeakEvent add support for CollectionChanging and CollectionItemChanged --- .../WeakEventCollectionChangedTest.cs | 2 +- .../WeakEventCollectionChangingTest.cs | 57 +++++++++ .../WeakEventCollectionItemChangedTest.cs | 57 +++++++++ .../System.Waf.Core/Foundation/WeakEvent.cs | 110 +++++++++++++++++- .../System.Waf.Core/GlobalSuppressions.cs | 3 + 5 files changed, 222 insertions(+), 7 deletions(-) create mode 100644 src/System.Waf/System.Waf/System.Waf.Core.Test/Foundation/WeakEventCollectionChangingTest.cs create mode 100644 src/System.Waf/System.Waf/System.Waf.Core.Test/Foundation/WeakEventCollectionItemChangedTest.cs diff --git a/src/System.Waf/System.Waf/System.Waf.Core.Test/Foundation/WeakEventCollectionChangedTest.cs b/src/System.Waf/System.Waf/System.Waf.Core.Test/Foundation/WeakEventCollectionChangedTest.cs index d32c1f35..8ada9d90 100644 --- a/src/System.Waf/System.Waf/System.Waf.Core.Test/Foundation/WeakEventCollectionChangedTest.cs +++ b/src/System.Waf/System.Waf/System.Waf.Core.Test/Foundation/WeakEventCollectionChangedTest.cs @@ -25,7 +25,7 @@ public class Manager : IManager public class Publisher : IPublisher, INotifyCollectionChanged { - private static readonly NotifyCollectionChangedEventArgs args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + private static readonly NotifyCollectionChangedEventArgs args = new(NotifyCollectionChangedAction.Reset); private NotifyCollectionChangedEventHandler? collectionChanged; public int EventHandlerCount { get; private set; } diff --git a/src/System.Waf/System.Waf/System.Waf.Core.Test/Foundation/WeakEventCollectionChangingTest.cs b/src/System.Waf/System.Waf/System.Waf.Core.Test/Foundation/WeakEventCollectionChangingTest.cs new file mode 100644 index 00000000..4b6121de --- /dev/null +++ b/src/System.Waf/System.Waf/System.Waf.Core.Test/Foundation/WeakEventCollectionChangingTest.cs @@ -0,0 +1,57 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Specialized; +using System.Waf.Foundation; +using System.Waf.UnitTesting; + +namespace Test.Waf.Foundation +{ + [TestClass] + public class WeakEventCollectionChangingTest : WeakEventTestBase + { + [TestMethod] + public void WeakEventAddArgumentException() + { + AssertHelper.ExpectedException(() => WeakEvent.CollectionChanging.Add(null!, (s, h) => { })); + AssertHelper.ExpectedException(() => WeakEvent.CollectionChanging.Add(new Publisher(), null!)); + } + + public class Manager : IManager + { + public IWeakEventProxy? Proxy { get; set; } + + public void Add(Publisher publisher, Subscriber subscriber) => Proxy = WeakEvent.CollectionChanging.Add(publisher, subscriber.Handler); + } + + public class Publisher : IPublisher, INotifyCollectionChanging + { + private static readonly NotifyCollectionChangedEventArgs args = new(NotifyCollectionChangedAction.Reset); + private NotifyCollectionChangedEventHandler? collectionChanging; + + public int EventHandlerCount { get; private set; } + + public event NotifyCollectionChangedEventHandler? CollectionChanging + { + add + { + collectionChanging += value; + EventHandlerCount++; + } + remove + { + collectionChanging -= value; + EventHandlerCount--; + } + } + + public void RaiseEvent() => collectionChanging?.Invoke(this, args); + } + + public class Subscriber : ISubscriber + { + public int HandlerCallCount { get; set; } + + public void Handler(object? sender, NotifyCollectionChangedEventArgs e) => HandlerCallCount++; + } + } +} diff --git a/src/System.Waf/System.Waf/System.Waf.Core.Test/Foundation/WeakEventCollectionItemChangedTest.cs b/src/System.Waf/System.Waf/System.Waf.Core.Test/Foundation/WeakEventCollectionItemChangedTest.cs new file mode 100644 index 00000000..abec584e --- /dev/null +++ b/src/System.Waf/System.Waf/System.Waf.Core.Test/Foundation/WeakEventCollectionItemChangedTest.cs @@ -0,0 +1,57 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.ComponentModel; +using System.Waf.Foundation; +using System.Waf.UnitTesting; + +namespace Test.Waf.Foundation +{ + [TestClass] + public class WeakEventCollectionItemChangedTest : WeakEventTestBase + { + [TestMethod] + public void WeakEventAddArgumentException() + { + AssertHelper.ExpectedException(() => WeakEvent.CollectionItemChanged.Add(null!, (s, h) => { })); + AssertHelper.ExpectedException(() => WeakEvent.CollectionItemChanged.Add(new Publisher(), null!)); + } + + public class Manager : IManager + { + public IWeakEventProxy? Proxy { get; set; } + + public void Add(Publisher publisher, Subscriber subscriber) => Proxy = WeakEvent.CollectionItemChanged.Add(publisher, subscriber.Handler); + } + + public class Publisher : IPublisher, INotifyCollectionItemChanged + { + private static readonly PropertyChangedEventArgs args = new("Test"); + private PropertyChangedEventHandler? collectionItemChanged; + + public int EventHandlerCount { get; private set; } + + public event PropertyChangedEventHandler? CollectionItemChanged + { + add + { + collectionItemChanged += value; + EventHandlerCount++; + } + remove + { + collectionItemChanged -= value; + EventHandlerCount--; + } + } + + public void RaiseEvent() => collectionItemChanged?.Invoke(this, args); + } + + public class Subscriber : ISubscriber + { + public int HandlerCallCount { get; set; } + + public void Handler(object? sender, PropertyChangedEventArgs e) => HandlerCallCount++; + } + } +} diff --git a/src/System.Waf/System.Waf/System.Waf.Core/Foundation/WeakEvent.cs b/src/System.Waf/System.Waf/System.Waf.Core/Foundation/WeakEvent.cs index 58945978..13faea12 100644 --- a/src/System.Waf/System.Waf/System.Waf.Core/Foundation/WeakEvent.cs +++ b/src/System.Waf/System.Waf/System.Waf.Core/Foundation/WeakEvent.cs @@ -49,8 +49,7 @@ public static IWeakEventProxy Add(TSource source, S.EventHandler target return new WeakEventProxy(source, targetHandler, subscribe, unsubscribe); } - private sealed class WeakEventProxy : IWeakEventProxy - where TSource : class + private sealed class WeakEventProxy : IWeakEventProxy where TSource : class { private readonly WeakReference source; private readonly WeakReference weakTargetHandler; @@ -107,8 +106,7 @@ public static IWeakEventProxy Add(TSource source, S.EventHandler return new WeakEventProxy(source, targetHandler, subscribe, unsubscribe); } - private sealed class WeakEventProxy : IWeakEventProxy - where TSource : class + private sealed class WeakEventProxy : IWeakEventProxy where TSource : class { private readonly WeakReference source; private readonly WeakReference> weakTargetHandler; @@ -295,6 +293,56 @@ private void ProxyHandler(object? sender, PropertyChangedEventArgs e) } } + /// Supports listening to INotifyCollectionChanging.CollectionChanging events via a weak reference. + public static class CollectionChanging + { + /// Add an event handler which is kept only by a weak reference. + /// The source (publisher). + /// The target event handler of the subscriber. + /// The created proxy object. + public static IWeakEventProxy Add(INotifyCollectionChanging source, NotifyCollectionChangedEventHandler targetHandler) + { + if (source is null) throw new ArgumentNullException(nameof(source)); + if (targetHandler is null) throw new ArgumentNullException(nameof(targetHandler)); + return new WeakEventProxy(source, targetHandler, (s, h) => s.CollectionChanging += h, (s, h) => s.CollectionChanging -= h); + } + + private sealed class WeakEventProxy : IWeakEventProxy + { + private readonly WeakReference source; + private readonly WeakReference weakTargetHandler; + private Action? unsubscribe; + + public WeakEventProxy(INotifyCollectionChanging source, NotifyCollectionChangedEventHandler targetHandler, Action subscribe, Action unsubscribe) + { + this.source = new(source); + weakTargetHandler = new(targetHandler); + this.unsubscribe = unsubscribe; + subscribe(source, ProxyHandler); + weakTable.Add(targetHandler); + } + + public void Remove() + { + if (RemoveCore() && weakTargetHandler.TryGetTarget(out var targetHandler)) weakTable.Remove(targetHandler); + } + + private bool RemoveCore() + { + var unsub = Interlocked.Exchange(ref unsubscribe, null); + if (unsub is null) return false; + if (source.TryGetTarget(out var src)) unsub.Invoke(src, ProxyHandler); + return true; + } + + private void ProxyHandler(object? sender, NotifyCollectionChangedEventArgs e) + { + if (weakTargetHandler.TryGetTarget(out var targetHandler)) targetHandler(sender, e); + else RemoveCore(); + } + } + } + /// Supports listening to INotifyCollectionChanged.CollectionChanged events via a weak reference. public static class CollectionChanged { @@ -317,8 +365,8 @@ private sealed class WeakEventProxy : IWeakEventProxy public WeakEventProxy(INotifyCollectionChanged source, NotifyCollectionChangedEventHandler targetHandler, Action subscribe, Action unsubscribe) { - this.source = new WeakReference(source); - weakTargetHandler = new WeakReference(targetHandler); + this.source = new(source); + weakTargetHandler = new(targetHandler); this.unsubscribe = unsubscribe; subscribe(source, ProxyHandler); weakTable.Add(targetHandler); @@ -345,6 +393,56 @@ private void ProxyHandler(object? sender, NotifyCollectionChangedEventArgs e) } } + /// Supports listening to INotifyCollectionItemChanged.CollectionItemChanged events via a weak reference. + public static class CollectionItemChanged + { + /// Add an event handler which is kept only by a weak reference. + /// The source (publisher). + /// The target event handler of the subscriber. + /// The created proxy object. + public static IWeakEventProxy Add(INotifyCollectionItemChanged source, PropertyChangedEventHandler targetHandler) + { + if (source is null) throw new ArgumentNullException(nameof(source)); + if (targetHandler is null) throw new ArgumentNullException(nameof(targetHandler)); + return new WeakEventProxy(source, targetHandler, (s, h) => s.CollectionItemChanged += h, (s, h) => s.CollectionItemChanged -= h); + } + + private sealed class WeakEventProxy : IWeakEventProxy + { + private readonly WeakReference source; + private readonly WeakReference weakTargetHandler; + private Action? unsubscribe; + + public WeakEventProxy(INotifyCollectionItemChanged source, PropertyChangedEventHandler targetHandler, Action subscribe, Action unsubscribe) + { + this.source = new(source); + weakTargetHandler = new(targetHandler); + this.unsubscribe = unsubscribe; + subscribe(source, ProxyHandler); + weakTable.Add(targetHandler); + } + + public void Remove() + { + if (RemoveCore() && weakTargetHandler.TryGetTarget(out var targetHandler)) weakTable.Remove(targetHandler); + } + + private bool RemoveCore() + { + var unsub = Interlocked.Exchange(ref unsubscribe, null); + if (unsub is null) return false; + if (source.TryGetTarget(out var src)) unsub.Invoke(src, ProxyHandler); + return true; + } + + private void ProxyHandler(object? sender, PropertyChangedEventArgs e) + { + if (weakTargetHandler.TryGetTarget(out var targetHandler)) targetHandler(sender, e); + else RemoveCore(); + } + } + } + /// Supports listening to ICommand.CanExecuteChanged events via a weak reference. public static class CanExecuteChanged { diff --git a/src/System.Waf/System.Waf/System.Waf.Core/GlobalSuppressions.cs b/src/System.Waf/System.Waf/System.Waf.Core/GlobalSuppressions.cs index c4ecc870..c67c91d2 100644 --- a/src/System.Waf/System.Waf/System.Waf.Core/GlobalSuppressions.cs +++ b/src/System.Waf/System.Waf/System.Waf.Core/GlobalSuppressions.cs @@ -43,3 +43,6 @@ [assembly: SuppressMessage("Design", "CA1063:Implement IDisposable Correctly", Justification = "", Scope = "member", Target = "~M:System.Waf.Presentation.Services.SettingsService.Dispose(System.Boolean)")] [assembly: SuppressMessage("Design", "CA1030:Use events where appropriate", Justification = "", Scope = "member", Target = "~M:System.Waf.Applications.IDelegateCommand.RaiseCanExecuteChanged")] [assembly: SuppressMessage("Design", "CA1030:Use events where appropriate", Justification = "", Scope = "member", Target = "~M:System.Waf.Applications.DelegateCommand.RaiseCanExecuteChanged(System.Waf.Applications.IDelegateCommand[])")] +[assembly: SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "", Scope = "type", Target = "~T:System.Waf.Foundation.WeakEvent.CollectionChanging")] +[assembly: SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "", Scope = "member", Target = "~M:System.Waf.Foundation.WeakEvent.CollectionItemChanged.Add(System.Waf.Foundation.INotifyCollectionItemChanged,System.ComponentModel.PropertyChangedEventHandler)~System.Waf.Foundation.IWeakEventProxy")] +[assembly: SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "", Scope = "type", Target = "~T:System.Waf.Foundation.WeakEvent.CollectionItemChanged")]