From 70df03057d1a541771e8bcd4a156da6fcbb4d744 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 9 Jun 2019 11:06:49 -0700 Subject: [PATCH 01/21] Add Support for `event Action` and `event ActionT` --- .../Tests_WeakEventManager_Action.cs | 200 ++++++++++++++++ .../Tests_WeakEventManager_ActionT.cs | 226 ++++++++++++++++++ .../Tests_WeakEventManager_Delegate.cs | 19 +- .../Tests_WeakEventManager_EventHandlerT.cs | 16 +- .../WeakEventManager/EventManagerService.cs | 62 ++++- .../InvalidHandleEventException.cs | 21 ++ .../WeakEventManager/WeakEventManager.cs | 50 +++- 7 files changed, 576 insertions(+), 18 deletions(-) create mode 100644 Src/AsyncAwaitBestPractices.UnitTests/WeakEventManager/Tests_WeakEventManager_Action.cs create mode 100644 Src/AsyncAwaitBestPractices.UnitTests/WeakEventManager/Tests_WeakEventManager_ActionT.cs create mode 100644 Src/AsyncAwaitBestPractices/WeakEventManager/InvalidHandleEventException.cs diff --git a/Src/AsyncAwaitBestPractices.UnitTests/WeakEventManager/Tests_WeakEventManager_Action.cs b/Src/AsyncAwaitBestPractices.UnitTests/WeakEventManager/Tests_WeakEventManager_Action.cs new file mode 100644 index 0000000..fd55075 --- /dev/null +++ b/Src/AsyncAwaitBestPractices.UnitTests/WeakEventManager/Tests_WeakEventManager_Action.cs @@ -0,0 +1,200 @@ +using System; +using NUnit.Framework; + +namespace AsyncAwaitBestPractices.UnitTests +{ + public class Tests_WeakEventManager_Action : BaseTest + { + #region Constant Fields + readonly WeakEventManager _actionEventManager = new WeakEventManager(); + #endregion + + #region Events + public event Action ActionEvent + { + add => _actionEventManager.AddEventHandler(value); + remove => _actionEventManager.RemoveEventHandler(value); + } + #endregion + + + #region Methods + [Test] + public void WeakEventManagerAction_HandleEvent_ValidImplementation() + { + //Arrange + ActionEvent += HandleDelegateTest; + bool didEventFire = false; + + void HandleDelegateTest() + { + didEventFire = true; + ActionEvent -= HandleDelegateTest; + } + + //Act + _actionEventManager.HandleEvent(nameof(ActionEvent)); + + //Assert + Assert.IsTrue(didEventFire); + } + + [Test] + public void WeakEventManagerAction_HandleEvent_InvalidHandleEventEventName() + { + //Arrange + ActionEvent += HandleDelegateTest; + bool didEventFire = false; + + void HandleDelegateTest() => didEventFire = true; + + //Act + _actionEventManager.HandleEvent(nameof(TestStringEvent)); + + //Assert + Assert.False(didEventFire); + ActionEvent -= HandleDelegateTest; + } + + [Test] + public void WeakEventManagerAction_UnassignedEvent() + { + //Arrange + bool didEventFire = false; + + ActionEvent += HandleDelegateTest; + ActionEvent -= HandleDelegateTest; + void HandleDelegateTest() => didEventFire = true; + + //Act + _actionEventManager.HandleEvent(nameof(ActionEvent)); + + //Assert + Assert.IsFalse(didEventFire); + } + + [Test] + public void WeakEventManagerAction_UnassignedEventManager() + { + //Arrange + var unassignedEventManager = new WeakEventManager(); + bool didEventFire = false; + + ActionEvent += HandleDelegateTest; + void HandleDelegateTest() => didEventFire = true; + + //Act + unassignedEventManager.HandleEvent(nameof(ActionEvent)); + + //Assert + Assert.IsFalse(didEventFire); + ActionEvent -= HandleDelegateTest; + } + + [Test] + public void WeakEventManagerAction_HandleEvent_InvalidHandleEvent() + { + //Arrange + ActionEvent += HandleDelegateTest; + bool didEventFire = false; + + void HandleDelegateTest() => didEventFire = true; + + //Act + + //Assert + Assert.Throws(() => _actionEventManager.HandleEvent(this, EventArgs.Empty, nameof(ActionEvent))); + Assert.IsFalse(didEventFire); + ActionEvent -= HandleDelegateTest; + } + + [Test] + public void WeakEventManagerAction_AddEventHandler_NullHandler() + { + //Arrange + + //Act + + //Assert + Assert.Throws(() => _actionEventManager.AddEventHandler(null), "Value cannot be null.\nParameter name: handler"); + } + + [Test] + public void WeakEventManagerAction_AddEventHandler_NullEventName() + { + //Arrange + + //Act + + //Assert + Assert.Throws(() => _actionEventManager.AddEventHandler(null, null), "Value cannot be null.\nParameter name: eventName"); + } + + [Test] + public void WeakEventManagerAction_AddEventHandler_EmptyEventName() + { + //Arrange + + //Act + + //Assert + Assert.Throws(() => _actionEventManager.AddEventHandler(null, string.Empty), "Value cannot be null.\nParameter name: eventName"); + } + + [Test] + public void WeakEventManagerAction_AddEventHandler_WhitespaceEventName() + { + //Arrange + + //Act + + //Assert + Assert.Throws(() => _actionEventManager.AddEventHandler(null, " "), "Value cannot be null.\nParameter name: eventName"); + } + + [Test] + public void WeakEventManagerAction_RemoveventHandler_NullHandler() + { + //Arrange + + //Act + + //Assert + Assert.Throws(() => _actionEventManager.RemoveEventHandler(null), "Value cannot be null.\nParameter name: handler"); + } + + [Test] + public void WeakEventManagerAction_RemoveventHandler_NullEventName() + { + //Arrange + + //Act + + //Assert + Assert.Throws(() => _actionEventManager.RemoveEventHandler(null, null), "Value cannot be null.\nParameter name: eventName"); + } + + [Test] + public void WeakEventManagerAction_RemoveventHandler_EmptyEventName() + { + //Arrange + + //Act + + //Assert + Assert.Throws(() => _actionEventManager.RemoveEventHandler(null, string.Empty), "Value cannot be null.\nParameter name: eventName"); + } + + [Test] + public void WeakEventManagerAction_RemoveventHandler_WhiteSpaceEventName() + { + //Arrange + + //Act + + //Assert + Assert.Throws(() => _actionEventManager.RemoveEventHandler(null, " "), "Value cannot be null.\nParameter name: eventName"); + } + #endregion + } +} diff --git a/Src/AsyncAwaitBestPractices.UnitTests/WeakEventManager/Tests_WeakEventManager_ActionT.cs b/Src/AsyncAwaitBestPractices.UnitTests/WeakEventManager/Tests_WeakEventManager_ActionT.cs new file mode 100644 index 0000000..fa35524 --- /dev/null +++ b/Src/AsyncAwaitBestPractices.UnitTests/WeakEventManager/Tests_WeakEventManager_ActionT.cs @@ -0,0 +1,226 @@ +using System; +using NUnit.Framework; + +namespace AsyncAwaitBestPractices.UnitTests +{ + public class Tests_WeakEventManager_ActionT : BaseTest + { + #region Constant Fields + readonly WeakEventManager _actionEventManager = new WeakEventManager(); + #endregion + + #region Events + public event Action ActionEvent + { + add => _actionEventManager.AddEventHandler(value); + remove => _actionEventManager.RemoveEventHandler(value); + } + #endregion + + #region Methods + [Test] + public void WeakEventManagerActionT_HandleEvent_ValidImplementation() + { + //Arrange + ActionEvent += HandleDelegateTest; + bool didEventFire = false; + + void HandleDelegateTest(string message) + { + Assert.IsNotNull(message); + Assert.IsNotEmpty(message); + + didEventFire = true; + ActionEvent -= HandleDelegateTest; + } + + //Act + _actionEventManager.HandleEvent("Test", nameof(ActionEvent)); + + //Assert + Assert.IsTrue(didEventFire); + } + + [Test] + public void WeakEventManagerActionT_HandleEvent_InvalidHandleEventEventName() + { + //Arrange + ActionEvent += HandleDelegateTest; + bool didEventFire = false; + + void HandleDelegateTest(string message) + { + Assert.IsNotNull(message); + Assert.IsNotEmpty(message); + + didEventFire = true; + } + + //Act + _actionEventManager.HandleEvent("Test", nameof(TestEvent)); + + //Assert + Assert.False(didEventFire); + ActionEvent -= HandleDelegateTest; + } + + [Test] + public void WeakEventManagerActionT_UnassignedEvent() + { + //Arrange + bool didEventFire = false; + + ActionEvent += HandleDelegateTest; + ActionEvent -= HandleDelegateTest; + void HandleDelegateTest(string message) + { + Assert.IsNotNull(message); + Assert.IsNotEmpty(message); + + didEventFire = true; + } + + //Act + _actionEventManager.HandleEvent("Test", nameof(ActionEvent)); + + //Assert + Assert.IsFalse(didEventFire); + } + + [Test] + public void WeakEventManagerActionT_UnassignedEventManager() + { + //Arrange + var unassignedEventManager = new WeakEventManager(); + bool didEventFire = false; + + ActionEvent += HandleDelegateTest; + void HandleDelegateTest(string message) + { + Assert.IsNotNull(message); + Assert.IsNotEmpty(message); + + didEventFire = true; + } + + //Act + unassignedEventManager.HandleEvent(nameof(ActionEvent)); + + //Assert + Assert.IsFalse(didEventFire); + ActionEvent -= HandleDelegateTest; + } + + [Test] + public void WeakEventManagerActionT_HandleEvent_InvalidHandleEvent() + { + //Arrange + ActionEvent += HandleDelegateTest; + bool didEventFire = false; + + void HandleDelegateTest(string message) + { + Assert.IsNotNull(message); + Assert.IsNotEmpty(message); + + didEventFire = true; + } + + //Act + + //Assert + Assert.Throws(() => _actionEventManager.HandleEvent(this, "Test", nameof(ActionEvent))); + Assert.IsFalse(didEventFire); + ActionEvent -= HandleDelegateTest; + } + + [Test] + public void WeakEventManagerActionT_AddEventHandler_NullHandler() + { + //Arrange + + //Act + + //Assert + Assert.Throws(() => _actionEventManager.AddEventHandler((Action)null, nameof(ActionEvent)), "Value cannot be null.\nParameter name: action"); + } + + [Test] + public void WeakEventManagerActionT_AddEventHandler_NullEventName() + { + //Arrange + + //Act + + //Assert + Assert.Throws(() => _actionEventManager.AddEventHandler(s => { var temp = s; }, null), "Value cannot be null.\nParameter name: eventName"); + } + + [Test] + public void WeakEventManagerActionT_AddEventHandler_EmptyEventName() + { + //Arrange + + //Act + + //Assert + Assert.Throws(() => _actionEventManager.AddEventHandler((Action)null, string.Empty), "Value cannot be null.\nParameter name: eventName"); + } + + [Test] + public void WeakEventManagerActionT_AddEventHandler_WhitespaceEventName() + { + //Arrange + + //Act + + //Assert + Assert.Throws(() => _actionEventManager.AddEventHandler(s => { var temp = s; }, " "), "Value cannot be null.\nParameter name: eventName"); + } + + [Test] + public void WeakEventManagerActionT_RemoveventHandler_NullHandler() + { + //Arrange + + //Act + + //Assert + Assert.Throws(() => _actionEventManager.RemoveEventHandler((Action)null), "Value cannot be null.\nParameter name: handler"); + } + + [Test] + public void WeakEventManagerActionT_RemoveventHandler_NullEventName() + { + //Arrange + + //Act + + //Assert + Assert.Throws(() => _actionEventManager.RemoveEventHandler(s => { var temp = s; }, null), "Value cannot be null.\nParameter name: eventName"); + } + + [Test] + public void WeakEventManagerActionT_RemoveventHandler_EmptyEventName() + { + //Arrange + + //Act + + //Assert + Assert.Throws(() => _actionEventManager.RemoveEventHandler(s => { var temp = s; }, string.Empty), "Value cannot be null.\nParameter name: eventName"); + } + + [Test] + public void WeakEventManagerActionT_RemoveventHandler_WhiteSpaceEventName() + { + //Arrange + + //Act + + //Assert + Assert.Throws(() => _actionEventManager.RemoveEventHandler(s => { var temp = s; }, " "), "Value cannot be null.\nParameter name: eventName"); + } + #endregion + } +} diff --git a/Src/AsyncAwaitBestPractices.UnitTests/WeakEventManager/Tests_WeakEventManager_Delegate.cs b/Src/AsyncAwaitBestPractices.UnitTests/WeakEventManager/Tests_WeakEventManager_Delegate.cs index 0c6412e..b00face 100644 --- a/Src/AsyncAwaitBestPractices.UnitTests/WeakEventManager/Tests_WeakEventManager_Delegate.cs +++ b/Src/AsyncAwaitBestPractices.UnitTests/WeakEventManager/Tests_WeakEventManager_Delegate.cs @@ -110,7 +110,7 @@ void HandleDelegateTest(object sender, PropertyChangedEventArgs e) } [Test] - public void WeakEventManagerDelegate_HandleEvent_InvalidHandleEvent() + public void WeakEventManagerDelegate_HandleEvent_InvalidHandleEventEventName() { //Arrange PropertyChanged += HandleDelegateTest; @@ -161,6 +161,23 @@ public void WeakEventManagerDelegate_UnassignedEventManager() PropertyChanged -= HandleDelegateTest; } + [Test] + public void WeakEventManagerDelegate_HandleEvent_InvalidHandleEvent() + { + //Arrange + PropertyChanged += HandleDelegateTest; + bool didEventFire = false; + + void HandleDelegateTest(object sender, PropertyChangedEventArgs e) => didEventFire = true; + + //Act + + //Assert + Assert.Throws(() => _propertyChangedWeakEventManager.HandleEvent(nameof(PropertyChanged))); + Assert.IsFalse(didEventFire); + PropertyChanged -= HandleDelegateTest; + } + [Test] public void WeakEventManagerDelegate_AddEventHandler_NullHandler() { diff --git a/Src/AsyncAwaitBestPractices.UnitTests/WeakEventManager/Tests_WeakEventManager_EventHandlerT.cs b/Src/AsyncAwaitBestPractices.UnitTests/WeakEventManager/Tests_WeakEventManager_EventHandlerT.cs index f4814b0..6f7b609 100644 --- a/Src/AsyncAwaitBestPractices.UnitTests/WeakEventManager/Tests_WeakEventManager_EventHandlerT.cs +++ b/Src/AsyncAwaitBestPractices.UnitTests/WeakEventManager/Tests_WeakEventManager_EventHandlerT.cs @@ -159,7 +159,7 @@ public void WeakEventManagerT_AddEventHandler_NullHandler() //Act //Assert - Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(null), "Value cannot be null.\nParameter name: handler"); + Assert.Throws(() => TestStringWeakEventManager.AddEventHandler((EventHandler)null), "Value cannot be null.\nParameter name: handler"); } [Test] @@ -170,7 +170,7 @@ public void WeakEventManagerT_AddEventHandler_NullEventName() //Act //Assert - Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(null, null), "Value cannot be null.\nParameter name: eventName"); + Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(s=> { var temp = s; }, null), "Value cannot be null.\nParameter name: eventName"); } [Test] @@ -181,7 +181,7 @@ public void WeakEventManagerT_AddEventHandler_EmptyEventName() //Act //Assert - Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(null, string.Empty), "Value cannot be null.\nParameter name: eventName"); + Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(s => { var temp = s; }, string.Empty), "Value cannot be null.\nParameter name: eventName"); } [Test] @@ -192,7 +192,7 @@ public void WeakEventManagerT_AddEventHandler_WhiteSpaceEventName() //Act //Assert - Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(null, " "), "Value cannot be null.\nParameter name: eventName"); + Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(s => { var temp = s; }, " "), "Value cannot be null.\nParameter name: eventName"); } [Test] @@ -203,7 +203,7 @@ public void WeakEventManagerT_RemoveventHandler_NullHandler() //Act //Assert - Assert.Throws(() => TestStringWeakEventManager.RemoveEventHandler(null), "Value cannot be null.\nParameter name: handler"); + Assert.Throws(() => TestStringWeakEventManager.RemoveEventHandler((EventHandler)null), "Value cannot be null.\nParameter name: handler"); } @@ -215,7 +215,7 @@ public void WeakEventManagerT_RemoveventHandler_NullEventName() //Act //Assert - Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(null, null), "Value cannot be null.\nParameter name: eventName"); + Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(s => { var temp = s; }, null), "Value cannot be null.\nParameter name: eventName"); } [Test] @@ -226,7 +226,7 @@ public void WeakEventManagerT_RemoveventHandler_EmptyEventName() //Act //Assert - Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(null, string.Empty), "Value cannot be null.\nParameter name: eventName"); + Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(s => { var temp = s; }, string.Empty), "Value cannot be null.\nParameter name: eventName"); } [Test] @@ -237,7 +237,7 @@ public void WeakEventManagerT_RemoveventHandler_WhiteSpaceEventName() //Act //Assert - Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(null, string.Empty), "Value cannot be null.\nParameter name: eventName"); + Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(s => { var temp = s; }, string.Empty), "Value cannot be null.\nParameter name: eventName"); } } } diff --git a/Src/AsyncAwaitBestPractices/WeakEventManager/EventManagerService.cs b/Src/AsyncAwaitBestPractices/WeakEventManager/EventManagerService.cs index 3dcd693..fe21972 100644 --- a/Src/AsyncAwaitBestPractices/WeakEventManager/EventManagerService.cs +++ b/Src/AsyncAwaitBestPractices/WeakEventManager/EventManagerService.cs @@ -44,8 +44,62 @@ internal static void RemoveEventHandler(string eventName, object handlerTarget, internal static void HandleEvent(string eventName, object sender, object eventArgs, in Dictionary> eventHandlers) { - var toRaise = new List>(); + AddRemoveEvents(eventName, eventHandlers, out var toRaise); + + for (int i = 0; i < toRaise.Count; i++) + { + try + { + Tuple tuple = toRaise[i]; + tuple.Item2.Invoke(tuple.Item1, new[] { sender, eventArgs }); + } + catch (TargetParameterCountException e) when (e.Message.Contains("Parameter count mismatch")) + { + throw new InvalidHandleEventException("Parameter count mismatch. If invoking an `event Action` use `HandleEvent(string eventName)` or if invoking an `event Action` use `HandleEvent(object eventArgs, string eventName)`instead.", e); + } + } + } + + internal static void HandleEvent(string eventName, object actionEventArgs, in Dictionary> eventHandlers) + { + AddRemoveEvents(eventName, eventHandlers, out var toRaise); + + for (int i = 0; i < toRaise.Count; i++) + { + try + { + Tuple tuple = toRaise[i]; + tuple.Item2.Invoke(tuple.Item1, new[] { actionEventArgs }); + } + catch (TargetParameterCountException e) when (e.Message.Contains("Parameter count mismatch")) + { + throw new InvalidHandleEventException("Parameter count mismatch. If invoking an `event EventHandler` use `HandleEvent(object sender, TEventArgs eventArgs, string eventName)` or if invoking an `event Action` use `HandleEvent(string eventName)`instead.", e); + } + } + } + + internal static void HandleEvent(string eventName, in Dictionary> eventHandlers) + { + AddRemoveEvents(eventName, eventHandlers, out var toRaise); + + for (int i = 0; i < toRaise.Count; i++) + { + try + { + Tuple tuple = toRaise[i]; + tuple.Item2.Invoke(tuple.Item1, null); + } + catch (TargetParameterCountException e) when (e.Message.Contains("Parameter count mismatch")) + { + throw new InvalidHandleEventException("Parameter count mismatch. If invoking an `event EventHandler` use `HandleEvent(object sender, TEventArgs eventArgs, string eventName)` or if invoking an `event Action` use `HandleEvent(object eventArgs, string eventName)`instead.", e); + } + } + } + + static void AddRemoveEvents(in string eventName, in Dictionary> eventHandlers, out List> toRaise) + { var toRemove = new List(); + toRaise = new List>(); if (eventHandlers.TryGetValue(eventName, out List target)) { @@ -73,12 +127,6 @@ internal static void HandleEvent(string eventName, object sender, object eventAr target.Remove(subscription); } } - - for (int i = 0; i < toRaise.Count; i++) - { - Tuple tuple = toRaise[i]; - tuple.Item2.Invoke(tuple.Item1, new[] { sender, eventArgs }); - } } } } diff --git a/Src/AsyncAwaitBestPractices/WeakEventManager/InvalidHandleEventException.cs b/Src/AsyncAwaitBestPractices/WeakEventManager/InvalidHandleEventException.cs new file mode 100644 index 0000000..20704fa --- /dev/null +++ b/Src/AsyncAwaitBestPractices/WeakEventManager/InvalidHandleEventException.cs @@ -0,0 +1,21 @@ +using System; +using System.Reflection; + +namespace AsyncAwaitBestPractices +{ + /// + /// Represents errors that occur during WeakEventManager.HandleEvent execution. + /// + public class InvalidHandleEventException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// Message. + /// Target parameter count exception. + public InvalidHandleEventException(string message, TargetParameterCountException targetParameterCountException) : base(message, targetParameterCountException) + { + + } + } +} diff --git a/Src/AsyncAwaitBestPractices/WeakEventManager/WeakEventManager.cs b/Src/AsyncAwaitBestPractices/WeakEventManager/WeakEventManager.cs index 12201e1..99dc2d3 100644 --- a/Src/AsyncAwaitBestPractices/WeakEventManager/WeakEventManager.cs +++ b/Src/AsyncAwaitBestPractices/WeakEventManager/WeakEventManager.cs @@ -31,6 +31,22 @@ public void AddEventHandler(EventHandler handler, [CallerMemberName] EventManagerService.AddEventHandler(eventName, handler.Target, handler.GetMethodInfo(), _eventHandlers); } + /// + /// Adds the event handler + /// + /// Handler + /// Event name + public void AddEventHandler(Action action, [CallerMemberName] string eventName = "") + { + if (IsNullOrWhiteSpace(eventName)) + throw new ArgumentNullException(nameof(eventName)); + + if (action is null) + throw new ArgumentNullException(nameof(action)); + + EventManagerService.AddEventHandler(eventName, action.Target, action.GetMethodInfo(), _eventHandlers); + } + /// /// Removes the event handler /// @@ -47,14 +63,38 @@ public void RemoveEventHandler(EventHandler handler, [CallerMemberNa EventManagerService.RemoveEventHandler(eventName, handler.Target, handler.GetMethodInfo(), _eventHandlers); } + /// + /// Removes the event handler + /// + /// Handler + /// Event name + public void RemoveEventHandler(Action action, [CallerMemberName] string eventName = "") + { + if (IsNullOrWhiteSpace(eventName)) + throw new ArgumentNullException(nameof(eventName)); + + if (action is null) + throw new ArgumentNullException(nameof(action)); + + EventManagerService.RemoveEventHandler(eventName, action.Target, action.GetMethodInfo(), _eventHandlers); + } + /// /// Executes the event /// /// Sender /// Event arguments /// Event name - public void HandleEvent(object sender, TEventArgs eventArgs, string eventName) => + public void HandleEvent(object sender, TEventArgs eventArgs, string eventName) => EventManagerService.HandleEvent(eventName, sender, eventArgs, _eventHandlers); + + /// + /// Executes the event + /// + /// Event arguments + /// Event name + public void HandleEvent(TEventArgs eventArgs, string eventName) => + EventManagerService.HandleEvent(eventName, eventArgs, _eventHandlers); } /// @@ -102,7 +142,13 @@ public void RemoveEventHandler(Delegate handler, [CallerMemberName] string event /// Sender /// Event arguments /// Event name - public void HandleEvent(object sender, object eventArgs, string eventName) => + public void HandleEvent(object sender, object eventArgs, string eventName) => EventManagerService.HandleEvent(eventName, sender, eventArgs, _eventHandlers); + + /// + /// Executes the event + /// + /// Event name + public void HandleEvent(string eventName) => EventManagerService.HandleEvent(eventName, _eventHandlers); } } \ No newline at end of file From dcc0b1ca6d5c9789f5f88f41bbca00c6000950ec Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 9 Jun 2019 11:15:57 -0700 Subject: [PATCH 02/21] Added `event Action` --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 61ae7ac..25ba3ef 100644 --- a/README.md +++ b/README.md @@ -134,10 +134,12 @@ async Task ExampleAsyncMethod() ### `WeakEventManager` -An event implementation that enables the [garbage collector to collect an object without needing to unsubscribe event handlers](http://paulstovell.com/blog/weakevents), inspired by [Xamarin.Forms.WeakEventManager](https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Core/WeakEventManager.cs): +An event implementation that enables the [garbage collector to collect an object without needing to unsubscribe event handlers](http://paulstovell.com/blog/weakevents), inspired by [Xamarin.Forms.WeakEventManager](https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Core/WeakEventManager.cs). + +Using `EventHandler` ```csharp -readonly WeakEventManager _weakEventManager = new WeakEventManager(); +readonly WeakEventManager _canExecuteChangedEventManager = new WeakEventManager(); public event EventHandler CanExecuteChanged { @@ -145,10 +147,41 @@ public event EventHandler CanExecuteChanged remove => _weakEventManager.RemoveEventHandler(value); } -public void RaiseCanExecuteChanged() => _weakEventManager.HandleEvent(this, EventArgs.Empty, nameof(CanExecuteChanged)); +public void RaiseCanExecuteChanged() => _canExecuteChangedEventManager.HandleEvent(this, EventArgs.Empty, nameof(CanExecuteChanged)); +``` + +Using `Delegate` + +```csharp +readonly WeakEventManager _propertyChangedEventManager = new WeakEventManager(); + +public event PropertyChangedEventHandler PropertyChanged +{ + add => _propertyChangedEventManager.AddEventHandler(value); + remove => _propertyChangedEventManager.RemoveEventHandler(value); +} + +public void OnPropertyChanged([CallerMemberName]string propertyName = "") => _weakEventManager.HandleEvent(this, new PropertyChangedEventArgs(propertyName), nameof(PropertyChanged)); +``` + +Using `Action` + +```csharp +readonly WeakEventManager _weakActionEventManager = new WeakEventManager(); + +public event Action ActionEvent +{ + add => _weakActionEventManager.AddEventHandler(value); + remove => _weakActionEventManager.RemoveEventHandler(value); +} + +public void OnActionEvent(string message) => _weakActionEventManager.HandleEvent(message, nameof(ActionEvent)); ``` ### `WeakEventManager` +An event implementation that enables the [garbage collector to collect an object without needing to unsubscribe event handlers](http://paulstovell.com/blog/weakevents), inspired by [Xamarin.Forms.WeakEventManager](https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Core/WeakEventManager.cs). + +Using `EventHandler` ```csharp readonly WeakEventManager _errorOcurredEventManager = new WeakEventManager(); @@ -159,7 +192,21 @@ public event EventHandler ErrorOcurred remove => _errorOcurredEventManager.RemoveEventHandler(value); } -public void RaiseErrorOcurred(string message) => _weakEventManager.HandleEvent(this, message, nameof(ErrorOcurred)); +public void OnErrorOcurred(string message) => _errorOcurredEventManager.HandleEvent(this, message, nameof(ErrorOcurred)); +``` + +Using `Action` + +```csharp +readonly WeakEventManager _weakActionEventManager = new WeakEventManager(); + +public event Action ActionEvent +{ + add => _weakActionEventManager.AddEventHandler(value); + remove => _weakActionEventManager.RemoveEventHandler(value); +} + +public void OnActionEvent(string message) => _weakActionEventManager.HandleEvent(message, nameof(ActionEvent)); ``` ## AsyncAwaitBestPractices.MVVM From ed72e088d30610f730a02d7c548b5fdc8f0f8503 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 9 Jun 2019 11:20:38 -0700 Subject: [PATCH 03/21] Update README.md --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 25ba3ef..dfced38 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ async Task ExampleAsyncMethod() An event implementation that enables the [garbage collector to collect an object without needing to unsubscribe event handlers](http://paulstovell.com/blog/weakevents), inspired by [Xamarin.Forms.WeakEventManager](https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Core/WeakEventManager.cs). -Using `EventHandler` +Using `EventHandler`: ```csharp readonly WeakEventManager _canExecuteChangedEventManager = new WeakEventManager(); @@ -150,7 +150,7 @@ public event EventHandler CanExecuteChanged public void RaiseCanExecuteChanged() => _canExecuteChangedEventManager.HandleEvent(this, EventArgs.Empty, nameof(CanExecuteChanged)); ``` -Using `Delegate` +Using `Delegate`: ```csharp readonly WeakEventManager _propertyChangedEventManager = new WeakEventManager(); @@ -161,10 +161,10 @@ public event PropertyChangedEventHandler PropertyChanged remove => _propertyChangedEventManager.RemoveEventHandler(value); } -public void OnPropertyChanged([CallerMemberName]string propertyName = "") => _weakEventManager.HandleEvent(this, new PropertyChangedEventArgs(propertyName), nameof(PropertyChanged)); +public void OnPropertyChanged([CallerMemberName]string propertyName = "") => _propertyChangedEventManager.HandleEvent(this, new PropertyChangedEventArgs(propertyName), nameof(PropertyChanged)); ``` -Using `Action` +Using `Action`: ```csharp readonly WeakEventManager _weakActionEventManager = new WeakEventManager(); @@ -181,7 +181,7 @@ public void OnActionEvent(string message) => _weakActionEventManager.HandleEvent ### `WeakEventManager` An event implementation that enables the [garbage collector to collect an object without needing to unsubscribe event handlers](http://paulstovell.com/blog/weakevents), inspired by [Xamarin.Forms.WeakEventManager](https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Core/WeakEventManager.cs). -Using `EventHandler` +Using `EventHandler`: ```csharp readonly WeakEventManager _errorOcurredEventManager = new WeakEventManager(); @@ -195,10 +195,10 @@ public event EventHandler ErrorOcurred public void OnErrorOcurred(string message) => _errorOcurredEventManager.HandleEvent(this, message, nameof(ErrorOcurred)); ``` -Using `Action` +Using `Action`: ```csharp -readonly WeakEventManager _weakActionEventManager = new WeakEventManager(); +readonly WeakEventManager _weakActionEventManager = new WeakEventManager(); public event Action ActionEvent { @@ -224,7 +224,7 @@ Allows for `Task` to safely be used asynchronously with `ICommand`: public AsyncCommand(Func execute, Func canExecute = null, Action onException = null, - bool continueOnCapturedContext = true) + bool continueOnCapturedContext = true) ``` ```csharp From 480e7adf7a4534c80518c94f3a3725195200c2ae Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 9 Jun 2019 11:22:15 -0700 Subject: [PATCH 04/21] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dfced38..f7477d0 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ public event EventHandler CanExecuteChanged remove => _weakEventManager.RemoveEventHandler(value); } -public void RaiseCanExecuteChanged() => _canExecuteChangedEventManager.HandleEvent(this, EventArgs.Empty, nameof(CanExecuteChanged)); +public void OnCanExecuteChanged() => _canExecuteChangedEventManager.HandleEvent(this, EventArgs.Empty, nameof(CanExecuteChanged)); ``` Using `Delegate`: From 64a266941fb55c9907d4da0ee9cb534004c62345 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 9 Jun 2019 12:46:12 -0700 Subject: [PATCH 05/21] Updated Nuspec for v3.0.0-pre1 --- Src/AsyncAwaitBestPractices.MVVM.nuspec | 8 ++++---- Src/AsyncAwaitBestPractices.nuspec | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Src/AsyncAwaitBestPractices.MVVM.nuspec b/Src/AsyncAwaitBestPractices.MVVM.nuspec index cfdcedc..98d9010 100644 --- a/Src/AsyncAwaitBestPractices.MVVM.nuspec +++ b/Src/AsyncAwaitBestPractices.MVVM.nuspec @@ -2,11 +2,11 @@ AsyncAwaitBestPractices.MVVM - 2.1.1 + 3.0.0-pre1 Task Extensions for MVVM Brandon Minnick, John Thiriet Brandon Minnick - https://github.com/brminnick/AsyncAwaitBestPractices/blob/master/LICENSE.md + MIT https://github.com/brminnick/AsyncAwaitBestPractices false @@ -14,11 +14,11 @@ Includes AsyncCommand and IAsyncCommand which allows ICommand to safely be used asynchronously with Task. task,fire and forget, threading, extensions, system.threading.tasks,async,await - + New In This Release: - - Performance improvements to SafeFireAndForget + - Added support for `event Action` and `event Action<T>` Copyright (c) 2018 Brandon Minnick diff --git a/Src/AsyncAwaitBestPractices.nuspec b/Src/AsyncAwaitBestPractices.nuspec index b0cac3c..707ec77 100644 --- a/Src/AsyncAwaitBestPractices.nuspec +++ b/Src/AsyncAwaitBestPractices.nuspec @@ -2,11 +2,11 @@ AsyncAwaitBestPractices - 2.1.1 + 3.0.0-pre1 Task Extensions for System.Threading.Tasks Brandon Minnick, John Thiriet Brandon Minnick - https://github.com/brminnick/AsyncAwaitBestPractices/blob/master/LICENSE.md + MIT https://github.com/brminnick/AsyncAwaitBestPractices false @@ -19,7 +19,7 @@ task,fire and forget, threading, extensions, system.threading.tasks,async,await New In This Release: - - Performance improvements to SafeFireAndForget + - Added support for `event Action` and `event Action<T>` Copyright (c) 2018 Brandon Minnick From 2191824f12a68236f0ca2c52e248600e79a93d04 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sat, 22 Jun 2019 20:29:55 +0200 Subject: [PATCH 06/21] Updated HandleEvent Descriptions --- .../WeakEventManager/WeakEventManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Src/AsyncAwaitBestPractices/WeakEventManager/WeakEventManager.cs b/Src/AsyncAwaitBestPractices/WeakEventManager/WeakEventManager.cs index 99dc2d3..5d98ab8 100644 --- a/Src/AsyncAwaitBestPractices/WeakEventManager/WeakEventManager.cs +++ b/Src/AsyncAwaitBestPractices/WeakEventManager/WeakEventManager.cs @@ -80,7 +80,7 @@ public void RemoveEventHandler(Action action, [CallerMemberName] str } /// - /// Executes the event + /// Executes the event EventHandler /// /// Sender /// Event arguments @@ -89,7 +89,7 @@ public void HandleEvent(object sender, TEventArgs eventArgs, string eventName) = EventManagerService.HandleEvent(eventName, sender, eventArgs, _eventHandlers); /// - /// Executes the event + /// Executes the event Action /// /// Event arguments /// Event name @@ -137,7 +137,7 @@ public void RemoveEventHandler(Delegate handler, [CallerMemberName] string event } /// - /// Executes the event + /// Executes the event EventHandler /// /// Sender /// Event arguments @@ -146,7 +146,7 @@ public void HandleEvent(object sender, object eventArgs, string eventName) => EventManagerService.HandleEvent(eventName, sender, eventArgs, _eventHandlers); /// - /// Executes the event + /// Executes the event Action /// /// Event name public void HandleEvent(string eventName) => EventManagerService.HandleEvent(eventName, _eventHandlers); From 9b527db46b3913929801d8d05fe088c1d8485472 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Mon, 1 Jul 2019 19:03:35 -0700 Subject: [PATCH 07/21] Added SafeFireAndForget, SetDefaultExceptionHandling & AlwaysThrowException --- .../BaseTest.cs | 12 +-- .../Tests_SafeFIreAndForgetT.cs | 76 +++++++++++++++++++ .../Tests_SafeFireAndForget.cs | 56 +++++++++++++- .../AsyncAwaitBestPractices.csproj | 2 +- .../SafeFireAndForgetExtensions.cs | 63 +++++++++++++++ Src/AsyncAwaitBestPractices/TaskExtensions.cs | 28 ------- 6 files changed, 201 insertions(+), 36 deletions(-) create mode 100644 Src/AsyncAwaitBestPractices.UnitTests/Tests_SafeFIreAndForgetT.cs create mode 100644 Src/AsyncAwaitBestPractices/SafeFireAndForgetExtensions.cs delete mode 100644 Src/AsyncAwaitBestPractices/TaskExtensions.cs diff --git a/Src/AsyncAwaitBestPractices.UnitTests/BaseTest.cs b/Src/AsyncAwaitBestPractices.UnitTests/BaseTest.cs index f829be2..d38ba5c 100644 --- a/Src/AsyncAwaitBestPractices.UnitTests/BaseTest.cs +++ b/Src/AsyncAwaitBestPractices.UnitTests/BaseTest.cs @@ -29,19 +29,19 @@ protected event EventHandler TestStringEvent protected Task NoParameterTask() => Task.Delay(Delay); protected Task IntParameterTask(int delay) => Task.Delay(delay); protected Task StringParameterTask(string text) => Task.Delay(Delay); - protected Task NoParameterImmediateExceptionTask() => throw new Exception(); - protected Task ParameterImmediateExceptionTask(int delay) => throw new Exception(); + protected Task NoParameterImmediateNullReferenceExceptionTask() => throw new NullReferenceException(); + protected Task ParameterImmediateNullReferenceExceptionTask(int delay) => throw new NullReferenceException(); - protected async Task NoParameterDelayedExceptionTask() + protected async Task NoParameterDelayedNullReferenceExceptionTask() { await Task.Delay(Delay); - throw new Exception(); + throw new NullReferenceException(); } - protected async Task IntParameterDelayedExceptionTask(int delay) + protected async Task IntParameterDelayedNullReferenceExceptionTask(int delay) { await Task.Delay(delay); - throw new Exception(); + throw new NullReferenceException(); } protected bool CanExecuteTrue(object parameter) => true; diff --git a/Src/AsyncAwaitBestPractices.UnitTests/Tests_SafeFIreAndForgetT.cs b/Src/AsyncAwaitBestPractices.UnitTests/Tests_SafeFIreAndForgetT.cs new file mode 100644 index 0000000..c473a9e --- /dev/null +++ b/Src/AsyncAwaitBestPractices.UnitTests/Tests_SafeFIreAndForgetT.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace AsyncAwaitBestPractices.UnitTests +{ + public class Tests_SafeFireAndForgetT : BaseTest + { + [SetUp] + public void BeforeEachTest() + { + SafeFireAndForgetExtensions.Initialize(false); + SafeFireAndForgetExtensions.SetDefaultExceptionHandling(null); + } + + [TearDown] + public void AfterEachTest() + { + SafeFireAndForgetExtensions.Initialize(false); + SafeFireAndForgetExtensions.SetDefaultExceptionHandling(null); + } + + [Test] + public async Task SafeFireAndForget_HandledException() + { + //Arrange + NullReferenceException exception = null; + + //Act + NoParameterDelayedNullReferenceExceptionTask().SafeFireAndForget(onException: ex => exception = ex); + await NoParameterTask(); + await NoParameterTask(); + + //Assert + Assert.IsNotNull(exception); + } + + [Test] + public async Task SafeFireAndForgetT_SetDefaultExceptionHandling_NoParams() + { + //Arrange + Exception exception = null; + SafeFireAndForgetExtensions.SetDefaultExceptionHandling(ex => exception = ex); + + //Act + NoParameterDelayedNullReferenceExceptionTask().SafeFireAndForget(); + await NoParameterTask(); + await NoParameterTask(); + + //Assert + Assert.IsNotNull(exception); + + SafeFireAndForgetExtensions.SetDefaultExceptionHandling(null); + } + + [Test] + public async Task SafeFireAndForgetT_SetDefaultExceptionHandling_WithParams() + { + //Arrange + Exception exception1 = null; + NullReferenceException exception2 = null; + SafeFireAndForgetExtensions.SetDefaultExceptionHandling(ex => exception1 = ex); + + //Act + NoParameterDelayedNullReferenceExceptionTask().SafeFireAndForget(onException: ex => exception2 = ex); + await NoParameterTask(); + await NoParameterTask(); + + //Assert + Assert.IsNotNull(exception1); + Assert.IsNotNull(exception2); + + SafeFireAndForgetExtensions.SetDefaultExceptionHandling(null); + } + } +} diff --git a/Src/AsyncAwaitBestPractices.UnitTests/Tests_SafeFireAndForget.cs b/Src/AsyncAwaitBestPractices.UnitTests/Tests_SafeFireAndForget.cs index 1ae5f91..b0444ec 100644 --- a/Src/AsyncAwaitBestPractices.UnitTests/Tests_SafeFireAndForget.cs +++ b/Src/AsyncAwaitBestPractices.UnitTests/Tests_SafeFireAndForget.cs @@ -1,4 +1,6 @@ using System; +using System.Reflection; +using System.Threading; using System.Threading.Tasks; using NUnit.Framework; @@ -6,6 +8,20 @@ namespace AsyncAwaitBestPractices.UnitTests { public class Tests_SafeFireAndForget : BaseTest { + [SetUp] + public void BeforeEachTest() + { + SafeFireAndForgetExtensions.Initialize(false); + SafeFireAndForgetExtensions.SetDefaultExceptionHandling(null); + } + + [TearDown] + public void AfterEachTest() + { + SafeFireAndForgetExtensions.Initialize(false); + SafeFireAndForgetExtensions.SetDefaultExceptionHandling(null); + } + [Test] public async Task SafeFireAndForget_HandledException() { @@ -13,12 +29,50 @@ public async Task SafeFireAndForget_HandledException() Exception exception = null; //Act - NoParameterDelayedExceptionTask().SafeFireAndForget(onException: ex => exception = ex); + NoParameterDelayedNullReferenceExceptionTask().SafeFireAndForget(onException: ex => exception = ex); + await NoParameterTask(); + await NoParameterTask(); + + //Assert + Assert.IsNotNull(exception); + } + + [Test] + public async Task SafeFireAndForget_SetDefaultExceptionHandling_NoParams() + { + //Arrange + Exception exception = null; + SafeFireAndForgetExtensions.SetDefaultExceptionHandling(ex => exception = ex); + + //Act + NoParameterDelayedNullReferenceExceptionTask().SafeFireAndForget(); await NoParameterTask(); await NoParameterTask(); //Assert Assert.IsNotNull(exception); + + SafeFireAndForgetExtensions.SetDefaultExceptionHandling(null); + } + + [Test] + public async Task SafeFireAndForget_SetDefaultExceptionHandling_WithParams() + { + //Arrange + Exception exception1 = null; + Exception exception2 = null; + SafeFireAndForgetExtensions.SetDefaultExceptionHandling(ex => exception1 = ex); + + //Act + NoParameterDelayedNullReferenceExceptionTask().SafeFireAndForget(onException: ex => exception2 = ex); + await NoParameterTask(); + await NoParameterTask(); + + //Assert + Assert.IsNotNull(exception1); + Assert.IsNotNull(exception2); + + SafeFireAndForgetExtensions.SetDefaultExceptionHandling(null); } } } diff --git a/Src/AsyncAwaitBestPractices/AsyncAwaitBestPractices.csproj b/Src/AsyncAwaitBestPractices/AsyncAwaitBestPractices.csproj index b178500..924bee5 100644 --- a/Src/AsyncAwaitBestPractices/AsyncAwaitBestPractices.csproj +++ b/Src/AsyncAwaitBestPractices/AsyncAwaitBestPractices.csproj @@ -10,6 +10,6 @@ bin\Release\netstandard1.0\AsyncAwaitBestPractices.xml - + \ No newline at end of file diff --git a/Src/AsyncAwaitBestPractices/SafeFireAndForgetExtensions.cs b/Src/AsyncAwaitBestPractices/SafeFireAndForgetExtensions.cs new file mode 100644 index 0000000..8636781 --- /dev/null +++ b/Src/AsyncAwaitBestPractices/SafeFireAndForgetExtensions.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading.Tasks; + +namespace AsyncAwaitBestPractices +{ + /// + /// Extension methods for System.Threading.Tasks.Task + /// + public static class SafeFireAndForgetExtensions + { + static Action _onException; + static bool _shouldAlwaysThrowException; + + /// + /// Safely execute the Task without waiting for it to complete before moving to the next line of code; commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async Void": https://johnthiriet.com/removing-async-void/. + /// + /// Task. + /// If set to true, continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false, continue on a different context; this will allow the Synchronization Context to continue on a different thread + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + public static void SafeFireAndForget(this Task task, bool continueOnCapturedContext = false, Action onException = null) => HandleSafeFireAndForget(task, continueOnCapturedContext, onException); + + /// + /// Safely execute the Task without waiting for it to complete before moving to the next line of code; commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async Void": https://johnthiriet.com/removing-async-void/. + /// + /// Task. + /// If set to true, continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false, continue on a different context; this will allow the Synchronization Context to continue on a different thread + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// Exception type. If an exception is thrown of a different type, it will not be handled + public static void SafeFireAndForget(this Task task, bool continueOnCapturedContext = false, Action onException = null) where TException : Exception => HandleSafeFireAndForget(task, continueOnCapturedContext, onException); + + /// + /// Initialize SafeFireAndForget to always rethrow an exception. + /// + /// Warning: When true, there is no way to catch this exception and it will always result in a crash. Recommend only using for debugging purposes. + /// + /// If set to true, after the exception has been caught and handled, the exception will always be rethrown. + public static void Initialize(in bool shouldAlwaysThrowException = false) => _shouldAlwaysThrowException = shouldAlwaysThrowException; + + /// + /// Set the default actionfor SafeFireAndForget to handle every exception + /// + /// If an exception is thrown in the Task using SafeFireAndForget, onException will execute + public static void SetDefaultExceptionHandling(in Action onException) => _onException = onException; + +#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void + static async void HandleSafeFireAndForget(Task task, bool continueOnCapturedContext, Action onException) where TException : Exception + { + try + { + await task.ConfigureAwait(continueOnCapturedContext); + } + catch (TException ex) when (_onException != null || onException != null) + { + _onException?.Invoke(ex); + onException?.Invoke(ex); + + if (_shouldAlwaysThrowException) + throw; + } + } +#pragma warning restore RECS0165 // Asynchronous methods should return a Task instead of void + } +} diff --git a/Src/AsyncAwaitBestPractices/TaskExtensions.cs b/Src/AsyncAwaitBestPractices/TaskExtensions.cs deleted file mode 100644 index 0fdf502..0000000 --- a/Src/AsyncAwaitBestPractices/TaskExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace AsyncAwaitBestPractices -{ - /// - /// Extension methods for System.Threading.Tasks.Task - /// - public static class TaskExtensions - { - /// - /// Safely execute the Task without waiting for it to complete before moving to the next line of code; commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async Void": https://johnthiriet.com/removing-async-void/. - /// - /// Task. - /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread - /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown -#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void - public static async void SafeFireAndForget(this System.Threading.Tasks.Task task, bool continueOnCapturedContext = true, System.Action onException = null) -#pragma warning restore RECS0165 // Asynchronous methods should return a Task instead of void - { - try - { - await task.ConfigureAwait(continueOnCapturedContext); - } - catch (System.Exception ex) when (onException != null) - { - onException(ex); - } - } - } -} From 6e0b289983b10f78fe549c271657cceec7379eb8 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Mon, 1 Jul 2019 19:04:57 -0700 Subject: [PATCH 08/21] Updated Initialize XML --- Src/AsyncAwaitBestPractices/SafeFireAndForgetExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/AsyncAwaitBestPractices/SafeFireAndForgetExtensions.cs b/Src/AsyncAwaitBestPractices/SafeFireAndForgetExtensions.cs index 8636781..d7188c3 100644 --- a/Src/AsyncAwaitBestPractices/SafeFireAndForgetExtensions.cs +++ b/Src/AsyncAwaitBestPractices/SafeFireAndForgetExtensions.cs @@ -31,7 +31,7 @@ public static class SafeFireAndForgetExtensions /// /// Initialize SafeFireAndForget to always rethrow an exception. /// - /// Warning: When true, there is no way to catch this exception and it will always result in a crash. Recommend only using for debugging purposes. + /// Warning: When true, there is no way to catch this exception and it will always result in a crash. Recommended only for debugging purposes. /// /// If set to true, after the exception has been caught and handled, the exception will always be rethrown. public static void Initialize(in bool shouldAlwaysThrowException = false) => _shouldAlwaysThrowException = shouldAlwaysThrowException; From 776ebc6789cff9a0c3b08b1b1251958dd57ea3ea Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Tue, 2 Jul 2019 15:41:26 -0700 Subject: [PATCH 09/21] Updated README for `SafeFireAndForgetExtensions.Initialize` & `SafeFireAndForgetExtensions.SetDefaultExceptionHandling` --- README.md | 72 +++++++++++++++---- Src/AsyncAwaitBestPractices.MVVM.nuspec | 8 ++- .../AsyncCommand.cs | 4 +- Src/AsyncAwaitBestPractices.nuspec | 6 +- .../SafeFireAndForgetExtensions.cs | 12 ++-- 5 files changed, 76 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index f7477d0..30d32b5 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,13 @@ Inspired by [John Thiriet](https://github.com/johnthiriet)'s blog posts: [Removi Available on NuGet: https://www.nuget.org/packages/AsyncAwaitBestPractices/ - - `SafeFireAndForget` - - An extension method to safely fire-and-forget a `Task`: - - `WeakEventManager` +- `SafeFireAndForget` + - An extension method to safely fire-and-forget a `Task` + - Ensures the `Task` will rethrow an `Exception` if an `Exception` is caught in `IAsyncStateMachine.MoveNext()` +- `WeakEventManager` - Avoids memory leaks when events are not unsubscribed - Used by `AsyncCommand` and `AsyncCommand` - - [Usage instructions](#asyncawaitbestpractices-3) +- [Usage instructions](#asyncawaitbestpractices-3) ### AsyncAwaitBestPractices.MVVM @@ -112,15 +113,17 @@ Never, never, never, never, never use `.Result` or `.Wait()`: An extension method to safely fire-and-forget a `Task`: ```csharp -public static async void SafeFireAndForget(this System.Threading.Tasks.Task task, bool continueOnCapturedContext = true, System.Action onException = null) +public static async void SafeFireAndForget(this System.Threading.Tasks.Task task, bool continueOnCapturedContext = false, System.Action onException = null) ``` +#### Basic Usage + ```csharp void HandleButtonTapped(object sender, EventArgs e) { // Allows the async Task method to safely run on a different thread while not awaiting its completion - // If an exception is thrown, Console.WriteLine - ExampleAsyncMethod().SafeFireAndForget(onException: ex => Console.WriteLine(ex.ToString())); + // onException: If an Exception is thrown, print it to the Console + ExampleAsyncMethod().SafeFireAndForget(onException: ex => Console.WriteLine(ex)); // HandleButtonTapped continues execution here while `ExampleAsyncMethod()` is running on a different thread // ... @@ -132,11 +135,50 @@ async Task ExampleAsyncMethod() } ``` +#### Advanced Usage + +```csharp +void InitializeSafeFireAndForget() +{ + // Initialize SafeFireAndForget + // Only use `shouldAlwaysRethrowException: true` when you want `.SafeFireAndForget()` to always rethrow every exception. This is not recommended, because there is no way to catch an Exception rethrown by `SafeFireAndForget()`; `shouldAlwaysRethrowException: true` should **not** be used in Production/Release builds. + SafeFireAndForgetExtensions.Initialize(shouldAlwaysRethrowException: false); + + // SafeFireAndForget will print every exception to the Console + SafeFireAndForgetExtensions.SetDefaultExceptionHandling(ex => Console.WriteLine(ex)); +} + +void HandleButtonTapped(object sender, EventArgs e) +{ + // Allows the async Task method to safely run on a different thread while not awaiting its completion + + // onException: If a WebException is thrown, print its StatusCode to the Console. **Note**: If a non-WebException is thrown, it will not be handled by `onException` + + // Because we set `SetDefaultExceptionHandling` in `void InitializeSafeFireAndForget()`, the entire exception will also be printed to the Console + ExampleAsyncMethodThrowingAnException().SafeFireAndForget(onException: ex => + { + if(e.Response is HttpWebResponse webResponse) + Console.WriteLine($"Status Code: {webResponse.StatusCode}"); + }); + + // HandleButtonTapped continues execution here while `ExampleAsyncMethod()` is running on a different thread + // ... +} + +async Task ExampleAsyncMethodThrowingAnException() +{ + await Task.Delay(1000); + throw new WebException(); +} +``` + ### `WeakEventManager` -An event implementation that enables the [garbage collector to collect an object without needing to unsubscribe event handlers](http://paulstovell.com/blog/weakevents), inspired by [Xamarin.Forms.WeakEventManager](https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Core/WeakEventManager.cs). +An event implementation that enables the [garbage collector to collect an object without needing to unsubscribe event handlers](http://paulstovell.com/blog/weakevents). + +Inspired by [Xamarin.Forms.WeakEventManager](https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Core/WeakEventManager.cs). -Using `EventHandler`: +#### Using `EventHandler` ```csharp readonly WeakEventManager _canExecuteChangedEventManager = new WeakEventManager(); @@ -150,7 +192,7 @@ public event EventHandler CanExecuteChanged public void OnCanExecuteChanged() => _canExecuteChangedEventManager.HandleEvent(this, EventArgs.Empty, nameof(CanExecuteChanged)); ``` -Using `Delegate`: +#### Using `Delegate` ```csharp readonly WeakEventManager _propertyChangedEventManager = new WeakEventManager(); @@ -164,7 +206,7 @@ public event PropertyChangedEventHandler PropertyChanged public void OnPropertyChanged([CallerMemberName]string propertyName = "") => _propertyChangedEventManager.HandleEvent(this, new PropertyChangedEventArgs(propertyName), nameof(PropertyChanged)); ``` -Using `Action`: +#### Using `Action` ```csharp readonly WeakEventManager _weakActionEventManager = new WeakEventManager(); @@ -181,7 +223,7 @@ public void OnActionEvent(string message) => _weakActionEventManager.HandleEvent ### `WeakEventManager` An event implementation that enables the [garbage collector to collect an object without needing to unsubscribe event handlers](http://paulstovell.com/blog/weakevents), inspired by [Xamarin.Forms.WeakEventManager](https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Core/WeakEventManager.cs). -Using `EventHandler`: +#### Using `EventHandler` ```csharp readonly WeakEventManager _errorOcurredEventManager = new WeakEventManager(); @@ -195,7 +237,7 @@ public event EventHandler ErrorOcurred public void OnErrorOcurred(string message) => _errorOcurredEventManager.HandleEvent(this, message, nameof(ErrorOcurred)); ``` -Using `Action`: +#### Using `Action` ```csharp readonly WeakEventManager _weakActionEventManager = new WeakEventManager(); @@ -224,14 +266,14 @@ Allows for `Task` to safely be used asynchronously with `ICommand`: public AsyncCommand(Func execute, Func canExecute = null, Action onException = null, - bool continueOnCapturedContext = true) + bool continueOnCapturedContext = false) ``` ```csharp public AsyncCommand(Func execute, Func canExecute = null, Action onException = null, - bool continueOnCapturedContext = true) + bool continueOnCapturedContext = false) ``` ```csharp diff --git a/Src/AsyncAwaitBestPractices.MVVM.nuspec b/Src/AsyncAwaitBestPractices.MVVM.nuspec index 98d9010..c84b73c 100644 --- a/Src/AsyncAwaitBestPractices.MVVM.nuspec +++ b/Src/AsyncAwaitBestPractices.MVVM.nuspec @@ -2,7 +2,7 @@ AsyncAwaitBestPractices.MVVM - 3.0.0-pre1 + 3.0.0-pre2 Task Extensions for MVVM Brandon Minnick, John Thiriet Brandon Minnick @@ -14,11 +14,15 @@ Includes AsyncCommand and IAsyncCommand which allows ICommand to safely be used asynchronously with Task. task,fire and forget, threading, extensions, system.threading.tasks,async,await - + New In This Release: - Added support for `event Action` and `event Action<T>` + - Added support for `.SafeFireAndForget<TException>()` + - Added `SafeFireAndForgetExtensions.SetDefaultExceptionHandling(Action onException)` to set a default action for every call to `SafeFireAndForget` + - Added `SafeFireAndForgetExtensions.Initialize(bool shouldAlwaysRethrowException = false)`. When set to `true` will rethrow every exception caught by `SafeFireAndForget`. Warning: `SafeFireAndForgetExtensions.Initialize(true)` is only recommended for DEBUG environments. + - Breaking Change: Changed default value to `continueOnCapturedContext = false`. This improves performance by not requiring a context switch when `.SafeFireAndForget()` and `IAsyncCommand` have completed. Copyright (c) 2018 Brandon Minnick diff --git a/Src/AsyncAwaitBestPractices.MVVM/AsyncCommand.cs b/Src/AsyncAwaitBestPractices.MVVM/AsyncCommand.cs index fbaa23a..3d97146 100644 --- a/Src/AsyncAwaitBestPractices.MVVM/AsyncCommand.cs +++ b/Src/AsyncAwaitBestPractices.MVVM/AsyncCommand.cs @@ -28,7 +28,7 @@ public sealed class AsyncCommand : IAsyncCommand public AsyncCommand(Func execute, Func canExecute = null, Action onException = null, - bool continueOnCapturedContext = true) + bool continueOnCapturedContext = false) { _execute = execute ?? throw new ArgumentNullException(nameof(execute), $"{nameof(execute)} cannot be null"); _canExecute = canExecute ?? (_ => true); @@ -104,7 +104,7 @@ public sealed class AsyncCommand : IAsyncCommand public AsyncCommand(Func execute, Func canExecute = null, Action onException = null, - bool continueOnCapturedContext = true) + bool continueOnCapturedContext = false) { _execute = execute ?? throw new ArgumentNullException(nameof(execute), $"{nameof(execute)} cannot be null"); _canExecute = canExecute ?? (_ => true); diff --git a/Src/AsyncAwaitBestPractices.nuspec b/Src/AsyncAwaitBestPractices.nuspec index 707ec77..a25202a 100644 --- a/Src/AsyncAwaitBestPractices.nuspec +++ b/Src/AsyncAwaitBestPractices.nuspec @@ -2,7 +2,7 @@ AsyncAwaitBestPractices - 3.0.0-pre1 + 3.0.0-pre2 Task Extensions for System.Threading.Tasks Brandon Minnick, John Thiriet Brandon Minnick @@ -20,6 +20,10 @@ New In This Release: - Added support for `event Action` and `event Action<T>` + - Added support for `.SafeFireAndForget<TException>()` + - Added `SafeFireAndForgetExtensions.SetDefaultExceptionHandling(Action onException)` to set a default action for every call to `SafeFireAndForget` + - Added `SafeFireAndForgetExtensions.Initialize(bool shouldAlwaysRethrowException = false)`. When set to `true` will rethrow every exception caught by `SafeFireAndForget`. Warning: `SafeFireAndForgetExtensions.Initialize(true)` is only recommended for DEBUG environments. + - Breaking Change: Changed default value to `continueOnCapturedContext = false`. This improves performance by not requiring a context switch when `.SafeFireAndForget()` has completed. Copyright (c) 2018 Brandon Minnick diff --git a/Src/AsyncAwaitBestPractices/SafeFireAndForgetExtensions.cs b/Src/AsyncAwaitBestPractices/SafeFireAndForgetExtensions.cs index d7188c3..39e266e 100644 --- a/Src/AsyncAwaitBestPractices/SafeFireAndForgetExtensions.cs +++ b/Src/AsyncAwaitBestPractices/SafeFireAndForgetExtensions.cs @@ -5,11 +5,11 @@ namespace AsyncAwaitBestPractices { /// /// Extension methods for System.Threading.Tasks.Task - /// + /// public static class SafeFireAndForgetExtensions { static Action _onException; - static bool _shouldAlwaysThrowException; + static bool _shouldAlwaysRethrowException; /// /// Safely execute the Task without waiting for it to complete before moving to the next line of code; commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async Void": https://johnthiriet.com/removing-async-void/. @@ -29,12 +29,12 @@ public static class SafeFireAndForgetExtensions public static void SafeFireAndForget(this Task task, bool continueOnCapturedContext = false, Action onException = null) where TException : Exception => HandleSafeFireAndForget(task, continueOnCapturedContext, onException); /// - /// Initialize SafeFireAndForget to always rethrow an exception. + /// Initialize SafeFireAndForget /// /// Warning: When true, there is no way to catch this exception and it will always result in a crash. Recommended only for debugging purposes. /// - /// If set to true, after the exception has been caught and handled, the exception will always be rethrown. - public static void Initialize(in bool shouldAlwaysThrowException = false) => _shouldAlwaysThrowException = shouldAlwaysThrowException; + /// If set to true, after the exception has been caught and handled, the exception will always be rethrown. + public static void Initialize(in bool shouldAlwaysRethrowException = false) => _shouldAlwaysRethrowException = shouldAlwaysRethrowException; /// /// Set the default actionfor SafeFireAndForget to handle every exception @@ -54,7 +54,7 @@ static async void HandleSafeFireAndForget(Task task, bool continueOn _onException?.Invoke(ex); onException?.Invoke(ex); - if (_shouldAlwaysThrowException) + if (_shouldAlwaysRethrowException) throw; } } From 8caee9745bcdb3767648356627478eee30adbe20 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Tue, 2 Jul 2019 15:44:50 -0700 Subject: [PATCH 10/21] Fixed XML errors --- Src/AsyncAwaitBestPractices.MVVM.nuspec | 2 +- Src/AsyncAwaitBestPractices.nuspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Src/AsyncAwaitBestPractices.MVVM.nuspec b/Src/AsyncAwaitBestPractices.MVVM.nuspec index c84b73c..2682d2a 100644 --- a/Src/AsyncAwaitBestPractices.MVVM.nuspec +++ b/Src/AsyncAwaitBestPractices.MVVM.nuspec @@ -20,7 +20,7 @@ New In This Release: - Added support for `event Action` and `event Action<T>` - Added support for `.SafeFireAndForget<TException>()` - - Added `SafeFireAndForgetExtensions.SetDefaultExceptionHandling(Action onException)` to set a default action for every call to `SafeFireAndForget` + - Added `SafeFireAndForgetExtensions.SetDefaultExceptionHandling(Action<Exception> onException)` to set a default action for every call to `SafeFireAndForget` - Added `SafeFireAndForgetExtensions.Initialize(bool shouldAlwaysRethrowException = false)`. When set to `true` will rethrow every exception caught by `SafeFireAndForget`. Warning: `SafeFireAndForgetExtensions.Initialize(true)` is only recommended for DEBUG environments. - Breaking Change: Changed default value to `continueOnCapturedContext = false`. This improves performance by not requiring a context switch when `.SafeFireAndForget()` and `IAsyncCommand` have completed. diff --git a/Src/AsyncAwaitBestPractices.nuspec b/Src/AsyncAwaitBestPractices.nuspec index a25202a..30f0e8d 100644 --- a/Src/AsyncAwaitBestPractices.nuspec +++ b/Src/AsyncAwaitBestPractices.nuspec @@ -21,7 +21,7 @@ New In This Release: - Added support for `event Action` and `event Action<T>` - Added support for `.SafeFireAndForget<TException>()` - - Added `SafeFireAndForgetExtensions.SetDefaultExceptionHandling(Action onException)` to set a default action for every call to `SafeFireAndForget` + - Added `SafeFireAndForgetExtensions.SetDefaultExceptionHandling(Action<Exception> onException)` to set a default action for every call to `SafeFireAndForget` - Added `SafeFireAndForgetExtensions.Initialize(bool shouldAlwaysRethrowException = false)`. When set to `true` will rethrow every exception caught by `SafeFireAndForget`. Warning: `SafeFireAndForgetExtensions.Initialize(true)` is only recommended for DEBUG environments. - Breaking Change: Changed default value to `continueOnCapturedContext = false`. This improves performance by not requiring a context switch when `.SafeFireAndForget()` has completed. From d4b9c5190db71ce3e0a375c8e323e187be50aaf0 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Wed, 3 Jul 2019 10:30:19 -0700 Subject: [PATCH 11/21] Added CanExecuteChanged Tests --- .../BaseTest.cs | 1 + .../Tests_AsyncCommand.cs | 32 ++++++++++++++++++- .../Tests_IAsyncCommand.cs | 4 +-- .../Tests_ICommand.cs | 30 ++++++++++++----- 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/Src/AsyncAwaitBestPractices.UnitTests/BaseTest.cs b/Src/AsyncAwaitBestPractices.UnitTests/BaseTest.cs index d38ba5c..6d8ef74 100644 --- a/Src/AsyncAwaitBestPractices.UnitTests/BaseTest.cs +++ b/Src/AsyncAwaitBestPractices.UnitTests/BaseTest.cs @@ -46,6 +46,7 @@ protected async Task IntParameterDelayedNullReferenceExceptionTask(int delay) protected bool CanExecuteTrue(object parameter) => true; protected bool CanExecuteFalse(object parameter) => false; + protected bool CanExecuteDynamic(object booleanParameter) => (bool)booleanParameter; #endregion } } diff --git a/Src/AsyncAwaitBestPractices.UnitTests/Tests_AsyncCommand.cs b/Src/AsyncAwaitBestPractices.UnitTests/Tests_AsyncCommand.cs index 9db5cca..439a457 100644 --- a/Src/AsyncAwaitBestPractices.UnitTests/Tests_AsyncCommand.cs +++ b/Src/AsyncAwaitBestPractices.UnitTests/Tests_AsyncCommand.cs @@ -106,6 +106,36 @@ public void AsyncCommand_NoParameter_CanExecuteFalse_Test() //Assert Assert.False(command.CanExecute(null)); - } + } + + + [Test] + public void AsyncCommand_CanExecuteChanged_Test() + { + //Arrange + bool canCommandExecute = false; + bool didCanExecuteChangeFire = false; + AsyncCommand command = new AsyncCommand(NoParameterTask, commandCanExecute); + + Assert.False(command.CanExecute(null)); + command.CanExecuteChanged += handleCanExecuteChanged; + + void handleCanExecuteChanged(object sender, EventArgs e) => didCanExecuteChangeFire = true; + bool commandCanExecute(object parameter) => canCommandExecute; + + //Act + canCommandExecute = true; + + //Assert + Assert.True(command.CanExecute(null)); + Assert.False(didCanExecuteChangeFire); + + //Act + command.RaiseCanExecuteChanged(); + + //Assert + Assert.True(didCanExecuteChangeFire); + Assert.True(command.CanExecute(null)); + } } } diff --git a/Src/AsyncAwaitBestPractices.UnitTests/Tests_IAsyncCommand.cs b/Src/AsyncAwaitBestPractices.UnitTests/Tests_IAsyncCommand.cs index c491adb..29e460a 100644 --- a/Src/AsyncAwaitBestPractices.UnitTests/Tests_IAsyncCommand.cs +++ b/Src/AsyncAwaitBestPractices.UnitTests/Tests_IAsyncCommand.cs @@ -63,7 +63,7 @@ public void IAsyncCommand_Parameter_CanExecuteFalse_Test() public void IAsyncCommand_NoParameter_CanExecuteTrue_Test() { //Arrange - IAsyncCommand command = new AsyncCommand(NoParameterTask, canExecute: CanExecuteTrue); + IAsyncCommand command = new AsyncCommand(NoParameterTask, CanExecuteTrue); //Act @@ -75,7 +75,7 @@ public void IAsyncCommand_NoParameter_CanExecuteTrue_Test() public void IAsyncCommand_NoParameter_CanExecuteFalse_Test() { //Arrange - IAsyncCommand command = new AsyncCommand(NoParameterTask, canExecute: CanExecuteFalse); + IAsyncCommand command = new AsyncCommand(NoParameterTask, CanExecuteFalse); //Act diff --git a/Src/AsyncAwaitBestPractices.UnitTests/Tests_ICommand.cs b/Src/AsyncAwaitBestPractices.UnitTests/Tests_ICommand.cs index c00b56a..7f8061f 100644 --- a/Src/AsyncAwaitBestPractices.UnitTests/Tests_ICommand.cs +++ b/Src/AsyncAwaitBestPractices.UnitTests/Tests_ICommand.cs @@ -5,7 +5,7 @@ namespace AsyncAwaitBestPractices.UnitTests { - public class Tests_ICommand : BaseTest + public class Tests_ICommand : BaseTest { [TestCase(500)] [TestCase(default)] @@ -99,7 +99,7 @@ public void ICommand_Parameter_CanExecuteTrue_Test() //Act //Assert - Assert.IsTrue(command.CanExecute(null)); + Assert.True(command.CanExecute(null)); } [Test] @@ -115,27 +115,41 @@ public void ICommand_Parameter_CanExecuteFalse_Test() } [Test] - public void ICommand_NoParameter_CanExecuteTrue_Test() + public void ICommand_NoParameter_CanExecuteFalse_Test() { //Arrange - ICommand command = new AsyncCommand(NoParameterTask, CanExecuteTrue); + ICommand command = new AsyncCommand(NoParameterTask, CanExecuteFalse); //Act //Assert - Assert.IsTrue(command.CanExecute(null)); + Assert.False(command.CanExecute(null)); } [Test] - public void ICommand_NoParameter_CanExecuteFalse_Test() + public void ICommand_Parameter_CanExecuteDynamic_Test() { //Arrange - ICommand command = new AsyncCommand(NoParameterTask, CanExecuteFalse); + ICommand command = new AsyncCommand(IntParameterTask, CanExecuteDynamic); //Act //Assert - Assert.False(command.CanExecute(null)); + Assert.True(command.CanExecute(true)); + Assert.False(command.CanExecute(false)); + } + + [Test] + public void ICommand_Parameter_CanExecuteChanged_Test() + { + //Arrange + ICommand command = new AsyncCommand(IntParameterTask, CanExecuteDynamic); + + //Act + + //Assert + Assert.True(command.CanExecute(true)); + Assert.False(command.CanExecute(false)); } } } From 4ce5950a578e5504f34b32dc75925d826d74ccd0 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Wed, 3 Jul 2019 10:38:48 -0700 Subject: [PATCH 12/21] Update Tests_AsyncCommand.cs --- Src/AsyncAwaitBestPractices.UnitTests/Tests_AsyncCommand.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Src/AsyncAwaitBestPractices.UnitTests/Tests_AsyncCommand.cs b/Src/AsyncAwaitBestPractices.UnitTests/Tests_AsyncCommand.cs index 439a457..9328490 100644 --- a/Src/AsyncAwaitBestPractices.UnitTests/Tests_AsyncCommand.cs +++ b/Src/AsyncAwaitBestPractices.UnitTests/Tests_AsyncCommand.cs @@ -115,14 +115,15 @@ public void AsyncCommand_CanExecuteChanged_Test() //Arrange bool canCommandExecute = false; bool didCanExecuteChangeFire = false; - AsyncCommand command = new AsyncCommand(NoParameterTask, commandCanExecute); - Assert.False(command.CanExecute(null)); + AsyncCommand command = new AsyncCommand(NoParameterTask, commandCanExecute); command.CanExecuteChanged += handleCanExecuteChanged; void handleCanExecuteChanged(object sender, EventArgs e) => didCanExecuteChangeFire = true; bool commandCanExecute(object parameter) => canCommandExecute; + Assert.False(command.CanExecute(null)); + //Act canCommandExecute = true; From a046d594a78ec48a25717e8ae97be08167437f08 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 7 Jul 2019 13:06:14 -0700 Subject: [PATCH 13/21] Added support for InnerExceptions to InvalidCommandParameterException --- Src/AsyncAwaitBestPractices.MVVM.nuspec | 5 ++-- .../InvalidCommandParameterException.cs | 23 ++++++++++++++++++- Src/AsyncAwaitBestPractices.nuspec | 2 +- .../WeakEventManager/WeakEventManager.cs | 4 ++-- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Src/AsyncAwaitBestPractices.MVVM.nuspec b/Src/AsyncAwaitBestPractices.MVVM.nuspec index 2682d2a..dd75eba 100644 --- a/Src/AsyncAwaitBestPractices.MVVM.nuspec +++ b/Src/AsyncAwaitBestPractices.MVVM.nuspec @@ -2,7 +2,7 @@ AsyncAwaitBestPractices.MVVM - 3.0.0-pre2 + 3.0.0-pre3 Task Extensions for MVVM Brandon Minnick, John Thiriet Brandon Minnick @@ -14,7 +14,7 @@ Includes AsyncCommand and IAsyncCommand which allows ICommand to safely be used asynchronously with Task. task,fire and forget, threading, extensions, system.threading.tasks,async,await - + New In This Release: @@ -22,6 +22,7 @@ - Added support for `.SafeFireAndForget<TException>()` - Added `SafeFireAndForgetExtensions.SetDefaultExceptionHandling(Action<Exception> onException)` to set a default action for every call to `SafeFireAndForget` - Added `SafeFireAndForgetExtensions.Initialize(bool shouldAlwaysRethrowException = false)`. When set to `true` will rethrow every exception caught by `SafeFireAndForget`. Warning: `SafeFireAndForgetExtensions.Initialize(true)` is only recommended for DEBUG environments. + - Added support for `Exception innerException` to `InvalidCommandParameterException` - Breaking Change: Changed default value to `continueOnCapturedContext = false`. This improves performance by not requiring a context switch when `.SafeFireAndForget()` and `IAsyncCommand` have completed. Copyright (c) 2018 Brandon Minnick diff --git a/Src/AsyncAwaitBestPractices.MVVM/InvalidCommandParameterException.cs b/Src/AsyncAwaitBestPractices.MVVM/InvalidCommandParameterException.cs index 14248ce..7e33fbe 100644 --- a/Src/AsyncAwaitBestPractices.MVVM/InvalidCommandParameterException.cs +++ b/Src/AsyncAwaitBestPractices.MVVM/InvalidCommandParameterException.cs @@ -12,7 +12,28 @@ public class InvalidCommandParameterException : Exception /// /// Excpected parameter type for AsyncCommand.Execute. /// Actual parameter type for AsyncCommand.Execute. - public InvalidCommandParameterException(Type excpectedType, Type actualType) : base(CreateErrorMessage(excpectedType, actualType)) + /// Inner Exception + public InvalidCommandParameterException(Type excpectedType, Type actualType, Exception innerException) : this(CreateErrorMessage(excpectedType, actualType), innerException) + { + + } + + /// + /// Initializes a new instance of the class. + /// + /// Excpected parameter type for AsyncCommand.Execute. + /// Actual parameter type for AsyncCommand.Execute. + public InvalidCommandParameterException(Type excpectedType, Type actualType) : this(CreateErrorMessage(excpectedType, actualType)) + { + + } + + /// + /// Initializes a new instance of the class. + /// + /// Exception Message + /// Inner Exception + public InvalidCommandParameterException(string message, Exception innerException) : base(message, innerException) { } diff --git a/Src/AsyncAwaitBestPractices.nuspec b/Src/AsyncAwaitBestPractices.nuspec index 30f0e8d..ab6f320 100644 --- a/Src/AsyncAwaitBestPractices.nuspec +++ b/Src/AsyncAwaitBestPractices.nuspec @@ -2,7 +2,7 @@ AsyncAwaitBestPractices - 3.0.0-pre2 + 3.0.0-pre3 Task Extensions for System.Threading.Tasks Brandon Minnick, John Thiriet Brandon Minnick diff --git a/Src/AsyncAwaitBestPractices/WeakEventManager/WeakEventManager.cs b/Src/AsyncAwaitBestPractices/WeakEventManager/WeakEventManager.cs index 5d98ab8..8fc7a7e 100644 --- a/Src/AsyncAwaitBestPractices/WeakEventManager/WeakEventManager.cs +++ b/Src/AsyncAwaitBestPractices/WeakEventManager/WeakEventManager.cs @@ -80,7 +80,7 @@ public void RemoveEventHandler(Action action, [CallerMemberName] str } /// - /// Executes the event EventHandler + /// Executes the event EventHandler /// /// Sender /// Event arguments @@ -89,7 +89,7 @@ public void HandleEvent(object sender, TEventArgs eventArgs, string eventName) = EventManagerService.HandleEvent(eventName, sender, eventArgs, _eventHandlers); /// - /// Executes the event Action + /// Executes the event Action /// /// Event arguments /// Event name From ecb56879b680873f7b7ef3143a16ac97c2c3f95e Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sat, 13 Jul 2019 19:00:55 -0700 Subject: [PATCH 14/21] Updated DebugType --- Src/AsyncAwaitBestPractices.MVVM.nuspec | 2 +- .../AsyncAwaitBestPractices.MVVM.csproj | 3 ++- .../AsyncAwaitBestPractices.UnitTests.csproj | 3 ++- Src/AsyncAwaitBestPractices.nuspec | 2 +- .../AsyncAwaitBestPractices.csproj | 3 ++- Src/HackNews.Droid/HackerNews.Droid.csproj | 11 +++++------ Src/HackerNews.UITests/HackerNews.UITests.csproj | 2 +- Src/HackerNews.iOS/HackerNews.iOS.csproj | 11 +++++------ Src/HackerNews/HackerNews.csproj | 5 +++-- 9 files changed, 22 insertions(+), 20 deletions(-) diff --git a/Src/AsyncAwaitBestPractices.MVVM.nuspec b/Src/AsyncAwaitBestPractices.MVVM.nuspec index dd75eba..fbba808 100644 --- a/Src/AsyncAwaitBestPractices.MVVM.nuspec +++ b/Src/AsyncAwaitBestPractices.MVVM.nuspec @@ -2,7 +2,7 @@ AsyncAwaitBestPractices.MVVM - 3.0.0-pre3 + 3.0.0-pre4 Task Extensions for MVVM Brandon Minnick, John Thiriet Brandon Minnick diff --git a/Src/AsyncAwaitBestPractices.MVVM/AsyncAwaitBestPractices.MVVM.csproj b/Src/AsyncAwaitBestPractices.MVVM/AsyncAwaitBestPractices.MVVM.csproj index 2dae1c7..afa6339 100644 --- a/Src/AsyncAwaitBestPractices.MVVM/AsyncAwaitBestPractices.MVVM.csproj +++ b/Src/AsyncAwaitBestPractices.MVVM/AsyncAwaitBestPractices.MVVM.csproj @@ -3,10 +3,11 @@ netstandard2.0 latest + True true - full + portable bin\Release\netstandard2.0\AsyncAwaitBestPractices.MVVM.xml diff --git a/Src/AsyncAwaitBestPractices.UnitTests/AsyncAwaitBestPractices.UnitTests.csproj b/Src/AsyncAwaitBestPractices.UnitTests/AsyncAwaitBestPractices.UnitTests.csproj index 17505d9..1ba2c36 100644 --- a/Src/AsyncAwaitBestPractices.UnitTests/AsyncAwaitBestPractices.UnitTests.csproj +++ b/Src/AsyncAwaitBestPractices.UnitTests/AsyncAwaitBestPractices.UnitTests.csproj @@ -4,11 +4,12 @@ netcoreapp2.1 latest false + True - + diff --git a/Src/AsyncAwaitBestPractices.nuspec b/Src/AsyncAwaitBestPractices.nuspec index ab6f320..33ffe58 100644 --- a/Src/AsyncAwaitBestPractices.nuspec +++ b/Src/AsyncAwaitBestPractices.nuspec @@ -2,7 +2,7 @@ AsyncAwaitBestPractices - 3.0.0-pre3 + 3.0.0-pre4 Task Extensions for System.Threading.Tasks Brandon Minnick, John Thiriet Brandon Minnick diff --git a/Src/AsyncAwaitBestPractices/AsyncAwaitBestPractices.csproj b/Src/AsyncAwaitBestPractices/AsyncAwaitBestPractices.csproj index 924bee5..71f9f6b 100644 --- a/Src/AsyncAwaitBestPractices/AsyncAwaitBestPractices.csproj +++ b/Src/AsyncAwaitBestPractices/AsyncAwaitBestPractices.csproj @@ -3,10 +3,11 @@ netstandard1.0 latest + True true - full + portable bin\Release\netstandard1.0\AsyncAwaitBestPractices.xml diff --git a/Src/HackNews.Droid/HackerNews.Droid.csproj b/Src/HackNews.Droid/HackerNews.Droid.csproj index 3132ee8..043afe7 100644 --- a/Src/HackNews.Droid/HackerNews.Droid.csproj +++ b/Src/HackNews.Droid/HackerNews.Droid.csproj @@ -17,10 +17,11 @@ Assets false armeabi-v7a;x86;arm64-v8a;x86_64 + True true - full + portable false bin\Debug DEBUG; @@ -32,7 +33,7 @@ true - pdbonly + true bin\Release prompt @@ -54,12 +55,10 @@ - + - - 1.1.0 - + diff --git a/Src/HackerNews.UITests/HackerNews.UITests.csproj b/Src/HackerNews.UITests/HackerNews.UITests.csproj index c177170..d25a5fc 100644 --- a/Src/HackerNews.UITests/HackerNews.UITests.csproj +++ b/Src/HackerNews.UITests/HackerNews.UITests.csproj @@ -32,7 +32,7 @@ - + diff --git a/Src/HackerNews.iOS/HackerNews.iOS.csproj b/Src/HackerNews.iOS/HackerNews.iOS.csproj index dd9acba..76d8dab 100644 --- a/Src/HackerNews.iOS/HackerNews.iOS.csproj +++ b/Src/HackerNews.iOS/HackerNews.iOS.csproj @@ -9,10 +9,11 @@ HackerNews.iOS HackerNews.iOS Resources + True true - full + portable false bin\iPhoneSimulator\Debug DEBUG;ENABLE_TEST_CLOUD; @@ -30,7 +31,7 @@ x86 - pdbonly + true bin\iPhone\Release prompt @@ -89,12 +90,10 @@ - + - - 1.1.0 - + diff --git a/Src/HackerNews/HackerNews.csproj b/Src/HackerNews/HackerNews.csproj index 5cfd7d2..c2a08eb 100644 --- a/Src/HackerNews/HackerNews.csproj +++ b/Src/HackerNews/HackerNews.csproj @@ -3,12 +3,13 @@ netstandard2.0 latest + True - + - + From eac5c87756a1b1872df7dcf3ed51222e353ae8ff Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sat, 13 Jul 2019 19:04:42 -0700 Subject: [PATCH 15/21] Update AsyncAwaitBestPractices.MVVM.nuspec --- Src/AsyncAwaitBestPractices.MVVM.nuspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/AsyncAwaitBestPractices.MVVM.nuspec b/Src/AsyncAwaitBestPractices.MVVM.nuspec index fbba808..598f85b 100644 --- a/Src/AsyncAwaitBestPractices.MVVM.nuspec +++ b/Src/AsyncAwaitBestPractices.MVVM.nuspec @@ -14,7 +14,7 @@ Includes AsyncCommand and IAsyncCommand which allows ICommand to safely be used asynchronously with Task. task,fire and forget, threading, extensions, system.threading.tasks,async,await - + New In This Release: From 7d7bbecb6784dbf984627d8f04e67c71761fc1f5 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Tue, 16 Jul 2019 14:30:20 -0700 Subject: [PATCH 16/21] Update NuGet Packages --- Src/HackNews.Droid/HackerNews.Droid.csproj | 11 +++++++++-- Src/HackerNews.iOS/HackerNews.iOS.csproj | 3 ++- Src/HackerNews/HackerNews.csproj | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Src/HackNews.Droid/HackerNews.Droid.csproj b/Src/HackNews.Droid/HackerNews.Droid.csproj index 043afe7..ebc7ffe 100644 --- a/Src/HackNews.Droid/HackerNews.Droid.csproj +++ b/Src/HackNews.Droid/HackerNews.Droid.csproj @@ -30,6 +30,7 @@ Xamarin.Android.Net.AndroidClientHandler btls true + d8 true @@ -41,6 +42,12 @@ true Xamarin.Android.Net.AndroidClientHandler btls + true + true + true + true + d8 + r8 @@ -56,8 +63,8 @@ - - + + diff --git a/Src/HackerNews.iOS/HackerNews.iOS.csproj b/Src/HackerNews.iOS/HackerNews.iOS.csproj index 76d8dab..bd2a1cb 100644 --- a/Src/HackerNews.iOS/HackerNews.iOS.csproj +++ b/Src/HackerNews.iOS/HackerNews.iOS.csproj @@ -43,6 +43,7 @@ ARM64 NSUrlSessionHandler x86 + true pdbonly @@ -91,7 +92,7 @@ - + diff --git a/Src/HackerNews/HackerNews.csproj b/Src/HackerNews/HackerNews.csproj index c2a08eb..bff2d83 100644 --- a/Src/HackerNews/HackerNews.csproj +++ b/Src/HackerNews/HackerNews.csproj @@ -8,7 +8,7 @@ - + From 3be9694530fc6c3e746a1b6f5ea709a90601bd26 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 21 Jul 2019 13:03:09 -0700 Subject: [PATCH 17/21] Make Exception Classes Internal --- .../InvalidCommandParameterException.cs | 2 +- .../WeakEventManager/InvalidHandleEventException.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Src/AsyncAwaitBestPractices.MVVM/InvalidCommandParameterException.cs b/Src/AsyncAwaitBestPractices.MVVM/InvalidCommandParameterException.cs index 7e33fbe..dae77e7 100644 --- a/Src/AsyncAwaitBestPractices.MVVM/InvalidCommandParameterException.cs +++ b/Src/AsyncAwaitBestPractices.MVVM/InvalidCommandParameterException.cs @@ -5,7 +5,7 @@ namespace AsyncAwaitBestPractices.MVVM /// /// Represents errors that occur during IAsyncCommand execution. /// - public class InvalidCommandParameterException : Exception + class InvalidCommandParameterException : Exception { /// /// Initializes a new instance of the class. diff --git a/Src/AsyncAwaitBestPractices/WeakEventManager/InvalidHandleEventException.cs b/Src/AsyncAwaitBestPractices/WeakEventManager/InvalidHandleEventException.cs index 20704fa..a304aad 100644 --- a/Src/AsyncAwaitBestPractices/WeakEventManager/InvalidHandleEventException.cs +++ b/Src/AsyncAwaitBestPractices/WeakEventManager/InvalidHandleEventException.cs @@ -6,7 +6,7 @@ namespace AsyncAwaitBestPractices /// /// Represents errors that occur during WeakEventManager.HandleEvent execution. /// - public class InvalidHandleEventException : Exception + class InvalidHandleEventException : Exception { /// /// Initializes a new instance of the class. From a1e31bb7b31ca08fd6d997689dd6286c06e58bb5 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Fri, 26 Jul 2019 09:18:41 -0700 Subject: [PATCH 18/21] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 30d32b5..6677807 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ void HandleButtonTapped(object sender, EventArgs e) // onException: If a WebException is thrown, print its StatusCode to the Console. **Note**: If a non-WebException is thrown, it will not be handled by `onException` // Because we set `SetDefaultExceptionHandling` in `void InitializeSafeFireAndForget()`, the entire exception will also be printed to the Console - ExampleAsyncMethodThrowingAnException().SafeFireAndForget(onException: ex => + ExampleAsyncMethod().SafeFireAndForget(onException: ex => { if(e.Response is HttpWebResponse webResponse) Console.WriteLine($"Status Code: {webResponse.StatusCode}"); @@ -165,7 +165,7 @@ void HandleButtonTapped(object sender, EventArgs e) // ... } -async Task ExampleAsyncMethodThrowingAnException() +async Task ExampleAsyncMethod() { await Task.Delay(1000); throw new WebException(); From 50e03605e61fd6d5129e989071c8ff74fbd659b4 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Fri, 26 Jul 2019 09:21:08 -0700 Subject: [PATCH 19/21] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6677807..ea6b94a 100644 --- a/README.md +++ b/README.md @@ -284,13 +284,13 @@ public class ExampleClass ExampleAsyncCommand = new AsyncCommand(ExampleAsyncMethod); ExampleAsyncIntCommand = new AsyncCommand(ExampleAsyncMethodWithIntParameter); ExampleAsyncExceptionCommand = new AsyncCommand(ExampleAsyncMethodWithException, onException: ex => Console.WriteLine(ex.ToString())); - ExampleAsyncCommandNotReturningToTheCallingThread = new AsyncCommand(ExampleAsyncMethod, continueOnCapturedContext: false); + ExampleAsyncCommandNotReturningToTheCallingThread = new AsyncCommand(ExampleAsyncMethod, continueOnCapturedContext: true); } public IAsyncCommand ExampleAsyncCommand { get; } public IAsyncCommand ExampleAsyncIntCommand { get; } public IAsyncCommand ExampleAsyncExceptionCommand { get; } - public IAsyncCommand ExampleAsyncCommandNotReturningToTheCallingThread { get; } + public IAsyncCommand ExampleAsyncCommandReturningToTheCallingThread { get; } async Task ExampleAsyncMethod() { @@ -313,7 +313,7 @@ public class ExampleClass ExampleAsyncCommand.Execute(null); ExampleAsyncIntCommand.Execute(1000); ExampleAsyncExceptionCommand.Execute(null); - ExampleAsyncCommandNotReturningToTheCallingThread.Execute(null); + ExampleAsyncCommandReturningToTheCallingThread.Execute(null); } } ``` From 66605692a8156c2efc52f243c3a22ed6806e557d Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Fri, 26 Jul 2019 09:24:42 -0700 Subject: [PATCH 20/21] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ea6b94a..c177134 100644 --- a/README.md +++ b/README.md @@ -284,7 +284,7 @@ public class ExampleClass ExampleAsyncCommand = new AsyncCommand(ExampleAsyncMethod); ExampleAsyncIntCommand = new AsyncCommand(ExampleAsyncMethodWithIntParameter); ExampleAsyncExceptionCommand = new AsyncCommand(ExampleAsyncMethodWithException, onException: ex => Console.WriteLine(ex.ToString())); - ExampleAsyncCommandNotReturningToTheCallingThread = new AsyncCommand(ExampleAsyncMethod, continueOnCapturedContext: true); + ExampleAsyncCommandReturningToTheCallingThread = new AsyncCommand(ExampleAsyncMethod, continueOnCapturedContext: true); } public IAsyncCommand ExampleAsyncCommand { get; } From f66270526beaa2daefb9a25fd1538b757499d51d Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Tue, 30 Jul 2019 10:13:15 -0700 Subject: [PATCH 21/21] Added .NET Standard Compatibility to nuspec --- Src/AsyncAwaitBestPractices.MVVM.nuspec | 10 +++++----- Src/AsyncAwaitBestPractices.nuspec | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Src/AsyncAwaitBestPractices.MVVM.nuspec b/Src/AsyncAwaitBestPractices.MVVM.nuspec index 598f85b..f361ec4 100644 --- a/Src/AsyncAwaitBestPractices.MVVM.nuspec +++ b/Src/AsyncAwaitBestPractices.MVVM.nuspec @@ -2,7 +2,7 @@ AsyncAwaitBestPractices.MVVM - 3.0.0-pre4 + 3.0.0 Task Extensions for MVVM Brandon Minnick, John Thiriet Brandon Minnick @@ -14,7 +14,7 @@ Includes AsyncCommand and IAsyncCommand which allows ICommand to safely be used asynchronously with Task. task,fire and forget, threading, extensions, system.threading.tasks,async,await - + New In This Release: @@ -28,8 +28,8 @@ Copyright (c) 2018 Brandon Minnick - - - + + + \ No newline at end of file diff --git a/Src/AsyncAwaitBestPractices.nuspec b/Src/AsyncAwaitBestPractices.nuspec index 33ffe58..8ee83aa 100644 --- a/Src/AsyncAwaitBestPractices.nuspec +++ b/Src/AsyncAwaitBestPractices.nuspec @@ -2,7 +2,7 @@ AsyncAwaitBestPractices - 3.0.0-pre4 + 3.0.0 Task Extensions for System.Threading.Tasks Brandon Minnick, John Thiriet Brandon Minnick @@ -28,8 +28,8 @@ Copyright (c) 2018 Brandon Minnick - - - + + + \ No newline at end of file