From 2cc113e395bf60725f14bd6ff94515ba587d375e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonatan=20Uusv=C3=A4li?= Date: Mon, 14 Apr 2025 15:25:50 +0300 Subject: [PATCH] Added SubscriberCount property to WeakEvent. Add and adjust tests. README.md and PACKAGE.md updated. --- README.md | 4 ++ src/WeakEvent.Tests/WeakEventGenericTests.cs | 42 +++++++++++++++--- .../WeakEventNonGenericTests.cs | 43 ++++++++++++++++--- src/WeakEvent/PACKAGE.md | 4 ++ src/WeakEvent/WeakEvent.cs | 5 +++ 5 files changed, 86 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7011a1b..0b86692 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,8 @@ public record MyEventData(string Message); ## API ### `WeakEvent` + * `SubscriberCount`\ + Number of alive subscribers currently registered to the event. * `Subscribe(Action handler)`\ `Subscribe(Func handler)`\ `Subscribe(Func handler)`\ @@ -89,6 +91,8 @@ public record MyEventData(string Message); Raises the event by invoking all live subscribers. Dead subscribers (whose targets have been garbage-collected) are removed. ### `WeakEvent` + * `SubscriberCount`\ + Number of alive subscribers currently registered to the event. * `Subscribe(Action handler)`\ `Subscribe(Func handler)`\ `Subscribe(Func handler)`\ diff --git a/src/WeakEvent.Tests/WeakEventGenericTests.cs b/src/WeakEvent.Tests/WeakEventGenericTests.cs index 12c54c1..c6dfc6f 100644 --- a/src/WeakEvent.Tests/WeakEventGenericTests.cs +++ b/src/WeakEvent.Tests/WeakEventGenericTests.cs @@ -116,7 +116,14 @@ public async Task DeadHandler_IsNotInvoked_AfterGarbageCollection() var weakEvent = new WeakEvent(); var callCount = 0; - await CreateSubscriberAndInvoke(weakEvent, () => callCount++); + static async Task createSubscriberAndInvoke(WeakEvent weakEvent, Action onEvent) + { + var subscriber = new GenericSubscriber(onEvent); + weakEvent.Subscribe(subscriber.Handler); + await weakEvent.PublishAsync("Test"); + // The subscriber goes out of scope after this method, allowing it to be GC’d. + } + await createSubscriberAndInvoke(weakEvent, () => callCount++); // Force garbage collection to reclaim the subscriber instance. GC.Collect(); @@ -129,12 +136,35 @@ public async Task DeadHandler_IsNotInvoked_AfterGarbageCollection() Assert.Equal(1, callCount); } - private static async Task CreateSubscriberAndInvoke(WeakEvent weakEvent, Action onEvent) + [Fact] + public void MultipleHandlers_CorrectCount() { - var subscriber = new GenericSubscriber(onEvent); - weakEvent.Subscribe(subscriber.Handler); - await weakEvent.PublishAsync("Test"); - // The subscriber goes out of scope after this method, allowing it to be GC’d. + // Stage 1 - No subscribers yet + var weakEvent = new WeakEvent(); + Assert.Equal(0, weakEvent.SubscriberCount); + + // Stage 2 - Add local handler + void syncHandler(string _) + { } + weakEvent.Subscribe(syncHandler); + + // Stage 3 - Add a garbage-collected handler + void createSubscriberAndAssert() + { + var subscriber = new GenericSubscriber(() => { }); + weakEvent.Subscribe(subscriber.Handler); + Assert.Equal(2, weakEvent.SubscriberCount); + } + createSubscriberAndAssert(); + + // Stage 4 - Force garbage collection to reclaim the garbage-collectable subscriber instance. + GC.Collect(); + GC.WaitForPendingFinalizers(); + Assert.Equal(1, weakEvent.SubscriberCount); + + // Stage 5 - Remove local handler + weakEvent.Unsubscribe(syncHandler); + Assert.Equal(0, weakEvent.SubscriberCount); } private class GenericSubscriber(Action onEvent) diff --git a/src/WeakEvent.Tests/WeakEventNonGenericTests.cs b/src/WeakEvent.Tests/WeakEventNonGenericTests.cs index 81461cb..02cf258 100644 --- a/src/WeakEvent.Tests/WeakEventNonGenericTests.cs +++ b/src/WeakEvent.Tests/WeakEventNonGenericTests.cs @@ -114,7 +114,14 @@ public async Task DeadHandler_IsNotInvoked_AfterGarbageCollection() var weakEvent = new WeakEvent(); var callCount = 0; - await CreateSubscriberAndInvoke(weakEvent, () => callCount++); + static async Task createSubscriberAndInvoke(WeakEvent weakEvent, Action onEvent) + { + var subscriber = new NonGenericSubscriber(onEvent); + weakEvent.Subscribe(subscriber.Handler); + await weakEvent.PublishAsync(); + // The subscriber goes out of scope after this method, allowing it to be GC’d. + } + await createSubscriberAndInvoke(weakEvent, () => callCount++); // Force garbage collection to reclaim the subscriber instance. GC.Collect(); @@ -127,12 +134,36 @@ public async Task DeadHandler_IsNotInvoked_AfterGarbageCollection() Assert.Equal(1, callCount); } - private static async Task CreateSubscriberAndInvoke(WeakEvent weakEvent, Action onEvent) + [Fact] + public void MultipleHandlers_CorrectCount() { - var subscriber = new NonGenericSubscriber(onEvent); - weakEvent.Subscribe(subscriber.Handler); - await weakEvent.PublishAsync(); - // The subscriber goes out of scope after this method, allowing it to be GC’d. + // Stage 1 - No subscribers yet + var weakEvent = new WeakEvent(); + Assert.Equal(0, weakEvent.SubscriberCount); + + // Stage 2 - Add local handler + void syncHandler() + { } + weakEvent.Subscribe(syncHandler); + Assert.Equal(1, weakEvent.SubscriberCount); + + // Stage 3 - Add a garbage-collected handler + void createSubscriberAndAssert() + { + var subscriber = new NonGenericSubscriber(() => { }); + weakEvent.Subscribe(subscriber.Handler); + Assert.Equal(2, weakEvent.SubscriberCount); + } + createSubscriberAndAssert(); + + // Stage 4 - Force garbage collection to reclaim the garbage-collectable subscriber instance. + GC.Collect(); + GC.WaitForPendingFinalizers(); + Assert.Equal(1, weakEvent.SubscriberCount); + + // Stage 5 - Remove local handler + weakEvent.Unsubscribe(syncHandler); + Assert.Equal(0, weakEvent.SubscriberCount); } private class NonGenericSubscriber(Action onEvent) diff --git a/src/WeakEvent/PACKAGE.md b/src/WeakEvent/PACKAGE.md index abb18ae..08433f2 100644 --- a/src/WeakEvent/PACKAGE.md +++ b/src/WeakEvent/PACKAGE.md @@ -68,6 +68,8 @@ public record MyEventData(string Message); ## API Overview ### `WeakEvent` + * `SubscriberCount`\ + Number of alive subscribers currently registered to the event. * `Subscribe(Action handler)`\ `Subscribe(Func handler)`\ `Subscribe(Func handler)`\ @@ -80,6 +82,8 @@ public record MyEventData(string Message); Raises the event by invoking all live subscribers. Dead subscribers (whose targets have been garbage-collected) are removed. ### `WeakEvent` + * `SubscriberCount`\ + Number of alive subscribers currently registered to the event. * `Subscribe(Action handler)`\ `Subscribe(Func handler)`\ `Subscribe(Func handler)`\ diff --git a/src/WeakEvent/WeakEvent.cs b/src/WeakEvent/WeakEvent.cs index 0312d6e..9e4f136 100644 --- a/src/WeakEvent/WeakEvent.cs +++ b/src/WeakEvent/WeakEvent.cs @@ -150,6 +150,11 @@ public abstract class WeakEventBase private readonly SemaphoreSlim _lock = new(1, 1); + /// + /// Number of alive subscribers currently registered to the event. + /// + public int SubscriberCount => _handlers.Count(x => x.IsAlive); + /// /// Subscribes the specified delegate handler to the event. ///