Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

StartStopEventWrapper #7137

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ public class Given_StartStopEventWrapper
[TestMethod]
public void When_Default_Event_Null()
{
var wrapper = new StartStopEventWrapper<TypedEventHandler<Accelerometer, AccelerometerReadingChangedEventArgs>>(() => { }, () => { });
var wrapper = new StartStopDelegateWrapper<TypedEventHandler<Accelerometer, AccelerometerReadingChangedEventArgs>>(() => { }, () => { });
Assert.IsNull(wrapper.Event);
}

[TestMethod]
public void When_Add_Handler_Event_Not_Null()
{
var wrapper = new StartStopEventWrapper<TypedEventHandler<Accelerometer, AccelerometerReadingChangedEventArgs>>(() => { }, () => { });
var wrapper = new StartStopDelegateWrapper<TypedEventHandler<Accelerometer, AccelerometerReadingChangedEventArgs>>(() => { }, () => { });
wrapper.AddHandler((s, e) => { });
Assert.IsNotNull(wrapper.Event);
}
Expand All @@ -31,7 +31,7 @@ public void When_Add_Remove_Event_Null()
void Handler(Accelerometer a, AccelerometerReadingChangedEventArgs e)
{
}
var wrapper = new StartStopEventWrapper<TypedEventHandler<Accelerometer, AccelerometerReadingChangedEventArgs>>(() => { }, () => { });
var wrapper = new StartStopDelegateWrapper<TypedEventHandler<Accelerometer, AccelerometerReadingChangedEventArgs>>(() => { }, () => { });
wrapper.AddHandler(Handler);
wrapper.RemoveHandler(Handler);
Assert.IsNull(wrapper.Event);
Expand All @@ -40,7 +40,7 @@ void Handler(Accelerometer a, AccelerometerReadingChangedEventArgs e)
[TestMethod]
public void When_Remove_On_Empty()
{
var wrapper = new StartStopEventWrapper<TypedEventHandler<Accelerometer, AccelerometerReadingChangedEventArgs>>(() => { }, () => { });
var wrapper = new StartStopDelegateWrapper<TypedEventHandler<Accelerometer, AccelerometerReadingChangedEventArgs>>(() => { }, () => { });
wrapper.RemoveHandler((s, e) => { });
Assert.IsNull(wrapper.Event);
}
Expand All @@ -49,7 +49,7 @@ public void When_Remove_On_Empty()
[TestMethod]
public void When_Remove_Nonexistent()
{
var wrapper = new StartStopEventWrapper<TypedEventHandler<Accelerometer, AccelerometerReadingChangedEventArgs>>(() => { }, () => { });
var wrapper = new StartStopDelegateWrapper<TypedEventHandler<Accelerometer, AccelerometerReadingChangedEventArgs>>(() => { }, () => { });
wrapper.AddHandler((s, e) => { });
wrapper.RemoveHandler((s, e) => { });
Assert.IsNotNull(wrapper.Event);
Expand All @@ -59,7 +59,7 @@ public void When_Remove_Nonexistent()
public void When_Single_Invoked()
{
var invoked = false;
var wrapper = new StartStopEventWrapper<EventHandler<EventArgs>>(() => { }, () => { });
var wrapper = new StartStopDelegateWrapper<EventHandler<EventArgs>>(() => { }, () => { });
wrapper.AddHandler((s, e) => invoked = true);
Assert.IsFalse(invoked);
wrapper.Event?.Invoke(null, EventArgs.Empty);
Expand All @@ -71,7 +71,7 @@ public void When_Multiple_Invoked()
{
var firstInvoked = false;
var secondInvoked = false;
var wrapper = new StartStopEventWrapper<EventHandler<EventArgs>>(() => { }, () => { });
var wrapper = new StartStopDelegateWrapper<EventHandler<EventArgs>>(() => { }, () => { });
wrapper.AddHandler((s, e) => firstInvoked = true);
wrapper.AddHandler((s, e) => secondInvoked = true);
Assert.IsFalse(firstInvoked);
Expand All @@ -90,7 +90,7 @@ void FirstHandler(object sender, EventArgs args)
firstInvoked = true;
}
var secondInvoked = false;
var wrapper = new StartStopEventWrapper<EventHandler<EventArgs>>(() => { }, () => { });
var wrapper = new StartStopDelegateWrapper<EventHandler<EventArgs>>(() => { }, () => { });
wrapper.AddHandler(FirstHandler);
wrapper.AddHandler((s, e) => secondInvoked = true);
Assert.IsFalse(firstInvoked);
Expand All @@ -105,7 +105,7 @@ void FirstHandler(object sender, EventArgs args)
public void When_First_Subscriber_OnFirst_Invoked()
{
var onFirst = false;
var wrapper = new StartStopEventWrapper<EventHandler<EventArgs>>(() => onFirst = true, () => { });
var wrapper = new StartStopDelegateWrapper<EventHandler<EventArgs>>(() => onFirst = true, () => { });
Assert.IsFalse(onFirst);
wrapper.AddHandler((s, e) => { });
Assert.IsTrue(onFirst);
Expand All @@ -115,7 +115,7 @@ public void When_First_Subscriber_OnFirst_Invoked()
public void When_Multiple_Subscribers_OnFirst_Once()
{
var onFirstCounter = 0;
var wrapper = new StartStopEventWrapper<EventHandler<EventArgs>>(() => onFirstCounter++, () => { });
var wrapper = new StartStopDelegateWrapper<EventHandler<EventArgs>>(() => onFirstCounter++, () => { });
Assert.AreEqual(0, onFirstCounter);
wrapper.AddHandler((s, e) => { });
wrapper.AddHandler((s, e) => { });
Expand All @@ -134,7 +134,7 @@ void SecondSubscriber(object sender, EventArgs e)
}

var onFirstCounter = 0;
var wrapper = new StartStopEventWrapper<EventHandler<EventArgs>>(() => onFirstCounter++, () => { });
var wrapper = new StartStopDelegateWrapper<EventHandler<EventArgs>>(() => onFirstCounter++, () => { });
Assert.AreEqual(0, onFirstCounter);
wrapper.AddHandler(FirstSubscriber);
wrapper.AddHandler(SecondSubscriber);
Expand All @@ -153,7 +153,7 @@ void FirstSubscriber(object sender, EventArgs e)
{
}
var onLast = false;
var wrapper = new StartStopEventWrapper<EventHandler<EventArgs>>(() => { }, () => onLast = true);
var wrapper = new StartStopDelegateWrapper<EventHandler<EventArgs>>(() => { }, () => onLast = true);
wrapper.AddHandler(FirstSubscriber);
Assert.IsFalse(onLast);
wrapper.RemoveHandler(FirstSubscriber);
Expand All @@ -171,7 +171,7 @@ void SecondSubscriber(object sender, EventArgs e)
{
}
var onLastCounter = 0;
var wrapper = new StartStopEventWrapper<EventHandler<EventArgs>>(() => { }, () => onLastCounter++);
var wrapper = new StartStopDelegateWrapper<EventHandler<EventArgs>>(() => { }, () => onLastCounter++);
wrapper.AddHandler(FirstSubscriber);
wrapper.AddHandler(SecondSubscriber);
Assert.AreEqual(0, onLastCounter);
Expand All @@ -188,7 +188,7 @@ void FirstSubscriber(object sender, EventArgs e)
}

var onLastCounter = 0;
var wrapper = new StartStopEventWrapper<EventHandler<EventArgs>>(() => { }, () => onLastCounter++);
var wrapper = new StartStopDelegateWrapper<EventHandler<EventArgs>>(() => { }, () => onLastCounter++);
wrapper.AddHandler(FirstSubscriber);
Assert.AreEqual(0, onLastCounter);
wrapper.RemoveHandler(FirstSubscriber);
Expand All @@ -208,7 +208,7 @@ void SecondSubscriber(object sender, EventArgs e)
}

var onLastCounter = 0;
var wrapper = new StartStopEventWrapper<EventHandler<EventArgs>>(() => { }, () => onLastCounter++);
var wrapper = new StartStopDelegateWrapper<EventHandler<EventArgs>>(() => { }, () => onLastCounter++);
wrapper.AddHandler(FirstSubscriber);
wrapper.AddHandler(SecondSubscriber);
Assert.AreEqual(0, onLastCounter);
Expand Down
74 changes: 74 additions & 0 deletions src/Uno.UWP/Helpers/StartStopDelegateWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#nullable enable

using System;

namespace Uno.Helpers
{
/// <summary>
/// Creates a wrapper around a generic event, which needs to be synchronized
/// and needs to run an action when first subscriber is added and when
/// last subscriber is removed. The operations executed when first subscriber
/// is added and last subscriber is removed will execute within
/// a synchronization lock, so please avoid blocking within the actions.
/// </summary>
internal class StartStopDelegateWrapper<TDelegate>
where TDelegate : Delegate
{
private readonly object _syncLock;
private readonly Action _onFirst;
private readonly Action _onLast;

/// <summary>
/// Creates a new instance of start-stop delegate wrapper.
/// </summary>
/// <param name="onFirst">Action to run when first subscriber is added.
/// This will run within a synchronization lock so it should not involve blocking operations.</param>
/// <param name="onLast">Action to run when last subscriber is removed.
/// This will run within a synchronization lock so it should not involve blocking operations.</param>
/// <param name="sharedLock">Optional shared object to lock on (when multiple events
/// rely on the same native platform operation.</param>
public StartStopDelegateWrapper(
Action onFirst,
Action onLast,
object? sharedLock = null)
{
_onFirst = onFirst;
_onLast = onLast;
_syncLock = sharedLock ?? new object();
}

public TDelegate? Event { get; private set; } = null;

Check failure on line 40 in src/Uno.UWP/Helpers/StartStopDelegateWrapper.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Member 'Event' is explicitly initialized to its default value (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1805)

Check failure on line 40 in src/Uno.UWP/Helpers/StartStopDelegateWrapper.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Member 'Event' is explicitly initialized to its default value (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1805)

public object SyncLock => _syncLock;

public void AddHandler(TDelegate handler)
{
lock (_syncLock)
{
var firstSubscriber = Event == null;
Event = (TDelegate)Delegate.Combine(Event, handler);
if (firstSubscriber)
{
_onFirst();
}
}
}

public void RemoveHandler(TDelegate handler)
{
lock (_syncLock)
{
if (Event != null)
{
Event = (TDelegate?)Delegate.Remove(Event, handler);
if (Event == null)
{
_onLast();
}
}
}
}

public bool IsActive => Event != null;
}
}
62 changes: 11 additions & 51 deletions src/Uno.UWP/Helpers/StartStopEventWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,11 @@
namespace Uno.Helpers
{
/// <summary>
/// Creates a wrapper around an event, which needs to be synchronized
/// and needs to run an action when first subscriber is added and when
/// last subscriber is removed. The operations executed when first subscriber
/// is added and last subscriber is removed will execute within
/// a synchronization lock, so please avoid blocking within the actions.
/// Start stop wrapper for EventHandler<T> events.
/// </summary>
internal class StartStopEventWrapper<TDelegate>
where TDelegate : Delegate
/// <typeparam name="TEventArgs">Event args type.</typeparam>
internal class StartStopEventWrapper<TEventArgs> : StartStopDelegateWrapper<EventHandler<TEventArgs>>
{
private readonly object _syncLock;
private readonly Action _onFirst;
private readonly Action _onLast;

/// <summary>
/// Creates a new instance of start-stop event wrapper.
/// </summary>
Expand All @@ -27,48 +19,16 @@ internal class StartStopEventWrapper<TDelegate>
/// This will run within a synchronization lock so it should not involve blocking operations.</param>
/// <param name="sharedLock">Optional shared object to lock on (when multiple events
/// rely on the same native platform operation.</param>
public StartStopEventWrapper(
Action onFirst,
Action onLast,
object? sharedLock = null)
public StartStopEventWrapper(Action onFirst, Action onLast, object? sharedLock = null) :
base(onFirst, onLast, sharedLock)
{
_onFirst = onFirst;
_onLast = onLast;
_syncLock = sharedLock ?? new object();
}

public TDelegate? Event { get; private set; }

public bool IsActive => Event != null;

public object SyncLock => _syncLock;

public void AddHandler(TDelegate handler)
{
lock (_syncLock)
{
var firstSubscriber = Event == null;
Event = (TDelegate)Delegate.Combine(Event, handler);
if (firstSubscriber)
{
_onFirst();
}
}
}

public void RemoveHandler(TDelegate handler)
{
lock (_syncLock)
{
if (Event != null)
{
Event = (TDelegate?)Delegate.Remove(Event, handler);
if (Event == null)
{
_onLast();
}
}
}
}
/// <summary>
/// Invokes the event.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="args">Args.</param>
public void Invoke(object sender, TEventArgs args) => Event?.Invoke(sender, args);
}
}
35 changes: 35 additions & 0 deletions src/Uno.UWP/Helpers/StartStopTypedEventWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#nullable enable

using System;
using Windows.Foundation;

namespace Uno.Helpers
{
/// <summary>
/// Start stop wrapper for TypedEventHandler<T> events.
/// </summary>
/// <typeparam name="TEventArgs">Event args type.</typeparam>
internal class StartStopTypedEventWrapper<TSender, TResult> : StartStopDelegateWrapper<TypedEventHandler<TSender, TResult>>
{
/// <summary>
/// Creates a new instance of start-stop event wrapper.
/// </summary>
/// <param name="onFirst">Action to run when first subscriber is added.
/// This will run within a synchronization lock so it should not involve blocking operations.</param>
/// <param name="onLast">Action to run when last subscriber is removed.
/// This will run within a synchronization lock so it should not involve blocking operations.</param>
/// <param name="sharedLock">Optional shared object to lock on (when multiple events
/// rely on the same native platform operation.</param>
public StartStopTypedEventWrapper(Action onFirst, Action onLast, object? sharedLock = null) :
base(onFirst, onLast, sharedLock)
{
}

/// <summary>
/// Invokes the event.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="args">Args.</param>
public void Invoke(TSender sender, TResult args) => Event?.Invoke(sender, args);
}
}
Loading