From 23baecafbdea0812264be996b5ffbb581299dc51 Mon Sep 17 00:00:00 2001 From: Yann Zahringer Ferrando Date: Sat, 19 Sep 2020 00:47:54 +0200 Subject: [PATCH] AsyncCommand (#279) * WeakEventManager added * AsyncCommand added * cosmetics * Update XamarinCommunityToolkit/ObjectModel/IAsyncCommand.shared.cs * Update XamarinCommunityToolkit/ObjectModel/AsyncCommand.shared.cs * Update XamarinCommunityToolkit/Helpers/WeakEventManager.shared.cs * cosmetics * ArgumentNullException constructor test added * Lazy isNullableParameterType added * cosmetics * Update XamarinCommunityToolkit/ObjectModel/AsyncCommand.shared.cs * Update XamarinCommunityToolkit/ObjectModel/AsyncCommand.shared.cs * Update XamarinCommunityToolkit/ObjectModel/AsyncCommand.shared.cs * CanExecute tests converted to Theory * build fix * unit test fix * async void test fixed with semaphore * Add AsyncValueCommand & UnitTests * Update Unit Tests for xUnit * Delete AsyncCommand_Tests.cs * Added IAsyncCommand.IsExecuting, IAsyncCommand.AllowsMultipleExecutions * Add missing NuGet Packages * Added NuGet Packages * Fixed Failing Unit Tests Defer `async void`. This ensures InvalidCommandParameterException is thrown on the calling thread/context before reaching an async void method * Add BaseCommand, Update Unit Tests * Finish Unit Tests * Implement AsyncCommand * Implement WeakEventManager * Updated XML Documentation * Update XML Documentation * Add & Implement IAsyncCommand + IAsyncValueCommand * Add IAsyncCommand Tests, Add IAsyncValueCommand Tests * Fix Failing Tests * Removed Default Parameters * Update XamarinCommunityToolkit/Helpers/WeakEventManagerService.shared.cs Co-authored-by: Pedro Jesus * Update XamarinCommunityToolkit/ObjectModel/AsyncCommand.shared.cs Co-authored-by: Pedro Jesus * Update XamarinCommunityToolkit/ObjectModel/AsyncValueCommand.shared.cs Co-authored-by: Pedro Jesus * Update XamarinCommunityToolkit/Helpers/WeakEventManager.shared.cs Co-authored-by: Pedro Jesus * Update XamarinCommunityToolkit/Helpers/WeakEventManager.shared.cs Co-authored-by: Pedro Jesus * Update XamarinCommunityToolkit/Helpers/WeakEventManagerService.shared.cs Co-authored-by: Pedro Jesus * Update XamarinCommunityToolkit/Helpers/WeakEventManagerService.shared.cs Co-authored-by: Pedro Jesus * Update XamarinCommunityToolkit/Helpers/WeakEventManager.shared.cs Co-authored-by: Pedro Jesus * Implement ValueTuple for EventManagerService, Remove WeakEventManager from CameraView * merge build fix * Re-add .NET Standard 2.1 Support * null check cleanup * Increase NETCORE_TEST_VERSION to 3.1.x * Add null check Co-authored-by: Andrei Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Co-authored-by: Pedro Jesus --- .../BaseWeakEventManagerTests.cs | 24 ++ .../WeakEventManager_ActionT_Tests.cs | 242 ++++++++++++++ .../WeakEventManager_Action_Tests.cs | 211 ++++++++++++ .../WeakEventManager_Delegate_Tests.cs | 304 ++++++++++++++++++ .../WeakEventManager_EventHandlerT_Tests.cs | 281 ++++++++++++++++ .../WeakEventManager_EventHandler_Tests.cs | 293 +++++++++++++++++ .../AsyncCommandTests/AsyncCommand_Tests.cs | 189 +++++++++++ .../BaseAsyncCommandTests.cs | 6 + .../AsyncCommandTests/IAsyncCommand_Tests.cs | 183 +++++++++++ .../ICommand_AsyncCommand_Tests.cs | 261 +++++++++++++++ .../AsyncValueCommand_Tests.cs | 264 +++++++++++++++ .../BaseAsyncValueCommandTests.cs | 32 ++ .../IAsyncValueCommand_Tests.cs | 142 ++++++++ .../ICommand_AsyncValueCommand_Tests.cs | 285 ++++++++++++++++ .../ICommandTests/BaseCommandTests.cs | 56 ++++ .../Xamarin.CommunityToolkit.UnitTests.csproj | 1 + ...InvalidCommandParameterException.shared.cs | 55 ++++ .../InvalidHandleEventException.shared.cs | 22 ++ .../Helpers/Subscription.shared.cs | 19 ++ .../Helpers/WeakEventManager.shared.cs | 155 +++++++++ .../Helpers/WeakEventManagerService.shared.cs | 178 ++++++++++ .../ObjectModel/AsyncCommand.shared.cs | 101 ++++++ .../ObjectModel/AsyncValueCommand.cs | 8 + .../ObjectModel/AsyncValueCommand.shared.cs | 99 ++++++ .../ObjectModel/BaseAsyncCommand.shared.cs | 95 ++++++ .../BaseAsyncValueCommand.shared.cs | 95 ++++++ .../ObjectModel/BaseCommand.shared.cs | 90 ++++++ .../ObjectModel/IAsyncCommand.shared.cs | 71 ++++ .../ObjectModel/IAsyncValueCommand.shared.cs | 71 ++++ .../Views/CameraView/CameraView.shared.cs | 4 +- .../Views/Expander/Expander.shared.cs | 13 +- .../Xamarin.CommunityToolkit.csproj | 24 +- .../Pages/Base/BasePage.cs | 5 +- .../ViewModels/AboutViewModel.cs | 10 +- .../ViewModels/Base/BaseViewModel.cs | 13 +- .../ItemSelectedEventArgsViewModel.cs | 5 +- .../ItemTappedEventArgsViewModel.cs | 6 +- azure-pipelines.yml | 2 +- 38 files changed, 3892 insertions(+), 23 deletions(-) create mode 100644 XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/BaseWeakEventManagerTests.cs create mode 100644 XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_ActionT_Tests.cs create mode 100644 XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_Action_Tests.cs create mode 100644 XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_Delegate_Tests.cs create mode 100644 XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_EventHandlerT_Tests.cs create mode 100644 XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_EventHandler_Tests.cs create mode 100644 XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/AsyncCommand_Tests.cs create mode 100644 XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/BaseAsyncCommandTests.cs create mode 100644 XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/IAsyncCommand_Tests.cs create mode 100644 XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/ICommand_AsyncCommand_Tests.cs create mode 100644 XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/AsyncValueCommand_Tests.cs create mode 100644 XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/BaseAsyncValueCommandTests.cs create mode 100644 XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/IAsyncValueCommand_Tests.cs create mode 100644 XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/ICommand_AsyncValueCommand_Tests.cs create mode 100644 XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/BaseCommandTests.cs create mode 100644 XamarinCommunityToolkit/Exceptions/InvalidCommandParameterException.shared.cs create mode 100644 XamarinCommunityToolkit/Exceptions/InvalidHandleEventException.shared.cs create mode 100644 XamarinCommunityToolkit/Helpers/Subscription.shared.cs create mode 100644 XamarinCommunityToolkit/Helpers/WeakEventManager.shared.cs create mode 100644 XamarinCommunityToolkit/Helpers/WeakEventManagerService.shared.cs create mode 100644 XamarinCommunityToolkit/ObjectModel/AsyncCommand.shared.cs create mode 100644 XamarinCommunityToolkit/ObjectModel/AsyncValueCommand.cs create mode 100644 XamarinCommunityToolkit/ObjectModel/AsyncValueCommand.shared.cs create mode 100644 XamarinCommunityToolkit/ObjectModel/BaseAsyncCommand.shared.cs create mode 100644 XamarinCommunityToolkit/ObjectModel/BaseAsyncValueCommand.shared.cs create mode 100644 XamarinCommunityToolkit/ObjectModel/BaseCommand.shared.cs create mode 100644 XamarinCommunityToolkit/ObjectModel/IAsyncCommand.shared.cs create mode 100644 XamarinCommunityToolkit/ObjectModel/IAsyncValueCommand.shared.cs diff --git a/XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/BaseWeakEventManagerTests.cs b/XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/BaseWeakEventManagerTests.cs new file mode 100644 index 00000000..5c78b4db --- /dev/null +++ b/XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/BaseWeakEventManagerTests.cs @@ -0,0 +1,24 @@ +using System; +using Xamarin.CommunityToolkit.Helpers; + +namespace Xamarin.CommunityToolkit.UnitTests.Helpers.WeakEventManagerTests +{ + public class BaseWeakEventManagerTests + { + protected event EventHandler TestEvent + { + add => TestWeakEventManager.AddEventHandler(value); + remove => TestWeakEventManager.RemoveEventHandler(value); + } + + protected event EventHandler TestStringEvent + { + add => TestStringWeakEventManager.AddEventHandler(value); + remove => TestStringWeakEventManager.RemoveEventHandler(value); + } + + protected WeakEventManager TestWeakEventManager { get; } = new WeakEventManager(); + + protected WeakEventManager TestStringWeakEventManager { get; } = new WeakEventManager(); + } +} diff --git a/XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_ActionT_Tests.cs b/XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_ActionT_Tests.cs new file mode 100644 index 00000000..8c98111a --- /dev/null +++ b/XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_ActionT_Tests.cs @@ -0,0 +1,242 @@ +using System; +using Xamarin.CommunityToolkit.Exceptions; +using Xamarin.CommunityToolkit.Helpers; +using Xunit; + +namespace Xamarin.CommunityToolkit.UnitTests.Helpers.WeakEventManagerTests +{ + public class WeakEventManager_ActionT_Tests : BaseWeakEventManagerTests + { + readonly WeakEventManager actionEventManager = new WeakEventManager(); + + public event Action ActionEvent + { + add => actionEventManager.AddEventHandler(value); + remove => actionEventManager.RemoveEventHandler(value); + } + + [Fact] + public void WeakEventManagerActionT_HandleEvent_ValidImplementation() + { + // Arrange + ActionEvent += HandleDelegateTest; + var didEventFire = false; + + void HandleDelegateTest(string message) + { + Assert.NotNull(message); + Assert.NotEmpty(message); + + didEventFire = true; + ActionEvent -= HandleDelegateTest; + } + + // Act + actionEventManager.RaiseEvent("Test", nameof(ActionEvent)); + + // Assert + Assert.True(didEventFire); + } + + [Fact] + public void WeakEventManagerActionT_HandleEvent_InvalidHandleEventEventName() + { + // Arrange + ActionEvent += HandleDelegateTest; + var didEventFire = false; + + void HandleDelegateTest(string message) + { + Assert.NotNull(message); + Assert.NotEmpty(message); + + didEventFire = true; + } + + // Act + actionEventManager.RaiseEvent("Test", nameof(TestEvent)); + + // Assert + Assert.False(didEventFire); + ActionEvent -= HandleDelegateTest; + } + + [Fact] + public void WeakEventManagerActionT_UnassignedEvent() + { + // Arrange + var didEventFire = false; + + ActionEvent += HandleDelegateTest; + ActionEvent -= HandleDelegateTest; + void HandleDelegateTest(string message) + { + Assert.NotNull(message); + Assert.NotEmpty(message); + + didEventFire = true; + } + + // Act + actionEventManager.RaiseEvent("Test", nameof(ActionEvent)); + + // Assert + Assert.False(didEventFire); + } + + [Fact] + public void WeakEventManagerActionT_UnassignedEventManager() + { + // Arrange + var unassignedEventManager = new WeakEventManager(); + var didEventFire = false; + + ActionEvent += HandleDelegateTest; + void HandleDelegateTest(string message) + { + Assert.NotNull(message); + Assert.NotEmpty(message); + + didEventFire = true; + } + + // Act + unassignedEventManager.RaiseEvent(nameof(ActionEvent)); + + // Assert + Assert.False(didEventFire); + ActionEvent -= HandleDelegateTest; + } + + [Fact] + public void WeakEventManagerActionT_HandleEvent_InvalidHandleEvent() + { + // Arrange + ActionEvent += HandleDelegateTest; + var didEventFire = false; + + void HandleDelegateTest(string message) + { + Assert.NotNull(message); + Assert.NotEmpty(message); + + didEventFire = true; + } + + // Act + + // Assert + Assert.Throws(() => actionEventManager.RaiseEvent(this, "Test", nameof(ActionEvent))); + Assert.False(didEventFire); + ActionEvent -= HandleDelegateTest; + } + + [Fact] + public void WeakEventManagerActionT_AddEventHandler_NullHandler() + { + // Arrange + Action nullAction = null; + + // Act + + // Assert +#pragma warning disable CS8604 //Possible null reference argument for parameter + Assert.Throws(() => actionEventManager.AddEventHandler(nullAction, nameof(ActionEvent))); +#pragma warning restore CS8604 //Possible null reference argument for parameter + + } + + [Fact] + public void WeakEventManagerActionT_AddEventHandler_NullEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type + Assert.Throws(() => actionEventManager.AddEventHandler(s => { var temp = s; }, null)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerActionT_AddEventHandler_EmptyEventName() + { + // Arrange + Action nullAction = null; + + // Act + + // Assert +#pragma warning disable CS8604 //Possible null reference argument for parameter + Assert.Throws(() => actionEventManager.AddEventHandler(nullAction, string.Empty)); +#pragma warning restore CS8604 //Possible null reference argument for parameter + } + + [Fact] + public void WeakEventManagerActionT_AddEventHandler_WhitespaceEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type + Assert.Throws(() => actionEventManager.AddEventHandler(s => { var temp = s; }, " ")); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerActionT_RemoveEventHandler_NullHandler() + { + // Arrange + Action nullAction = null; + + // Act + + // Assert +#pragma warning disable CS8604 //Possible null reference argument for parameter + Assert.Throws(() => actionEventManager.RemoveEventHandler(nullAction)); +#pragma warning restore CS8604 + } + + [Fact] + public void WeakEventManagerActionT_RemoveEventHandler_NullEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type + Assert.Throws(() => actionEventManager.RemoveEventHandler(s => { var temp = s; }, null)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerActionT_RemoveEventHandler_EmptyEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type + Assert.Throws(() => actionEventManager.RemoveEventHandler(s => { var temp = s; }, string.Empty)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerActionT_RemoveEventHandler_WhiteSpaceEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type + Assert.Throws(() => actionEventManager.RemoveEventHandler(s => { var temp = s; }, " ")); +#pragma warning restore CS8625 + } + } +} diff --git a/XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_Action_Tests.cs b/XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_Action_Tests.cs new file mode 100644 index 00000000..de271d7b --- /dev/null +++ b/XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_Action_Tests.cs @@ -0,0 +1,211 @@ +using System; +using Xamarin.CommunityToolkit.Exceptions; +using Xamarin.CommunityToolkit.Helpers; +using Xunit; + +namespace Xamarin.CommunityToolkit.UnitTests.Helpers.WeakEventManagerTests +{ + public class WeakEventManager_Action_Tests : BaseWeakEventManagerTests + { + readonly WeakEventManager actionEventManager = new WeakEventManager(); + + public event Action ActionEvent + { + add => actionEventManager.AddEventHandler(value); + remove => actionEventManager.RemoveEventHandler(value); + } + + [Fact] + public void WeakEventManagerAction_HandleEvent_ValidImplementation() + { + // Arrange + ActionEvent += HandleDelegateTest; + var didEventFire = false; + + void HandleDelegateTest() + { + didEventFire = true; + ActionEvent -= HandleDelegateTest; + } + + // Act + actionEventManager.RaiseEvent(nameof(ActionEvent)); + + // Assert + Assert.True(didEventFire); + } + + [Fact] + public void WeakEventManagerAction_HandleEvent_InvalidHandleEventEventName() + { + // Arrange + ActionEvent += HandleDelegateTest; + var didEventFire = false; + + void HandleDelegateTest() => didEventFire = true; + + // Act + actionEventManager.RaiseEvent(nameof(TestStringEvent)); + + // Assert + Assert.False(didEventFire); + ActionEvent -= HandleDelegateTest; + } + + [Fact] + public void WeakEventManagerAction_UnassignedEvent() + { + // Arrange + var didEventFire = false; + + ActionEvent += HandleDelegateTest; + ActionEvent -= HandleDelegateTest; + void HandleDelegateTest() => didEventFire = true; + + // Act + actionEventManager.RaiseEvent(nameof(ActionEvent)); + + // Assert + Assert.False(didEventFire); + } + + [Fact] + public void WeakEventManagerAction_UnassignedEventManager() + { + // Arrange + var unassignedEventManager = new WeakEventManager(); + var didEventFire = false; + + ActionEvent += HandleDelegateTest; + void HandleDelegateTest() => didEventFire = true; + + // Act + unassignedEventManager.RaiseEvent(nameof(ActionEvent)); + + // Assert + Assert.False(didEventFire); + ActionEvent -= HandleDelegateTest; + } + + [Fact] + public void WeakEventManagerAction_HandleEvent_InvalidHandleEvent() + { + // Arrange + ActionEvent += HandleDelegateTest; + var didEventFire = false; + + void HandleDelegateTest() => didEventFire = true; + + // Act + + // Assert + Assert.Throws(() => actionEventManager.RaiseEvent(this, EventArgs.Empty, nameof(ActionEvent))); + Assert.False(didEventFire); + ActionEvent -= HandleDelegateTest; + } + + [Fact] + public void WeakEventManagerAction_AddEventHandler_NullHandler() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => actionEventManager.AddEventHandler(null)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerAction_AddEventHandler_NullEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => actionEventManager.AddEventHandler(null, null)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerAction_AddEventHandler_EmptyEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => actionEventManager.AddEventHandler(null, string.Empty)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerAction_AddEventHandler_WhitespaceEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => actionEventManager.AddEventHandler(null, " ")); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerAction_RemoveEventHandler_NullHandler() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => actionEventManager.RemoveEventHandler(null)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerAction_RemoveEventHandler_NullEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => actionEventManager.RemoveEventHandler(null, null)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerAction_RemoveEventHandler_EmptyEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => actionEventManager.RemoveEventHandler(null, string.Empty)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerAction_RemoveEventHandler_WhiteSpaceEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => actionEventManager.RemoveEventHandler(null, " ")); +#pragma warning restore CS8625 + } + } +} diff --git a/XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_Delegate_Tests.cs b/XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_Delegate_Tests.cs new file mode 100644 index 00000000..30323719 --- /dev/null +++ b/XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_Delegate_Tests.cs @@ -0,0 +1,304 @@ +using System; +using System.ComponentModel; +using Xamarin.CommunityToolkit.Exceptions; +using Xamarin.CommunityToolkit.Helpers; +using Xunit; + +namespace Xamarin.CommunityToolkit.UnitTests.Helpers.WeakEventManagerTests +{ + public class WeakEventManager_Delegate_Tests : BaseWeakEventManagerTests, INotifyPropertyChanged + { + readonly WeakEventManager propertyChangedWeakEventManager = new WeakEventManager(); + + public event PropertyChangedEventHandler PropertyChanged + { + add => propertyChangedWeakEventManager.AddEventHandler(value); + remove => propertyChangedWeakEventManager.RemoveEventHandler(value); + } + + [Fact] + public void WeakEventManagerDelegate_HandleEvent_ValidImplementation() + { + // Arrange + PropertyChanged += HandleDelegateTest; + var didEventFire = false; + + void HandleDelegateTest(object sender, PropertyChangedEventArgs e) + { + Assert.NotNull(sender); + Assert.Equal(this.GetType(), sender.GetType()); + + Assert.NotNull(e); + + didEventFire = true; + PropertyChanged -= HandleDelegateTest; + } + + // Act + propertyChangedWeakEventManager.RaiseEvent(this, new PropertyChangedEventArgs("Test"), nameof(PropertyChanged)); + + // Assert + Assert.True(didEventFire); + } + + [Fact] + public void WeakEventManagerDelegate_HandleEvent_NullSender() + { + // Arrange + PropertyChanged += HandleDelegateTest; + var didEventFire = false; + + void HandleDelegateTest(object sender, PropertyChangedEventArgs e) + { + Assert.Null(sender); + Assert.NotNull(e); + + didEventFire = true; + PropertyChanged -= HandleDelegateTest; + } + + // Act + propertyChangedWeakEventManager.RaiseEvent(null, new PropertyChangedEventArgs("Test"), nameof(PropertyChanged)); + + // Assert + Assert.True(didEventFire); + } + + [Fact] + public void WeakEventManagerDelegate_HandleEvent_InvalidEventArgs() + { + // Arrange + PropertyChanged += HandleDelegateTest; + var didEventFire = false; + + void HandleDelegateTest(object sender, PropertyChangedEventArgs e) => didEventFire = true; + + // Act + + // Assert + Assert.Throws(() => propertyChangedWeakEventManager.RaiseEvent(this, EventArgs.Empty, nameof(PropertyChanged))); + Assert.False(didEventFire); + PropertyChanged -= HandleDelegateTest; + } + + [Fact] + public void WeakEventManagerDelegate_HandleEvent_NullEventArgs() + { + // Arrange + PropertyChanged += HandleDelegateTest; + var didEventFire = false; + + void HandleDelegateTest(object sender, PropertyChangedEventArgs e) + { + Assert.NotNull(sender); + Assert.Equal(this.GetType(), sender.GetType()); + + Assert.Null(e); + + didEventFire = true; + PropertyChanged -= HandleDelegateTest; + } + + // Act +#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type + propertyChangedWeakEventManager.RaiseEvent(this, null, nameof(PropertyChanged)); +#pragma warning restore CS8625 //Cannot convert null literal to non-nullable reference type + + // Assert + Assert.True(didEventFire); + } + + [Fact] + public void WeakEventManagerDelegate_HandleEvent_InvalidHandleEventEventName() + { + // Arrange + PropertyChanged += HandleDelegateTest; + var didEventFire = false; + + void HandleDelegateTest(object sender, PropertyChangedEventArgs e) => didEventFire = true; + + // Act + propertyChangedWeakEventManager.RaiseEvent(this, new PropertyChangedEventArgs("Test"), nameof(TestStringEvent)); + + // Assert + Assert.False(didEventFire); + PropertyChanged -= HandleDelegateTest; + } + + [Fact] + public void WeakEventManagerDelegate_HandleEvent_DynamicMethod_ValidImplementation() + { + // Arrange + var dynamicMethod = new System.Reflection.Emit.DynamicMethod(string.Empty, typeof(void), new[] { typeof(object), typeof(PropertyChangedEventArgs) }); + var ilGenerator = dynamicMethod.GetILGenerator(); + ilGenerator.Emit(System.Reflection.Emit.OpCodes.Ret); + + var handler = (PropertyChangedEventHandler)dynamicMethod.CreateDelegate(typeof(PropertyChangedEventHandler)); + PropertyChanged += handler; + + // Act + + // Assert + propertyChangedWeakEventManager.RaiseEvent(this, new PropertyChangedEventArgs("Test"), nameof(PropertyChanged)); + PropertyChanged -= handler; + } + + [Fact] + public void WeakEventManagerDelegate_UnassignedEvent() + { + // Arrange + var didEventFire = false; + + PropertyChanged += HandleDelegateTest; + PropertyChanged -= HandleDelegateTest; + void HandleDelegateTest(object sender, PropertyChangedEventArgs e) => didEventFire = true; + + // Act +#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type + propertyChangedWeakEventManager.RaiseEvent(null, null, nameof(PropertyChanged)); +#pragma warning restore CS8625 //Cannot convert null literal to non-nullable reference type + + // Assert + Assert.False(didEventFire); + } + + [Fact] + public void WeakEventManagerDelegate_UnassignedEventManager() + { + // Arrange + var unassignedEventManager = new WeakEventManager(); + var didEventFire = false; + + PropertyChanged += HandleDelegateTest; + void HandleDelegateTest(object sender, PropertyChangedEventArgs e) => didEventFire = true; + + // Act + unassignedEventManager.RaiseEvent(null, null, nameof(PropertyChanged)); + + // Assert + Assert.False(didEventFire); + PropertyChanged -= HandleDelegateTest; + } + + [Fact] + public void WeakEventManagerDelegate_HandleEvent_InvalidHandleEvent() + { + // Arrange + PropertyChanged += HandleDelegateTest; + var didEventFire = false; + + void HandleDelegateTest(object sender, PropertyChangedEventArgs e) => didEventFire = true; + + // Act + + // Assert + Assert.Throws(() => propertyChangedWeakEventManager.RaiseEvent(nameof(PropertyChanged))); + Assert.False(didEventFire); + PropertyChanged -= HandleDelegateTest; + } + + [Fact] + public void WeakEventManagerDelegate_AddEventHandler_NullHandler() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => propertyChangedWeakEventManager.AddEventHandler(null)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerDelegate_AddEventHandler_NullEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => propertyChangedWeakEventManager.AddEventHandler(null, null)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerDelegate_AddEventHandler_EmptyEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => propertyChangedWeakEventManager.AddEventHandler(null, string.Empty)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerDelegate_AddEventHandler_WhitespaceEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => propertyChangedWeakEventManager.AddEventHandler(null, " ")); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerDelegate_RemoveEventHandler_NullHandler() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => propertyChangedWeakEventManager.RemoveEventHandler(null)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerDelegate_RemoveEventHandler_NullEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => propertyChangedWeakEventManager.RemoveEventHandler(null, null)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerDelegate_RemoveEventHandler_EmptyEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => propertyChangedWeakEventManager.RemoveEventHandler(null, string.Empty)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerDelegate_RemoveEventHandler_WhiteSpaceEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => propertyChangedWeakEventManager.RemoveEventHandler(null, " ")); +#pragma warning restore CS8625 + } + } +} diff --git a/XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_EventHandlerT_Tests.cs b/XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_EventHandlerT_Tests.cs new file mode 100644 index 00000000..4d1ee40a --- /dev/null +++ b/XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_EventHandlerT_Tests.cs @@ -0,0 +1,281 @@ +using System; +using Xamarin.CommunityToolkit.Exceptions; +using Xamarin.CommunityToolkit.Helpers; +using Xunit; + +namespace Xamarin.CommunityToolkit.UnitTests.Helpers.WeakEventManagerTests +{ + public class WeakEventManager_EventHandlerT_Tests : BaseWeakEventManagerTests + { + [Fact] + public void WeakEventManagerTEventArgs_HandleEvent_ValidImplementation() + { + // Arrange + TestStringEvent += HandleTestEvent; + + const string stringEventArg = "Test"; + var didEventFire = false; + + void HandleTestEvent(object sender, string? e) + { + if (sender == null || e == null) + throw new ArgumentNullException(nameof(sender)); + + Assert.NotNull(sender); + Assert.Equal(GetType(), sender.GetType()); + + Assert.NotNull(e); + Assert.Equal(stringEventArg, e); + + didEventFire = true; + TestStringEvent -= HandleTestEvent; + } + + // Act + TestStringWeakEventManager.RaiseEvent(this, stringEventArg, nameof(TestStringEvent)); + + // Assert + Assert.True(didEventFire); + } + + [Fact] + public void WeakEventManageTEventArgs_HandleEvent_NullSender() + { + // Arrange + TestStringEvent += HandleTestEvent; + + const string stringEventArg = "Test"; + + var didEventFire = false; + + void HandleTestEvent(object sender, string e) + { + Assert.Null(sender); + + Assert.NotNull(e); + Assert.Equal(stringEventArg, e); + + didEventFire = true; + TestStringEvent -= HandleTestEvent; + } + + // Act + TestStringWeakEventManager.RaiseEvent(null, stringEventArg, nameof(TestStringEvent)); + + // Assert + Assert.True(didEventFire); + } + + [Fact] + public void WeakEventManagerTEventArgs_HandleEvent_NullEventArgs() + { + // Arrange + TestStringEvent += HandleTestEvent; + var didEventFire = false; + + void HandleTestEvent(object sender, string e) + { + if (sender == null) + throw new ArgumentNullException(nameof(sender)); + + Assert.NotNull(sender); + Assert.Equal(GetType(), sender.GetType()); + + Assert.Null(e); + + didEventFire = true; + TestStringEvent -= HandleTestEvent; + } + + // Act +#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type + TestStringWeakEventManager.RaiseEvent(this, null, nameof(TestStringEvent)); +#pragma warning restore CS8625 + + // Assert + Assert.True(didEventFire); + } + + [Fact] + public void WeakEventManagerTEventArgs_HandleEvent_InvalidHandleEvent() + { + // Arrange + TestStringEvent += HandleTestEvent; + + var didEventFire = false; + + void HandleTestEvent(object sender, string e) => didEventFire = true; + + // Act + TestStringWeakEventManager.RaiseEvent(this, "Test", nameof(TestEvent)); + + // Assert + Assert.False(didEventFire); + TestStringEvent -= HandleTestEvent; + } + + [Fact] + public void WeakEventManager_NullEventManager() + { + // Arrange + WeakEventManager unassignedEventManager = null; + + // Act + + // Assert +#pragma warning disable CS8602 //Dereference of a possible null reference + Assert.Throws(() => unassignedEventManager.RaiseEvent(null, null, nameof(TestEvent))); +#pragma warning restore CS8602 + } + + [Fact] + public void WeakEventManagerTEventArgs_UnassignedEventManager() + { + // Arrange + var unassignedEventManager = new WeakEventManager(); + var didEventFire = false; + + TestStringEvent += HandleTestEvent; + void HandleTestEvent(object sender, string e) => didEventFire = true; + + // Act +#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type + unassignedEventManager.RaiseEvent(null, null, nameof(TestStringEvent)); +#pragma warning restore CS8625 + + // Assert + Assert.False(didEventFire); + TestStringEvent -= HandleTestEvent; + } + + [Fact] + public void WeakEventManagerTEventArgs_UnassignedEvent() + { + // Arrange + var didEventFire = false; + + TestStringEvent += HandleTestEvent; + TestStringEvent -= HandleTestEvent; + void HandleTestEvent(object sender, string e) => didEventFire = true; + + // Act + TestStringWeakEventManager.RaiseEvent(this, "Test", nameof(TestStringEvent)); + + // Assert + Assert.False(didEventFire); + } + + [Fact] + public void WeakEventManagerT_AddEventHandler_NullHandler() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type + Assert.Throws(() => TestStringWeakEventManager.AddEventHandler((EventHandler)null)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerT_AddEventHandler_NullEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(s => { var temp = s; }, null)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerT_AddEventHandler_EmptyEventName() + { + // Arrange + + // Act + + // Assert + Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(s => { var temp = s; }, string.Empty)); + } + + [Fact] + public void WeakEventManagerT_AddEventHandler_WhiteSpaceEventName() + { + // Arrange + + // Act + + // Assert + Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(s => { var temp = s; }, " ")); + } + + [Fact] + public void WeakEventManagerT_RemoveEventHandler_NullHandler() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => TestStringWeakEventManager.RemoveEventHandler((EventHandler)null)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManagerT_RemoveEventHandler_NullEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(s => { var temp = s; }, null)); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference + } + + [Fact] + public void WeakEventManagerT_RemoveEventHandler_EmptyEventName() + { + // Arrange + + // Act + + // Assert + Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(s => { var temp = s; }, string.Empty)); + } + + [Fact] + public void WeakEventManagerT_RemoveEventHandler_WhiteSpaceEventName() + { + // Arrange + + // Act + + // Assert + Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(s => { var temp = s; }, string.Empty)); + } + + [Fact] + public void WeakEventManagerT_HandleEvent_InvalidHandleEvent() + { + // Arrange + TestStringEvent += HandleTestStringEvent; + var didEventFire = false; + + void HandleTestStringEvent(object sender, string e) => didEventFire = true; + + // Act + + // Assert + Assert.Throws(() => TestStringWeakEventManager.RaiseEvent("", nameof(TestStringEvent))); + Assert.False(didEventFire); + TestStringEvent -= HandleTestStringEvent; + } + } +} \ No newline at end of file diff --git a/XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_EventHandler_Tests.cs b/XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_EventHandler_Tests.cs new file mode 100644 index 00000000..9f248642 --- /dev/null +++ b/XamarinCommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_EventHandler_Tests.cs @@ -0,0 +1,293 @@ +using System; +using Xamarin.CommunityToolkit.Exceptions; +using Xamarin.CommunityToolkit.Helpers; +using Xunit; + +namespace Xamarin.CommunityToolkit.UnitTests.Helpers.WeakEventManagerTests +{ + public class WeakEventManager_EventHandler_Tests : BaseWeakEventManagerTests + { + [Fact] + public void WeakEventManager_HandleEvent_ValidImplementation() + { + // Arrange + TestEvent += HandleTestEvent; + var didEventFire = false; + + void HandleTestEvent(object? sender, EventArgs e) + { + if (sender == null) + throw new ArgumentNullException(nameof(sender)); + + Assert.NotNull(sender); + Assert.Equal(GetType(), sender.GetType()); + + Assert.NotNull(e); + + didEventFire = true; + TestEvent -= HandleTestEvent; + } + + // Act + TestWeakEventManager.RaiseEvent(this, new EventArgs(), nameof(TestEvent)); + + // Assert + Assert.True(didEventFire); + } + + [Fact] + public void WeakEventManager_HandleEvent_NullSender() + { + // Arrange + TestEvent += HandleTestEvent; + var didEventFire = false; + + void HandleTestEvent(object? sender, EventArgs e) + { + Assert.Null(sender); + Assert.NotNull(e); + + didEventFire = true; + TestEvent -= HandleTestEvent; + } + + // Act + TestWeakEventManager.RaiseEvent(null, new EventArgs(), nameof(TestEvent)); + + // Assert + Assert.True(didEventFire); + } + + [Fact] + public void WeakEventManager_HandleEvent_EmptyEventArgs() + { + // Arrange + TestEvent += HandleTestEvent; + var didEventFire = false; + + void HandleTestEvent(object? sender, EventArgs e) + { + if (sender == null) + throw new ArgumentNullException(nameof(sender)); + + Assert.NotNull(sender); + Assert.Equal(GetType(), sender.GetType()); + + Assert.NotNull(e); + Assert.Equal(EventArgs.Empty, e); + + didEventFire = true; + TestEvent -= HandleTestEvent; + } + + // Act + TestWeakEventManager.RaiseEvent(this, EventArgs.Empty, nameof(TestEvent)); + + // Assert + Assert.True(didEventFire); + } + + [Fact] + public void WeakEventManager_HandleEvent_NullEventArgs() + { + // Arrange + TestEvent += HandleTestEvent; + var didEventFire = false; + + void HandleTestEvent(object? sender, EventArgs e) + { + if (sender == null) + throw new ArgumentNullException(nameof(sender)); + + Assert.NotNull(sender); + Assert.Equal(GetType(), sender.GetType()); + + Assert.Null(e); + + didEventFire = true; + TestEvent -= HandleTestEvent; + } + + // Act +#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type + TestWeakEventManager.RaiseEvent(this, null, nameof(TestEvent)); +#pragma warning restore CS8625 + + // Assert + Assert.True(didEventFire); + } + + [Fact] + public void WeakEventManager_HandleEvent_InvalidHandleEventName() + { + // Arrange + TestEvent += HandleTestEvent; + var didEventFire = false; + + void HandleTestEvent(object? sender, EventArgs e) => didEventFire = true; + + // Act + TestWeakEventManager.RaiseEvent(this, new EventArgs(), nameof(TestStringEvent)); + + // Assert + Assert.False(didEventFire); + TestEvent -= HandleTestEvent; + } + + [Fact] + public void WeakEventManager_UnassignedEvent() + { + // Arrange + var didEventFire = false; + + TestEvent += HandleTestEvent; + TestEvent -= HandleTestEvent; + void HandleTestEvent(object? sender, EventArgs e) => didEventFire = true; + + // Act + TestWeakEventManager.RaiseEvent(null, null, nameof(TestEvent)); + + // Assert + Assert.False(didEventFire); + } + + [Fact] + public void WeakEventManager_UnassignedEventManager() + { + // Arrange + var unassignedEventManager = new WeakEventManager(); + var didEventFire = false; + + TestEvent += HandleTestEvent; + void HandleTestEvent(object? sender, EventArgs e) => didEventFire = true; + + // Act + unassignedEventManager.RaiseEvent(null, null, nameof(TestEvent)); + + // Assert + Assert.False(didEventFire); + TestEvent -= HandleTestEvent; + } + + [Fact] + public void WeakEventManager_AddEventHandler_NullHandler() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => TestWeakEventManager.AddEventHandler(null)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManager_AddEventHandler_NullEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => TestWeakEventManager.AddEventHandler(null, null)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManager_AddEventHandler_EmptyEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => TestWeakEventManager.AddEventHandler(null, string.Empty)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManager_AddEventHandler_WhitespaceEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => TestWeakEventManager.AddEventHandler(null, " ")); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManager_RemoveEventHandler_NullHandler() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => TestWeakEventManager.RemoveEventHandler(null)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManager_RemoveEventHandler_NullEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => TestWeakEventManager.RemoveEventHandler(null, null)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManager_RemoveEventHandler_EmptyEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => TestWeakEventManager.RemoveEventHandler(null, string.Empty)); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManager_RemoveEventHandler_WhiteSpaceEventName() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference + Assert.Throws(() => TestWeakEventManager.RemoveEventHandler(null, " ")); +#pragma warning restore CS8625 + } + + [Fact] + public void WeakEventManager_HandleEvent_InvalidHandleEvent() + { + // Arrange + TestEvent += HandleTestEvent; + var didEventFire = false; + + void HandleTestEvent(object? sender, EventArgs e) => didEventFire = true; + + // Act + + // Assert + Assert.Throws(() => TestWeakEventManager.RaiseEvent(nameof(TestEvent))); + Assert.False(didEventFire); + TestEvent -= HandleTestEvent; + } + } +} diff --git a/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/AsyncCommand_Tests.cs b/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/AsyncCommand_Tests.cs new file mode 100644 index 00000000..9d57eac8 --- /dev/null +++ b/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/AsyncCommand_Tests.cs @@ -0,0 +1,189 @@ +using System; +using System.Threading.Tasks; +using Xamarin.CommunityToolkit.ObjectModel; +using Xunit; + +namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests.AsyncCommandTests +{ + public class AsyncCommandTests : BaseAsyncCommandTests + { + [Fact] + public void AsyncCommand_NullExecuteParameter() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type + Assert.Throws(() => new AsyncCommand(null)); + Assert.Throws(() => new AsyncCommand(null)); + Assert.Throws(() => new AsyncCommand(null)); +#pragma warning restore CS8625 + } + + [Theory] + [InlineData(500)] + [InlineData(0)] + public async Task AsyncCommand_ExecuteAsync_IntParameter_Test(int parameter) + { + // Arrange + var command = new AsyncCommand(IntParameterTask); + + // Act + await command.ExecuteAsync(parameter); + + // Assert + } + + [Theory] + [InlineData("Hello")] + [InlineData(default)] + public async Task AsyncCommand_ExecuteAsync_StringParameter_Test(string parameter) + { + // Arrange + var command = new AsyncCommand(StringParameterTask); + + // Act + await command.ExecuteAsync(parameter); + + // Assert + } + + [Fact] + public void AsyncCommand_Parameter_CanExecuteTrue_Test() + { + // Arrange + var command = new AsyncCommand(IntParameterTask, CanExecuteTrue); + + // Act + + // Assert + + Assert.True(command.CanExecute(null)); + } + + [Fact] + public void AsyncCommand_Parameter_CanExecuteFalse_Test() + { + // Arrange + var command = new AsyncCommand(IntParameterTask, CanExecuteFalse); + + // Act + + // Assert + Assert.False(command.CanExecute(null)); + } + + [Fact] + public void AsyncCommand_NoParameter_CanExecuteTrue_Test() + { + // Arrange + var command = new AsyncCommand(NoParameterTask, CanExecuteTrue); + + // Act + + // Assert + Assert.True(command.CanExecute(null)); + } + + [Fact] + public void AsyncCommand_NoParameter_CanExecuteFalse_Test() + { + // Arrange + var command = new AsyncCommand(NoParameterTask, CanExecuteFalse); + + // Act + + // Assert + Assert.False(command.CanExecute(null)); + } + + [Fact] + public void AsyncCommand_CanExecuteChanged_Test() + { + // Arrange + var canCommandExecute = false; + var didCanExecuteChangeFire = false; + + var command = new AsyncCommand(NoParameterTask, commandCanExecute); + command.CanExecuteChanged += handleCanExecuteChanged; + + bool commandCanExecute(object parameter) => canCommandExecute; + + Assert.False(command.CanExecute(null)); + + // Act + canCommandExecute = true; + + // Assert + Assert.True(command.CanExecute(null)); + Assert.False(didCanExecuteChangeFire); + + // Act + command.RaiseCanExecuteChanged(); + + // Assert + Assert.True(didCanExecuteChangeFire); + Assert.True(command.CanExecute(null)); + + void handleCanExecuteChanged(object sender, EventArgs e) => didCanExecuteChangeFire = true; + } + + [Fact] + public async Task AsyncCommand_CanExecuteChanged_AllowsMultipleExecutions_Test() + { + // Arrange + var canExecuteChangedCount = 0; + + var command = new AsyncCommand(IntParameterTask); + command.CanExecuteChanged += handleCanExecuteChanged; + + Assert.True(command.AllowsMultipleExecutions); + + // Act + var asyncCommandTask = command.ExecuteAsync(Delay); + + // Assert + Assert.True(command.IsExecuting); + Assert.True(command.CanExecute(null)); + + // Act + await asyncCommandTask; + + // Assert + Assert.True(command.CanExecute(null)); + Assert.Equal(0, canExecuteChangedCount); + + void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + } + + [Fact] + public async Task AsyncCommand_CanExecuteChanged_DoesNotAllowMultipleExecutions_Test() + { + // Arrange + var canExecuteChangedCount = 0; + + var command = new AsyncCommand(IntParameterTask, allowsMultipleExecutions: false); + command.CanExecuteChanged += handleCanExecuteChanged; + + Assert.False(command.AllowsMultipleExecutions); + + // Act + var asyncCommandTask = command.ExecuteAsync(Delay); + + // Assert + Assert.True(command.IsExecuting); + Assert.False(command.CanExecute(null)); + + // Act + await asyncCommandTask; + + // Assert + Assert.True(command.CanExecute(null)); + Assert.Equal(2, canExecuteChangedCount); + + void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + } + } +} diff --git a/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/BaseAsyncCommandTests.cs b/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/BaseAsyncCommandTests.cs new file mode 100644 index 00000000..f42630f9 --- /dev/null +++ b/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/BaseAsyncCommandTests.cs @@ -0,0 +1,6 @@ +namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests.AsyncCommandTests +{ + public abstract class BaseAsyncCommandTests : BaseCommandTests + { + } +} diff --git a/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/IAsyncCommand_Tests.cs b/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/IAsyncCommand_Tests.cs new file mode 100644 index 00000000..8bbcad5f --- /dev/null +++ b/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/IAsyncCommand_Tests.cs @@ -0,0 +1,183 @@ +using System; +using System.Threading.Tasks; +using Xamarin.CommunityToolkit.Exceptions; +using Xamarin.CommunityToolkit.ObjectModel; +using Xunit; + +namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests.AsyncCommandTests +{ + public class IAsyncCommandTests : BaseAsyncCommandTests + { + [Fact] + public void IAsyncCommand_CanExecute_InvalidReferenceParameter() + { + // Arrange + IAsyncCommand command = new AsyncCommand(IntParameterTask, CanExecuteTrue); + + // Act + + // Assert + Assert.Throws(() => command.CanExecute("Hello World")); + } + + [Fact] + public void IAsyncCommand_Execute_InvalidValueTypeParameter() + { + // Arrange + IAsyncCommand command = new AsyncCommand(StringParameterTask, CanExecuteTrue); + + // Act + + // Assert + Assert.Throws(() => command.Execute(true)); + } + + [Fact] + public void IAsyncCommand_Execute_InvalidReferenceParameter() + { + // Arrange + IAsyncCommand command = new AsyncCommand(IntParameterTask, CanExecuteTrue); + + // Act + + // Assert + Assert.Throws(() => command.Execute("Hello World")); + } + + [Fact] + public void IAsyncCommand_CanExecute_InvalidValueTypeParameter() + { + // Arrange + IAsyncCommand command = new AsyncCommand(IntParameterTask, CanExecuteTrue); + + // Act + + // Assert + Assert.Throws(() => command.CanExecute(true)); + } + + [Theory] + [InlineData("Hello")] + [InlineData(default)] + public async Task AsyncCommand_ExecuteAsync_StringParameter_Test(string parameter) + { + // Arrange + IAsyncCommand command = new AsyncCommand(StringParameterTask); + IAsyncCommand command2 = new AsyncCommand(StringParameterTask); + + // Act + await command.ExecuteAsync(parameter); + await command2.ExecuteAsync(parameter); + + // Assert + } + + [Fact] + public void IAsyncCommand_Parameter_CanExecuteTrue_Test() + { + // Arrange + IAsyncCommand command = new AsyncCommand(IntParameterTask, CanExecuteTrue); + IAsyncCommand command2 = new AsyncCommand(IntParameterTask, CanExecuteTrue); + + // Act + + // Assert + Assert.True(command.CanExecute(null)); + Assert.True(command2.CanExecute(true)); + } + + [Fact] + public void IAsyncCommand_Parameter_CanExecuteFalse_Test() + { + // Arrange + IAsyncCommand command = new AsyncCommand(IntParameterTask, CanExecuteFalse); + IAsyncCommand command2 = new AsyncCommand(IntParameterTask, CanExecuteFalse); + + // Act + + // Assert + Assert.False(command.CanExecute(null)); + Assert.False(command2.CanExecute("Hello World")); + } + + [Fact] + public void IAsyncCommand_NoParameter_CanExecuteTrue_Test() + { + // Arrange + IAsyncCommand command = new AsyncCommand(NoParameterTask, CanExecuteTrue); + + // Act + + // Assert + Assert.True(command.CanExecute(null)); + } + + [Fact] + public void IAsyncCommand_NoParameter_CanExecuteFalse_Test() + { + // Arrange + IAsyncCommand command = new AsyncCommand(NoParameterTask, CanExecuteFalse); + + // Act + + // Assert + Assert.False(command.CanExecute(null)); + } + + [Fact] + public async Task IAsyncCommand_CanExecuteChanged_AllowsMultipleExecutions_Test() + { + // Arrange + var canExecuteChangedCount = 0; + + IAsyncCommand command = new AsyncCommand(IntParameterTask); + command.CanExecuteChanged += handleCanExecuteChanged; + + Assert.True(command.AllowsMultipleExecutions); + + // Act + var asyncCommandTask = command.ExecuteAsync(Delay); + + // Assert + Assert.True(command.IsExecuting); + Assert.True(command.CanExecute(null)); + + // Act + await asyncCommandTask; + + // Assert + Assert.True(command.CanExecute(null)); + Assert.Equal(0, canExecuteChangedCount); + + void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + } + + [Fact] + public async Task IAsyncCommand_CanExecuteChanged_DoesNotAllowMultipleExecutions_Test() + { + // Arrange + var canExecuteChangedCount = 0; + + IAsyncCommand command = new AsyncCommand(IntParameterTask, allowsMultipleExecutions: false); + command.CanExecuteChanged += handleCanExecuteChanged; + + Assert.False(command.AllowsMultipleExecutions); + + // Act + var asyncCommandTask = command.ExecuteAsync(Delay); + + // Assert + Assert.True(command.IsExecuting); + Assert.False(command.CanExecute(null)); + + // Act + await asyncCommandTask; + + // Assert + Assert.True(command.CanExecute(null)); + Assert.Equal(2, canExecuteChangedCount); + + void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + } + } +} diff --git a/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/ICommand_AsyncCommand_Tests.cs b/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/ICommand_AsyncCommand_Tests.cs new file mode 100644 index 00000000..bbb82d42 --- /dev/null +++ b/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/ICommand_AsyncCommand_Tests.cs @@ -0,0 +1,261 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Input; +using Xamarin.CommunityToolkit.Exceptions; +using Xamarin.CommunityToolkit.ObjectModel; +using Xunit; + +namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests.AsyncCommandTests +{ + public class ICommand_AsyncCommandTests : BaseAsyncCommandTests + { + [Theory] + [InlineData(500)] + [InlineData(0)] + public async Task ICommand_Execute_IntParameter_Test(int parameter) + { + // Arrange + ICommand command = new AsyncCommand(IntParameterTask); + + // Act + command.Execute(parameter); + await NoParameterTask(); + + // Assert + } + + [Theory] + [InlineData("Hello")] + [InlineData(default)] + public async Task ICommand_Execute_StringParameter_Test(string parameter) + { + // Arrange + ICommand command = new AsyncCommand(StringParameterTask); + + // Act + command.Execute(parameter); + await NoParameterTask(); + + // Assert + } + + [Fact] + public void ICommand_ExecuteAsync_InvalidValueTypeParameter_Test() + { + // Arrange + InvalidCommandParameterException actualInvalidCommandParameterException = null; + var expectedInvalidCommandParameterException = new InvalidCommandParameterException(typeof(string), typeof(int)); + + ICommand command = new AsyncCommand(StringParameterTask); + + // Act + + actualInvalidCommandParameterException = Assert.Throws(() => command.Execute(Delay)); + + // Assert + Assert.NotNull(actualInvalidCommandParameterException); + Assert.Equal(expectedInvalidCommandParameterException.Message, actualInvalidCommandParameterException?.Message); + } + + [Fact] + public void ICommand_ExecuteAsync_InvalidReferenceTypeParameter_Test() + { + // Arrange + InvalidCommandParameterException actualInvalidCommandParameterException = null; + var expectedInvalidCommandParameterException = new InvalidCommandParameterException(typeof(int), typeof(string)); + + ICommand command = new AsyncCommand(IntParameterTask); + + // Act + actualInvalidCommandParameterException = Assert.Throws(() => command.Execute("Hello World")); + + // Assert + Assert.NotNull(actualInvalidCommandParameterException); + Assert.Equal(expectedInvalidCommandParameterException.Message, actualInvalidCommandParameterException?.Message); + } + + [Fact] + public void ICommand_ExecuteAsync_ValueTypeParameter_Test() + { + // Arrange + InvalidCommandParameterException actualInvalidCommandParameterException = null; + var expectedInvalidCommandParameterException = new InvalidCommandParameterException(typeof(int)); + + ICommand command = new AsyncCommand(IntParameterTask); + + // Act + actualInvalidCommandParameterException = Assert.Throws(() => command.Execute(null)); + + // Assert + Assert.NotNull(actualInvalidCommandParameterException); + Assert.Equal(expectedInvalidCommandParameterException.Message, actualInvalidCommandParameterException?.Message); + } + + [Fact] + public void ICommand_Parameter_CanExecuteTrue_Test() + { + // Arrange + ICommand command = new AsyncCommand(IntParameterTask, CanExecuteTrue); + + // Act + + // Assert + Assert.True(command.CanExecute(null)); + } + + [Fact] + public void ICommand_Parameter_CanExecuteFalse_Test() + { + // Arrange + ICommand command = new AsyncCommand(IntParameterTask, CanExecuteFalse); + + // Act + + // Assert + Assert.False(command.CanExecute(null)); + } + + [Fact] + public void ICommand_NoParameter_CanExecuteFalse_Test() + { + // Arrange + ICommand command = new AsyncCommand(NoParameterTask, CanExecuteFalse); + + // Act + + // Assert + Assert.False(command.CanExecute(null)); + } + + [Fact] + public void ICommand_Parameter_CanExecuteDynamic_Test() + { + // Arrange + ICommand command = new AsyncCommand(IntParameterTask, CanExecuteDynamic); + + // Act + + // Assert + Assert.True(command.CanExecute(true)); + Assert.False(command.CanExecute(false)); + } + + [Fact] + 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)); + } + + [Fact] + public async Task ICommand_Parameter_CanExecuteChanged_AllowsMultipleExecutions_Test() + { + // Arrange + var canExecuteChangedCount = 0; + + ICommand command = new AsyncCommand(IntParameterTask); + + void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + + // Act + command.Execute(Delay); + + // Assert + Assert.True(command.CanExecute(null)); + + // Act + await IntParameterTask(Delay); + await IntParameterTask(Delay); + + // Assert + Assert.True(command.CanExecute(null)); + Assert.Equal(0, canExecuteChangedCount); + + command.CanExecuteChanged += handleCanExecuteChanged; + } + + [Fact] + public async Task ICommand_Parameter_CanExecuteChanged_DoesNotAllowMultipleExecutions_Test() + { + // Arrange + var canExecuteChangedCount = 0; + + ICommand command = new AsyncCommand(IntParameterTask, allowsMultipleExecutions: false); + command.CanExecuteChanged += handleCanExecuteChanged; + + // Act + command.Execute(Delay); + + // Assert + Assert.False(command.CanExecute(null)); + + // Act + await IntParameterTask(Delay); + await IntParameterTask(Delay); + + // Assert + Assert.True(command.CanExecute(null)); + Assert.Equal(2, canExecuteChangedCount); + + void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + } + + [Fact] + public async Task ICommand_NoParameter_CanExecuteChanged_AllowsMultipleExecutions_Test() + { + // Arrange + var canExecuteChangedCount = 0; + + ICommand command = new AsyncCommand(() => IntParameterTask(Delay)); + command.CanExecuteChanged += handleCanExecuteChanged; + + void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + + // Act + command.Execute(null); + + // Assert + Assert.True(command.CanExecute(null)); + + // Act + await IntParameterTask(Delay); + await IntParameterTask(Delay); + + // Assert + Assert.True(command.CanExecute(null)); + Assert.Equal(0, canExecuteChangedCount); + } + + [Fact] + public async Task ICommand_NoParameter_CanExecuteChanged_DoesNotAllowMultipleExecutions_Test() + { + // Arrange + var canExecuteChangedCount = 0; + + ICommand command = new AsyncCommand(() => IntParameterTask(Delay), allowsMultipleExecutions: false); + command.CanExecuteChanged += handleCanExecuteChanged; + + void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + + // Act + command.Execute(null); + + // Assert + Assert.False(command.CanExecute(null)); + + // Act + await IntParameterTask(Delay); + await IntParameterTask(Delay); + + // Assert + Assert.True(command.CanExecute(null)); + Assert.Equal(2, canExecuteChangedCount); + } + } +} diff --git a/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/AsyncValueCommand_Tests.cs b/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/AsyncValueCommand_Tests.cs new file mode 100644 index 00000000..2b8d35aa --- /dev/null +++ b/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/AsyncValueCommand_Tests.cs @@ -0,0 +1,264 @@ +using System; +using System.Threading.Tasks; +using Xamarin.CommunityToolkit.ObjectModel; +using Xunit; + +namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests.AsyncValueCommandTests +{ + public class AsyncValueCommandTests : BaseAsyncValueCommandTests + { + [Fact] + public void AsyncValueCommandNullExecuteParameter() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type + Assert.Throws(() => new AsyncValueCommand(null)); +#pragma warning restore CS8625 + } + + [Fact] + public void AsyncValueCommandT_NullExecuteParameter() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type + Assert.Throws(() => new AsyncValueCommand(null)); +#pragma warning restore CS8625 + } + + [Theory] + [InlineData(500)] + [InlineData(0)] + public async Task AsyncValueCommandExecuteAsync_IntParameter_Test(int parameter) + { + // Arrange + var command = new AsyncValueCommand(IntParameterTask); + var command2 = new AsyncValueCommand(IntParameterTask, CanExecuteTrue); + + // Act + await command.ExecuteAsync(parameter); + await command2.ExecuteAsync(parameter); + + // Assert + } + + [Theory] + [InlineData("Hello")] + [InlineData(default)] + public async Task AsyncValueCommandExecuteAsync_StringParameter_Test(string parameter) + { + // Arrange + var command = new AsyncValueCommand(StringParameterTask); + var command2 = new AsyncValueCommand(StringParameterTask, CanExecuteTrue); + + // Act + await command.ExecuteAsync(parameter); + await command2.ExecuteAsync(parameter); + + // Assert + } + + [Fact] + public void AsyncValueCommandParameter_CanExecuteTrue_Test() + { + // Arrange + var command = new AsyncValueCommand(IntParameterTask, CanExecuteTrue); + var command2 = new AsyncValueCommand(IntParameterTask, CanExecuteTrue); + + // Act + + // Assert + + Assert.True(command.CanExecute(null)); + Assert.True(command2.CanExecute(true)); + } + + [Fact] + public void AsyncValueCommandParameter_CanExecuteFalse_Test() + { + // Arrange + var command = new AsyncValueCommand(IntParameterTask, CanExecuteFalse); + var command2 = new AsyncValueCommand(IntParameterTask, CanExecuteFalse); + + // Act + + // Assert + Assert.False(command.CanExecute(null)); + Assert.False(command2.CanExecute("Hello World")); + } + + [Fact] + public void AsyncValueCommandNoParameter_CanExecuteTrue_Test() + { + // Arrange + var command = new AsyncValueCommand(NoParameterTask, CanExecuteTrue); + + // Act + + // Assert + Assert.True(command.CanExecute(null)); + } + + [Fact] + public void AsyncValueCommandNoParameter_CanExecuteFalse_Test() + { + // Arrange + var command = new AsyncValueCommand(NoParameterTask, CanExecuteFalse); + + // Act + + // Assert + Assert.False(command.CanExecute(null)); + } + + [Fact] + public void AsyncValueCommandCanExecuteChanged_Test() + { + // Arrange + var canCommandExecute = false; + var didCanExecuteChangeFire = false; + + var command = new AsyncValueCommand(NoParameterTask, commandCanExecute); + command.CanExecuteChanged += handleCanExecuteChanged; + + bool commandCanExecute(object parameter) => canCommandExecute; + + Assert.False(command.CanExecute(null)); + + // Act + canCommandExecute = true; + + // Assert + Assert.True(command.CanExecute(null)); + Assert.False(didCanExecuteChangeFire); + + // Act + command.RaiseCanExecuteChanged(); + + // Assert + Assert.True(didCanExecuteChangeFire); + Assert.True(command.CanExecute(null)); + + void handleCanExecuteChanged(object sender, EventArgs e) => didCanExecuteChangeFire = true; + } + + [Fact] + public async Task AsyncValueCommand_Parameter_CanExecuteChanged_AllowsMultipleExecutions_Test() + { + // Arrange + var canExecuteChangedCount = 0; + + var command = new AsyncValueCommand(IntParameterTask); + command.CanExecuteChanged += handleCanExecuteChanged; + + void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + + Assert.True(command.AllowsMultipleExecutions); + + // Act + var asyncCommandTask = command.ExecuteAsync(Delay); + + // Assert + Assert.True(command.IsExecuting); + Assert.True(command.CanExecute(null)); + + // Act + await asyncCommandTask; + + // Assert + Assert.True(command.CanExecute(null)); + Assert.Equal(0, canExecuteChangedCount); + } + + [Fact] + public async Task AsyncValueCommand_Parameter_CanExecuteChanged_DoesNotAllowMultipleExecutions_Test() + { + // Arrange + var canExecuteChangedCount = 0; + + var command = new AsyncValueCommand(IntParameterTask, allowsMultipleExecutions: false); + command.CanExecuteChanged += handleCanExecuteChanged; + + void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + + Assert.False(command.AllowsMultipleExecutions); + + // Act + var asyncCommandTask = command.ExecuteAsync(Delay); + + // Assert + Assert.True(command.IsExecuting); + Assert.False(command.CanExecute(null)); + + // Act + await asyncCommandTask; + + // Assert + Assert.True(command.CanExecute(null)); + Assert.Equal(2, canExecuteChangedCount); + } + + [Fact] + public async Task AsyncValueCommand_NoParameter_CanExecuteChanged_AllowsMultipleExecutions_Test() + { + // Arrange + var canExecuteChangedCount = 0; + + var command = new AsyncValueCommand(() => IntParameterTask(Delay)); + command.CanExecuteChanged += handleCanExecuteChanged; + + void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + + Assert.True(command.AllowsMultipleExecutions); + + // Act + var asyncCommandTask = command.ExecuteAsync(); + + // Assert + Assert.True(command.IsExecuting); + Assert.True(command.CanExecute(null)); + + // Act + await asyncCommandTask; + + // Assert + Assert.True(command.CanExecute(null)); + Assert.Equal(0, canExecuteChangedCount); + } + + [Fact] + public async Task AsyncValueCommand_NoParameter_CanExecuteChanged_DoesNotAllowMultipleExecutions_Test() + { + // Arrange + var canExecuteChangedCount = 0; + + var command = new AsyncValueCommand(() => IntParameterTask(Delay), allowsMultipleExecutions: false); + command.CanExecuteChanged += handleCanExecuteChanged; + + void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + + Assert.False(command.AllowsMultipleExecutions); + + // Act + var asyncCommandTask = command.ExecuteAsync(); + + // Assert + Assert.True(command.IsExecuting); + Assert.False(command.CanExecute(null)); + + // Act + await asyncCommandTask; + + // Assert + Assert.True(command.CanExecute(null)); + Assert.Equal(2, canExecuteChangedCount); + } + } +} diff --git a/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/BaseAsyncValueCommandTests.cs b/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/BaseAsyncValueCommandTests.cs new file mode 100644 index 00000000..235676a2 --- /dev/null +++ b/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/BaseAsyncValueCommandTests.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading.Tasks; + +namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests.AsyncValueCommandTests +{ + public abstract class BaseAsyncValueCommandTests : BaseCommandTests + { + protected new ValueTask NoParameterTask() => ValueTaskDelay(Delay); + + protected new ValueTask IntParameterTask(int delay) => ValueTaskDelay(delay); + + protected new ValueTask StringParameterTask(string text) => ValueTaskDelay(Delay); + + protected new ValueTask NoParameterImmediateNullReferenceExceptionTask() => throw new NullReferenceException(); + + protected new ValueTask ParameterImmediateNullReferenceExceptionTask(int delay) => throw new NullReferenceException(); + + protected async ValueTask ValueTaskDelay(int delay) => await Task.Delay(delay); + + protected new async ValueTask NoParameterDelayedNullReferenceExceptionTask() + { + await Task.Delay(Delay); + throw new NullReferenceException(); + } + + protected new async ValueTask IntParameterDelayedNullReferenceExceptionTask(int delay) + { + await Task.Delay(delay); + throw new NullReferenceException(); + } + } +} diff --git a/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/IAsyncValueCommand_Tests.cs b/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/IAsyncValueCommand_Tests.cs new file mode 100644 index 00000000..89eea33e --- /dev/null +++ b/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/IAsyncValueCommand_Tests.cs @@ -0,0 +1,142 @@ +using System.Threading.Tasks; +using Xamarin.CommunityToolkit.Exceptions; +using Xamarin.CommunityToolkit.ObjectModel; +using Xunit; + +namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests.AsyncValueCommandTests +{ + public class IAsyncValueCommandTests : BaseAsyncValueCommandTests + { + [Fact] + public void IAsyncCommand_CanExecute_InvalidReferenceParameter() + { + // Arrange + IAsyncValueCommand command = new AsyncValueCommand(IntParameterTask, CanExecuteTrue); + + // Act + + // Assert + Assert.Throws(() => command.CanExecute("Hello World")); + } + + [Fact] + public void IAsyncCommand_Execute_InvalidValueTypeParameter() + { + // Arrange + IAsyncValueCommand command = new AsyncValueCommand(StringParameterTask, CanExecuteTrue); + + // Act + + // Assert + Assert.Throws(() => command.Execute(true)); + } + + [Fact] + public void IAsyncCommand_Execute_InvalidReferenceParameter() + { + // Arrange + IAsyncValueCommand command = new AsyncValueCommand(IntParameterTask, CanExecuteTrue); + + // Act + + // Assert + Assert.Throws(() => command.Execute("Hello World")); + } + + [Fact] + public void IAsyncCommand_CanExecute_InvalidValueTypeParameter() + { + // Arrange + IAsyncValueCommand command = new AsyncValueCommand(IntParameterTask, CanExecuteTrue); + + // Act + + // Assert + Assert.Throws(() => command.CanExecute(true)); + } + + [Theory] + [InlineData(500)] + [InlineData(0)] + public async Task AsyncValueCommand_ExecuteAsync_IntParameter_Test(int parameter) + { + // Arrange + IAsyncValueCommand command = new AsyncValueCommand(IntParameterTask); + IAsyncValueCommand command2 = new AsyncValueCommand(IntParameterTask); + + // Act + await command.ExecuteAsync(parameter); + await command2.ExecuteAsync(parameter); + + // Assert + } + + [Theory] + [InlineData("Hello")] + [InlineData(default)] + public async Task AsyncValueCommand_ExecuteAsync_StringParameter_Test(string parameter) + { + // Arrange + IAsyncValueCommand command = new AsyncValueCommand(StringParameterTask); + IAsyncValueCommand command2 = new AsyncValueCommand(StringParameterTask); + + // Act + await command.ExecuteAsync(parameter); + await command2.ExecuteAsync(parameter); + + // Assert + } + + [Fact] + public void IAsyncValueCommand_Parameter_CanExecuteTrue_Test() + { + // Arrange + IAsyncValueCommand command = new AsyncValueCommand(IntParameterTask, CanExecuteTrue); + IAsyncValueCommand command2 = new AsyncValueCommand(IntParameterTask, CanExecuteTrue); + + // Act + + // Assert + Assert.True(command.CanExecute(null)); + Assert.True(command.CanExecute("Hello World")); + } + + [Fact] + public void IAsyncValueCommand_Parameter_CanExecuteFalse_Test() + { + // Arrange + IAsyncValueCommand command = new AsyncValueCommand(IntParameterTask, CanExecuteFalse); + IAsyncValueCommand command2 = new AsyncValueCommand(IntParameterTask, CanExecuteFalse); + + // Act + + // Assert + Assert.False(command.CanExecute(null)); + Assert.False(command2.CanExecute(true)); + } + + [Fact] + public void IAsyncValueCommand_NoParameter_CanExecuteTrue_Test() + { + // Arrange + IAsyncValueCommand command = new AsyncValueCommand(NoParameterTask, CanExecuteTrue); + + // Act + + // Assert + Assert.True(command.CanExecute(null)); + } + + [Fact] + public void IAsyncValueCommand_NoParameter_CanExecuteFalse_Test() + { + // Arrange + IAsyncValueCommand command = new AsyncValueCommand(NoParameterTask, CanExecuteFalse); + + // Act + + // Assert + Assert.False(command.CanExecute(null)); + } + } +} diff --git a/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/ICommand_AsyncValueCommand_Tests.cs b/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/ICommand_AsyncValueCommand_Tests.cs new file mode 100644 index 00000000..b85e6a6e --- /dev/null +++ b/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/ICommand_AsyncValueCommand_Tests.cs @@ -0,0 +1,285 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Input; +using Xamarin.CommunityToolkit.Exceptions; +using Xamarin.CommunityToolkit.ObjectModel; +using Xunit; + +namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests.AsyncValueCommandTests +{ + public class ICommand_AsyncValueCommandTests : BaseAsyncValueCommandTests + { + [Theory] + [InlineData(500)] + [InlineData(0)] + public async Task ICommand_Execute_IntParameter_Test(int parameter) + { + // Arrange + ICommand command = new AsyncValueCommand(IntParameterTask); + + // Act + command.Execute(parameter); + await NoParameterTask(); + + // Assert + } + + [Theory] + [InlineData("Hello")] + [InlineData(default)] + public async Task ICommand_Execute_StringParameter_Test(string parameter) + { + // Arrange + ICommand command = new AsyncValueCommand(StringParameterTask); + + // Act + command.Execute(parameter); + await NoParameterTask(); + + // Assert + } + + [Fact] + public async Task ICommand_Execute_InvalidValueTypeParameter_Test() + { + // Arrange + InvalidCommandParameterException actualInvalidCommandParameterException = null; + var expectedInvalidCommandParameterException = new InvalidCommandParameterException(typeof(string), typeof(int)); + + ICommand command = new AsyncValueCommand(StringParameterTask); + + // Act + try + { + command.Execute(Delay); + await NoParameterTask(); + await NoParameterTask(); + } + catch (InvalidCommandParameterException e) + { + actualInvalidCommandParameterException = e; + } + + // Assert + Assert.NotNull(actualInvalidCommandParameterException); + Assert.Equal(expectedInvalidCommandParameterException.Message, actualInvalidCommandParameterException?.Message); + } + + [Fact] + public async Task ICommand_Execute_InvalidReferenceTypeParameter_Test() + { + // Arrange + InvalidCommandParameterException actualInvalidCommandParameterException = null; + var expectedInvalidCommandParameterException = new InvalidCommandParameterException(typeof(int), typeof(string)); + + ICommand command = new AsyncValueCommand(IntParameterTask); + + // Act + try + { + command.Execute("Hello World"); + await NoParameterTask(); + await NoParameterTask(); + } + catch (InvalidCommandParameterException e) + { + actualInvalidCommandParameterException = e; + } + + // Assert + Assert.NotNull(actualInvalidCommandParameterException); + Assert.Equal(expectedInvalidCommandParameterException.Message, actualInvalidCommandParameterException?.Message); + } + + [Fact] + public async Task ICommand_Execute_ValueTypeParameter_Test() + { + // Arrange + InvalidCommandParameterException actualInvalidCommandParameterException = null; + var expectedInvalidCommandParameterException = new InvalidCommandParameterException(typeof(int)); + + ICommand command = new AsyncValueCommand(IntParameterTask); + + // Act + try + { + command.Execute(null); + await NoParameterTask(); + await NoParameterTask(); + } + catch (InvalidCommandParameterException e) + { + actualInvalidCommandParameterException = e; + } + + // Assert + Assert.NotNull(actualInvalidCommandParameterException); + Assert.Equal(expectedInvalidCommandParameterException.Message, actualInvalidCommandParameterException?.Message); + } + + [Fact] + public void ICommand_Parameter_CanExecuteTrue_Test() + { + // Arrange + ICommand command = new AsyncValueCommand(IntParameterTask, CanExecuteTrue); + + // Act + + // Assert + Assert.True(command.CanExecute(null)); + } + + [Fact] + public void ICommand_Parameter_CanExecuteFalse_Test() + { + // Arrange + ICommand command = new AsyncValueCommand(IntParameterTask, CanExecuteFalse); + + // Act + + // Assert + Assert.False(command.CanExecute(null)); + } + + [Fact] + public void ICommand_NoParameter_CanExecuteFalse_Test() + { + // Arrange + ICommand command = new AsyncValueCommand(NoParameterTask, CanExecuteFalse); + + // Act + + // Assert + Assert.False(command.CanExecute(null)); + } + + [Fact] + public void ICommand_Parameter_CanExecuteDynamic_Test() + { + // Arrange + ICommand command = new AsyncValueCommand(IntParameterTask, CanExecuteDynamic); + + // Act + + // Assert + Assert.True(command.CanExecute(true)); + Assert.False(command.CanExecute(false)); + } + + [Fact] + public void ICommand_Parameter_CanExecuteChanged_Test() + { + // Arrange + ICommand command = new AsyncValueCommand(IntParameterTask, CanExecuteDynamic); + + // Act + + // Assert + Assert.True(command.CanExecute(true)); + Assert.False(command.CanExecute(false)); + } + + [Fact] + public async Task ICommand_Parameter_CanExecuteChanged_AllowsMultipleExecutions_Test() + { + // Arrange + var canExecuteChangedCount = 0; + + ICommand command = new AsyncValueCommand(IntParameterTask); + command.CanExecuteChanged += handleCanExecuteChanged; + + void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + + // Act + command.Execute(Delay); + + // Assert + Assert.True(command.CanExecute(null)); + + // Act + await IntParameterTask(Delay); + + // Assert + Assert.True(command.CanExecute(null)); + Assert.Equal(0, canExecuteChangedCount); + } + + [Fact] + public async Task ICommand_Parameter_CanExecuteChanged_DoesNotAllowMultipleExecutions_Test() + { + // Arrange + var canExecuteChangedCount = 0; + + ICommand command = new AsyncValueCommand(IntParameterTask, allowsMultipleExecutions: false); + command.CanExecuteChanged += handleCanExecuteChanged; + + void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + + // Act + command.Execute(Delay); + + // Assert + Assert.False(command.CanExecute(null)); + + // Act + await IntParameterTask(Delay); + await IntParameterTask(Delay); + + // Assert + Assert.True(command.CanExecute(null)); + Assert.Equal(2, canExecuteChangedCount); + } + + [Fact] + public async Task ICommand_NoParameter_CanExecuteChanged_AllowsMultipleExecutions_Test() + { + // Arrange + var canExecuteChangedCount = 0; + + ICommand command = new AsyncValueCommand(() => IntParameterTask(Delay)); + command.CanExecuteChanged += handleCanExecuteChanged; + + void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + + // Act + command.Execute(null); + + // Assert + Assert.True(command.CanExecute(null)); + + // Act + await IntParameterTask(Delay); + await IntParameterTask(Delay); + + // Assert + Assert.True(command.CanExecute(null)); + Assert.Equal(0, canExecuteChangedCount); + } + + [Fact] + public async Task ICommand_NoParameter_CanExecuteChanged_DoesNotAllowMultipleExecutions_Test() + { + // Arrange + var canExecuteChangedCount = 0; + + ICommand command = new AsyncValueCommand(() => IntParameterTask(Delay), allowsMultipleExecutions: false); + command.CanExecuteChanged += handleCanExecuteChanged; + + void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + + // Act + command.Execute(null); + + // Assert + Assert.False(command.CanExecute(null)); + + // Act + await IntParameterTask(Delay); + await IntParameterTask(Delay); + + // Assert + Assert.True(command.CanExecute(null)); + Assert.Equal(2, canExecuteChangedCount); + } + } +} diff --git a/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/BaseCommandTests.cs b/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/BaseCommandTests.cs new file mode 100644 index 00000000..f4418a28 --- /dev/null +++ b/XamarinCommunityToolkit.UnitTests/ObjectModel/ICommandTests/BaseCommandTests.cs @@ -0,0 +1,56 @@ +using System; +using System.Threading.Tasks; +using Xamarin.CommunityToolkit.UnitTests.Mocks; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests +{ + public abstract class BaseCommandTests + { + public const int Delay = 500; + + public BaseCommandTests() => Device.PlatformServices = new MockPlatformServices(); + + protected Task NoParameterTask() => Task.Delay(Delay); + + protected Task IntParameterTask(int delay) => Task.Delay(delay); + + protected Task StringParameterTask(string text) => Task.Delay(Delay); + + protected Task NoParameterImmediateNullReferenceExceptionTask() => throw new NullReferenceException(); + + protected Task ParameterImmediateNullReferenceExceptionTask(int delay) => throw new NullReferenceException(); + + protected async Task NoParameterDelayedNullReferenceExceptionTask() + { + await Task.Delay(Delay); + throw new NullReferenceException(); + } + + protected async Task IntParameterDelayedNullReferenceExceptionTask(int delay) + { + await Task.Delay(delay); + throw new NullReferenceException(); + } + + protected bool CanExecuteTrue(bool parameter) => true; + + protected bool CanExecuteTrue(string parameter) => true; + + protected bool CanExecuteTrue(object parameter) => true; + + protected bool CanExecuteFalse(bool parameter) => false; + + protected bool CanExecuteFalse(string parameter) => false; + + protected bool CanExecuteFalse(object parameter) => false; + + protected bool CanExecuteDynamic(object booleanParameter) + { + if (booleanParameter is bool parameter) + return parameter; + + throw new InvalidCastException(); + } + } +} diff --git a/XamarinCommunityToolkit.UnitTests/Xamarin.CommunityToolkit.UnitTests.csproj b/XamarinCommunityToolkit.UnitTests/Xamarin.CommunityToolkit.UnitTests.csproj index 40347cad..91084bc2 100644 --- a/XamarinCommunityToolkit.UnitTests/Xamarin.CommunityToolkit.UnitTests.csproj +++ b/XamarinCommunityToolkit.UnitTests/Xamarin.CommunityToolkit.UnitTests.csproj @@ -20,6 +20,7 @@ + diff --git a/XamarinCommunityToolkit/Exceptions/InvalidCommandParameterException.shared.cs b/XamarinCommunityToolkit/Exceptions/InvalidCommandParameterException.shared.cs new file mode 100644 index 00000000..33f52765 --- /dev/null +++ b/XamarinCommunityToolkit/Exceptions/InvalidCommandParameterException.shared.cs @@ -0,0 +1,55 @@ +using System; + +// Inspired by AsyncAwaitBestPractices.MVVM.InvalidCommandParameterException: https://github.com/brminnick/AsyncAwaitBestPractices +namespace Xamarin.CommunityToolkit.Exceptions +{ + /// + /// Represents errors that occur during IAsyncCommand execution. + /// + public class InvalidCommandParameterException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// Expected parameter type for AsyncCommand.Execute. + /// Actual parameter type for AsyncCommand.Execute. + /// Inner Exception + public InvalidCommandParameterException(Type expectedType, Type actualType, Exception innerException) + : base(CreateErrorMessage(expectedType, actualType), innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Expected parameter type for AsyncCommand.Execute. + /// Actual parameter type for AsyncCommand.Execute. + public InvalidCommandParameterException(Type expectedType, Type actualType) + : base(CreateErrorMessage(expectedType, actualType)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Expected parameter type for AsyncCommand.Execute. + /// Inner Exception + public InvalidCommandParameterException(Type expectedType, Exception innerException) + : base(CreateErrorMessage(expectedType), innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Expected parameter type for AsyncCommand.Execute. + public InvalidCommandParameterException(Type expectedType) + : base(CreateErrorMessage(expectedType)) + { + } + + static string CreateErrorMessage(Type expectedType) => $"Invalid type for parameter. Expected Type {expectedType}"; + + static string CreateErrorMessage(Type expectedType, Type actualType) => $"Invalid type for parameter. Expected Type {expectedType}, but received Type {actualType}"; + } +} diff --git a/XamarinCommunityToolkit/Exceptions/InvalidHandleEventException.shared.cs b/XamarinCommunityToolkit/Exceptions/InvalidHandleEventException.shared.cs new file mode 100644 index 00000000..6958c6a3 --- /dev/null +++ b/XamarinCommunityToolkit/Exceptions/InvalidHandleEventException.shared.cs @@ -0,0 +1,22 @@ +using System; +using System.Reflection; + +// Inspired by AsyncAwaitBestPractices.InvalidHandleEventException: https://github.com/brminnick/AsyncAwaitBestPractices +namespace Xamarin.CommunityToolkit.Exceptions +{ + /// + /// 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/XamarinCommunityToolkit/Helpers/Subscription.shared.cs b/XamarinCommunityToolkit/Helpers/Subscription.shared.cs new file mode 100644 index 00000000..f342ccdd --- /dev/null +++ b/XamarinCommunityToolkit/Helpers/Subscription.shared.cs @@ -0,0 +1,19 @@ +using System; +using System.Reflection; + +// Inspired by AsyncAwaitBestPractices.Subscription: https://github.com/brminnick/AsyncAwaitBestPractices +namespace Xamarin.CommunityToolkit.Helpers +{ + struct Subscription + { + public WeakReference Subscriber { get; } + + public MethodInfo Handler { get; } + + public Subscription(WeakReference subscriber, MethodInfo handler) + { + Subscriber = subscriber; + Handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } + } +} diff --git a/XamarinCommunityToolkit/Helpers/WeakEventManager.shared.cs b/XamarinCommunityToolkit/Helpers/WeakEventManager.shared.cs new file mode 100644 index 00000000..712886fc --- /dev/null +++ b/XamarinCommunityToolkit/Helpers/WeakEventManager.shared.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; + +using static System.String; + +// Inspired by AsyncAwaitBestPractices.WeakEventManager: https://github.com/brminnick/AsyncAwaitBestPractices +namespace Xamarin.CommunityToolkit.Helpers +{ + /// + /// Weak event manager that allows for garbage collection when the EventHandler is still subscribed + /// + /// Event args type. + public partial class WeakEventManager + { + readonly Dictionary> eventHandlers = new Dictionary>(); + + /// + /// Adds the event handler + /// + /// Handler + /// Event name + public void AddEventHandler(EventHandler handler, [CallerMemberName] string eventName = "") + { + if (IsNullOrWhiteSpace(eventName)) + throw new ArgumentNullException(nameof(eventName)); + + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + 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 == null) + throw new ArgumentNullException(nameof(action)); + + EventManagerService.AddEventHandler(eventName, action.Target, action.GetMethodInfo(), eventHandlers); + } + + /// + /// Removes the event handler + /// + /// Handler + /// Event name + public void RemoveEventHandler(EventHandler handler, [CallerMemberName] string eventName = "") + { + if (IsNullOrWhiteSpace(eventName)) + throw new ArgumentNullException(nameof(eventName)); + + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + 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 == null) + throw new ArgumentNullException(nameof(action)); + + EventManagerService.RemoveEventHandler(eventName, action.Target, action.GetMethodInfo(), eventHandlers); + } + + /// + /// Invokes the event EventHandler + /// + /// Sender + /// Event arguments + /// Event name + public void RaiseEvent(object sender, TEventArgs eventArgs, string eventName) => + EventManagerService.HandleEvent(eventName, sender, eventArgs, eventHandlers); + + /// + /// Invokes the event Action + /// + /// Event arguments + /// Event name + public void RaiseEvent(TEventArgs eventArgs, string eventName) => + EventManagerService.HandleEvent(eventName, eventArgs, eventHandlers); + } + + /// + /// Weak event manager that allows for garbage collection when the EventHandler is still subscribed + /// + public partial class WeakEventManager + { + readonly Dictionary> eventHandlers = new Dictionary>(); + + /// + /// Adds the event handler + /// + /// Handler + /// Event name + public void AddEventHandler(Delegate handler, [CallerMemberName] string eventName = "") + { + if (IsNullOrWhiteSpace(eventName)) + throw new ArgumentNullException(nameof(eventName)); + + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + EventManagerService.AddEventHandler(eventName, handler.Target, handler.GetMethodInfo(), eventHandlers); + } + + /// + /// Removes the event handler. + /// + /// Handler + /// Event name + public void RemoveEventHandler(Delegate handler, [CallerMemberName] string eventName = "") + { + if (IsNullOrWhiteSpace(eventName)) + throw new ArgumentNullException(nameof(eventName)); + + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + EventManagerService.RemoveEventHandler(eventName, handler.Target, handler.GetMethodInfo(), eventHandlers); + } + + /// + /// Invokes the event EventHandler + /// + /// Sender + /// Event arguments + /// Event name + public void RaiseEvent(object sender, object? eventArgs, string eventName) => + EventManagerService.HandleEvent(eventName, sender, eventArgs, eventHandlers); + + /// + /// Invokes the event Action + /// + /// Event name + public void RaiseEvent(string eventName) => EventManagerService.HandleEvent(eventName, eventHandlers); + } +} diff --git a/XamarinCommunityToolkit/Helpers/WeakEventManagerService.shared.cs b/XamarinCommunityToolkit/Helpers/WeakEventManagerService.shared.cs new file mode 100644 index 00000000..003794c3 --- /dev/null +++ b/XamarinCommunityToolkit/Helpers/WeakEventManagerService.shared.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using Xamarin.CommunityToolkit.Exceptions; + +// Inspired by AsyncAwaitBestPractices.WeakEventManagerService: https://github.com/brminnick/AsyncAwaitBestPractices +namespace Xamarin.CommunityToolkit.Helpers +{ + static class EventManagerService + { + internal static void AddEventHandler(in string eventName, in object handlerTarget, in MethodInfo methodInfo, in Dictionary> eventHandlers) + { + var doesContainSubscriptions = eventHandlers.TryGetValue(eventName, out var targets); + if (!doesContainSubscriptions || targets == null) + { + targets = new List(); + eventHandlers.Add(eventName, targets); + } + + if (handlerTarget == null) + targets.Add(new Subscription(null, methodInfo)); + else + targets.Add(new Subscription(new WeakReference(handlerTarget), methodInfo)); + } + + internal static void RemoveEventHandler(in string eventName, in object handlerTarget, in MemberInfo methodInfo, in Dictionary> eventHandlers) + { + var doesContainSubscriptions = eventHandlers.TryGetValue(eventName, out var subscriptions); + if (!doesContainSubscriptions || subscriptions == null) + return; + + for (var n = subscriptions.Count; n > 0; n--) + { + var current = subscriptions[n - 1]; + + if (current.Subscriber?.Target != handlerTarget + || current.Handler.Name != methodInfo?.Name) + { + continue; + } + + subscriptions.Remove(current); + break; + } + } + + internal static void HandleEvent(in string eventName, in object sender, in object eventArgs, in Dictionary> eventHandlers) + { + AddRemoveEvents(eventName, eventHandlers, out var toRaise); + + for (var i = 0; i < toRaise.Count; i++) + { + try + { + var (instance, eventHandler) = toRaise[i]; + if (eventHandler.IsLightweightMethod()) + { + var method = TryGetDynamicMethod(eventHandler); + method?.Invoke(instance, new[] { sender, eventArgs }); + } + else + { + eventHandler.Invoke(instance, new[] { sender, eventArgs }); + } + } + catch (TargetParameterCountException e) + { + 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(in string eventName, in object actionEventArgs, in Dictionary> eventHandlers) + { + AddRemoveEvents(eventName, eventHandlers, out var toRaise); + + for (var i = 0; i < toRaise.Count; i++) + { + try + { + var (instance, eventHandler) = toRaise[i]; + if (eventHandler.IsLightweightMethod()) + { + var method = TryGetDynamicMethod(eventHandler); + method?.Invoke(instance, new[] { actionEventArgs }); + } + else + { + eventHandler.Invoke(instance, new[] { actionEventArgs }); + } + } + catch (TargetParameterCountException e) + { + 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(in string eventName, in Dictionary> eventHandlers) + { + AddRemoveEvents(eventName, eventHandlers, out var toRaise); + + for (var i = 0; i < toRaise.Count; i++) + { + try + { + var (instance, eventHandler) = toRaise[i]; + if (eventHandler.IsLightweightMethod()) + { + var method = TryGetDynamicMethod(eventHandler); + method?.Invoke(instance, null); + } + else + { + eventHandler.Invoke(instance, null); + } + } + catch (TargetParameterCountException e) + { + 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<(object Instance, MethodInfo EventHandler)> toRaise) + { + var toRemove = new List(); + toRaise = new List<(object, MethodInfo)>(); + + var doesContainEventName = eventHandlers.TryGetValue(eventName, out var target); + if (doesContainEventName && target != null) + { + for (var i = 0; i < target.Count; i++) + { + var subscription = target[i]; + var isStatic = subscription.Subscriber == null; + + if (isStatic) + { + toRaise.Add((null, subscription.Handler)); + continue; + } + + var subscriber = subscription.Subscriber?.Target; + + if (subscriber == null) + toRemove.Add(subscription); + else + toRaise.Add((subscriber, subscription.Handler)); + } + + for (var i = 0; i < toRemove.Count; i++) + { + var subscription = toRemove[i]; + target.Remove(subscription); + } + } + } + + static DynamicMethod TryGetDynamicMethod(in MethodInfo rtDynamicMethod) + { + var typeInfoRTDynamicMethod = typeof(DynamicMethod).GetTypeInfo().GetDeclaredNestedType("RTDynamicMethod"); + var typeRTDynamicMethod = typeInfoRTDynamicMethod?.AsType(); + + return (typeInfoRTDynamicMethod?.IsAssignableFrom(rtDynamicMethod.GetType().GetTypeInfo()) ?? false) ? + (DynamicMethod)typeRTDynamicMethod.GetRuntimeFields().First(f => f.Name is "m_owner").GetValue(rtDynamicMethod) + : null; + } + + static bool IsLightweightMethod(this MethodBase method) + { + var typeInfoRTDynamicMethod = typeof(DynamicMethod).GetTypeInfo().GetDeclaredNestedType("RTDynamicMethod"); + return method is DynamicMethod || (typeInfoRTDynamicMethod?.IsAssignableFrom(method.GetType().GetTypeInfo()) ?? false); + } + } +} diff --git a/XamarinCommunityToolkit/ObjectModel/AsyncCommand.shared.cs b/XamarinCommunityToolkit/ObjectModel/AsyncCommand.shared.cs new file mode 100644 index 00000000..2a98fb3d --- /dev/null +++ b/XamarinCommunityToolkit/ObjectModel/AsyncCommand.shared.cs @@ -0,0 +1,101 @@ +using System; +using System.Threading.Tasks; + +// Inspired by AsyncAwaitBestPractices.MVVM.AsyncCommand: https://github.com/brminnick/AsyncAwaitBestPractices +namespace Xamarin.CommunityToolkit.ObjectModel +{ + /// + /// An implementation of IAsyncCommand. Allows Commands to safely be used asynchronously with Task. + /// + public class AsyncCommand : BaseAsyncCommand, IAsyncCommand + { + /// + /// Initializes a new instance of the AsyncCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// 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 + public AsyncCommand( + Func execute, + Func canExecute = null, + Action onException = null, + bool continueOnCapturedContext = false, + bool allowsMultipleExecutions = true) + : base(execute, canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions) + { + } + + /// + /// Executes the Command as a Task + /// + /// The executed Task + public new Task ExecuteAsync(TExecute parameter) => base.ExecuteAsync(parameter); + } + + /// + /// An implementation of IAsyncCommand. Allows Commands to safely be used asynchronously with Task. + /// + public class AsyncCommand : BaseAsyncCommand, IAsyncCommand + { + /// + /// Initializes a new instance of AsyncCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// 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 + public AsyncCommand( + Func execute, + Func canExecute = null, + Action onException = null, + bool continueOnCapturedContext = false, + bool allowsMultipleExecutions = true) + : base(execute, canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions) + { + } + + /// + /// Executes the Command as a Task + /// + /// The executed Task + public new Task ExecuteAsync(T parameter) => base.ExecuteAsync(parameter); + } + + /// + /// An implementation of IAsyncCommand. Allows Commands to safely be used asynchronously with Task. + /// + public class AsyncCommand : BaseAsyncCommand, IAsyncCommand + { + /// + /// Initializes a new instance of AsyncCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// 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 + public AsyncCommand( + Func execute, + Func canExecute = null, + Action onException = null, + bool continueOnCapturedContext = false, + bool allowsMultipleExecutions = true) + : base(ConvertExecute(execute), canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions) + { + } + + /// + /// Executes the Command as a Task + /// + /// The executed Task + public Task ExecuteAsync() => ExecuteAsync(null); + + static Func ConvertExecute(Func execute) + { + if (execute == null) + return null; + + return _ => execute(); + } + } +} diff --git a/XamarinCommunityToolkit/ObjectModel/AsyncValueCommand.cs b/XamarinCommunityToolkit/ObjectModel/AsyncValueCommand.cs new file mode 100644 index 00000000..16b7c484 --- /dev/null +++ b/XamarinCommunityToolkit/ObjectModel/AsyncValueCommand.cs @@ -0,0 +1,8 @@ +// Inspired by AsyncAwaitBestPractices.MVVM.AsyncCommand: https://github.com/brminnick/AsyncAwaitBestPractices +namespace Xamarin.CommunityToolkit.ObjectModel +{ + public class AsyncValueCommand : AsyncValueCommand + { + + } +} diff --git a/XamarinCommunityToolkit/ObjectModel/AsyncValueCommand.shared.cs b/XamarinCommunityToolkit/ObjectModel/AsyncValueCommand.shared.cs new file mode 100644 index 00000000..ddf712e9 --- /dev/null +++ b/XamarinCommunityToolkit/ObjectModel/AsyncValueCommand.shared.cs @@ -0,0 +1,99 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Input; + +// Inspired by AsyncAwaitBestPractices.MVVM.AsyncCommand: https://github.com/brminnick/AsyncAwaitBestPractices +namespace Xamarin.CommunityToolkit.ObjectModel +{ + public class AsyncValueCommand : BaseAsyncValueCommand, IAsyncValueCommand + { + /// + /// Initializes a new instance of AsyncValueCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// 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 + public AsyncValueCommand( + Func execute, + Func canExecute = null, + Action onException = null, + bool continueOnCapturedContext = false, + bool allowsMultipleExecutions = true) + : base(execute, canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions) + { + } + + /// + /// Executes the Command as a ValueTask + /// + /// The executed ValueTask + public new ValueTask ExecuteAsync(TExecute parameter) => base.ExecuteAsync(parameter); + } + + /// + /// An implementation of IAsyncValueCommand. Allows Commands to safely be used asynchronously with Task. + /// + public class AsyncValueCommand : BaseAsyncValueCommand, IAsyncValueCommand + { + /// + /// Initializes a new instance of AsyncValueCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// 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 + public AsyncValueCommand( + Func execute, + Func canExecute = null, + Action onException = null, + bool continueOnCapturedContext = false, + bool allowsMultipleExecutions = true) + : base(execute, canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions) + { + } + + /// + /// Executes the Command as a ValueTask + /// + /// The executed ValueTask + public new ValueTask ExecuteAsync(T parameter) => base.ExecuteAsync(parameter); + } + + /// + /// An implementation of IAsyncValueCommand. Allows Commands to safely be used asynchronously with Task. + /// + public class AsyncValueCommand : BaseAsyncValueCommand, IAsyncValueCommand + { + /// + /// Initializes a new instance of AsyncValueCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// 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 + public AsyncValueCommand( + Func execute, + Func canExecute = null, + Action onException = null, + bool continueOnCapturedContext = false, + bool allowsMultipleExecutions = true) + : base(ConvertExecute(execute), canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions) + { + } + + /// + /// Executes the Command as a ValueTask + /// + /// The executed ValueTask + public ValueTask ExecuteAsync() => ExecuteAsync(null); + + static Func ConvertExecute(Func execute) + { + if (execute == null) + return null; + + return _ => execute(); + } + } +} diff --git a/XamarinCommunityToolkit/ObjectModel/BaseAsyncCommand.shared.cs b/XamarinCommunityToolkit/ObjectModel/BaseAsyncCommand.shared.cs new file mode 100644 index 00000000..d1e9479a --- /dev/null +++ b/XamarinCommunityToolkit/ObjectModel/BaseAsyncCommand.shared.cs @@ -0,0 +1,95 @@ +using System; +using System.ComponentModel; +using System.Reflection; +using System.Threading.Tasks; +using System.Windows.Input; +using Xamarin.CommunityToolkit.Exceptions; + +namespace Xamarin.CommunityToolkit.ObjectModel +{ + /// + /// Abstract Base Class used by AsyncValueCommand + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class BaseAsyncCommand : BaseCommand, ICommand + { + readonly Func execute; + readonly Action onException; + readonly bool continueOnCapturedContext; + + /// + /// Initializes a new instance of BaseAsyncCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// 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 + public BaseAsyncCommand( + Func execute, + Func canExecute, + Action onException, + bool continueOnCapturedContext, + bool allowsMultipleExecutions) + : base(canExecute, allowsMultipleExecutions) + { + this.execute = execute ?? throw new ArgumentNullException(nameof(execute), $"{nameof(execute)} cannot be null"); + this.onException = onException; + this.continueOnCapturedContext = continueOnCapturedContext; + } + + /// + /// Executes the Command as a Task + /// + /// The executed Task + /// Data used by the command. If the command does not require data to be passed, this object can be set to null. + private protected async Task ExecuteAsync(TExecute parameter) + { + ExecutionCount++; + + try + { + await execute(parameter).ConfigureAwait(continueOnCapturedContext); + } + catch (Exception e) when (onException != null) + { + onException(e); + } + finally + { + if (--ExecutionCount <= 0) + ExecutionCount = 0; + } + } + + bool ICommand.CanExecute(object parameter) => parameter switch + { + TCanExecute validParameter => CanExecute(validParameter), + null when !typeof(TCanExecute).GetTypeInfo().IsValueType => CanExecute((TCanExecute)parameter), + null => throw new InvalidCommandParameterException(typeof(TCanExecute)), + _ => throw new InvalidCommandParameterException(typeof(TCanExecute), parameter.GetType()), + }; + + void ICommand.Execute(object parameter) + { + switch (parameter) + { + case TExecute validParameter: + Execute(validParameter); + break; + + case null when !typeof(TExecute).GetTypeInfo().IsValueType: + Execute((TExecute)parameter); + break; + + case null: + throw new InvalidCommandParameterException(typeof(TExecute)); + + default: + throw new InvalidCommandParameterException(typeof(TExecute), parameter.GetType()); + } + + // Use local method to defer async void from ICommand.Execute, allowing InvalidCommandParameterException to be thrown on the calling thread context before reaching an async method + async void Execute(TExecute parameter) => await ExecuteAsync(parameter).ConfigureAwait(continueOnCapturedContext); + } + } +} diff --git a/XamarinCommunityToolkit/ObjectModel/BaseAsyncValueCommand.shared.cs b/XamarinCommunityToolkit/ObjectModel/BaseAsyncValueCommand.shared.cs new file mode 100644 index 00000000..050cc52a --- /dev/null +++ b/XamarinCommunityToolkit/ObjectModel/BaseAsyncValueCommand.shared.cs @@ -0,0 +1,95 @@ +using System; +using System.ComponentModel; +using System.Reflection; +using System.Threading.Tasks; +using System.Windows.Input; +using Xamarin.CommunityToolkit.Exceptions; + +namespace Xamarin.CommunityToolkit.ObjectModel +{ + /// + /// Abstract Base Class used by AsyncValueCommand + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class BaseAsyncValueCommand : BaseCommand, ICommand + { + readonly Func execute; + readonly Action onException; + readonly bool continueOnCapturedContext; + + /// + /// Initializes a new instance of BaseAsyncValueCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// 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 + public BaseAsyncValueCommand( + Func execute, + Func canExecute, + Action onException, + bool continueOnCapturedContext, + bool allowsMultipleExecutions) + : base(canExecute, allowsMultipleExecutions) + { + this.execute = execute ?? throw new ArgumentNullException(nameof(execute), $"{nameof(execute)} cannot be null"); + this.onException = onException; + this.continueOnCapturedContext = continueOnCapturedContext; + } + + /// + /// Executes the Command as a Task + /// + /// The executed Value + /// Data used by the command. If the command does not require data to be passed, this object can be set to null. + private protected async ValueTask ExecuteAsync(TExecute parameter) + { + ExecutionCount++; + + try + { + await execute(parameter).ConfigureAwait(continueOnCapturedContext); + } + catch (Exception e) when (onException != null) + { + onException(e); + } + finally + { + if (--ExecutionCount <= 0) + ExecutionCount = 0; + } + } + + bool ICommand.CanExecute(object parameter) => parameter switch + { + TCanExecute validParameter => CanExecute(validParameter), + null when !typeof(TCanExecute).GetTypeInfo().IsValueType => CanExecute((TCanExecute)parameter), + null => throw new InvalidCommandParameterException(typeof(TCanExecute)), + _ => throw new InvalidCommandParameterException(typeof(TCanExecute), parameter.GetType()), + }; + + void ICommand.Execute(object parameter) + { + switch (parameter) + { + case TExecute validParameter: + Execute(validParameter); + break; + + case null when !typeof(TExecute).GetTypeInfo().IsValueType: + Execute((TExecute)parameter); + break; + + case null: + throw new InvalidCommandParameterException(typeof(TExecute)); + + default: + throw new InvalidCommandParameterException(typeof(TExecute), parameter.GetType()); + } + + // Use local method to defer async void from ICommand.Execute, allowing InvalidCommandParameterException to be thrown on the calling thread context before reaching an async method + async void Execute(TExecute parameter) => await ExecuteAsync(parameter).ConfigureAwait(continueOnCapturedContext); + } + } +} diff --git a/XamarinCommunityToolkit/ObjectModel/BaseCommand.shared.cs b/XamarinCommunityToolkit/ObjectModel/BaseCommand.shared.cs new file mode 100644 index 00000000..a7425f89 --- /dev/null +++ b/XamarinCommunityToolkit/ObjectModel/BaseCommand.shared.cs @@ -0,0 +1,90 @@ +using System; +using System.ComponentModel; +using Xamarin.CommunityToolkit.Helpers; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.ObjectModel +{ + /// + /// Abstract Base Class used by AsyncCommand and AsyncValueCommand + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class BaseCommand + { + readonly Func canExecute; + readonly WeakEventManager weakEventManager = new WeakEventManager(); + + int executionCount; + + /// + /// Initializes BaseCommand + /// + /// + /// + public BaseCommand(Func canExecute, bool allowsMultipleExecutions) + { + this.canExecute = canExecute ?? (_ => true); + AllowsMultipleExecutions = allowsMultipleExecutions; + } + + /// + /// Occurs when changes occur that affect whether or not the command should execute + /// + public event EventHandler CanExecuteChanged + { + add => weakEventManager.AddEventHandler(value); + remove => weakEventManager.RemoveEventHandler(value); + } + + /// + /// Returns true when the Command is currently executing. Returns false when the Command is not executing + /// + public bool IsExecuting => ExecutionCount > 0; + + /// + /// Returns true if the Command allows simultaneous executions + /// + public bool AllowsMultipleExecutions { get; } + + protected int ExecutionCount + { + get => executionCount; + set + { + var shouldRaiseCanExecuteChanged = AllowsMultipleExecutions switch + { + true => false, + false when executionCount is 0 && value > 0 => true, + false when executionCount > 0 && value is 0 => true, + false => false + }; + + executionCount = value; + + if (shouldRaiseCanExecuteChanged) + RaiseCanExecuteChanged(); + } + } + + /// + /// Determines whether the command can execute in its current state + /// + /// true, if this command can be executed; otherwise, false. + /// Data used by the command. If the command does not require data to be passed, this object can be set to null. + public bool CanExecute(TCanExecute parameter) => (AllowsMultipleExecutions, IsExecuting) switch + { + (true, _) => canExecute(parameter), + (false, true) => false, + (false, false) => canExecute(parameter), + }; + + /// + /// Raises the CanExecuteChanged event. + /// + public void RaiseCanExecuteChanged() + { + // Automatically marshall to the Main Thread to adhere to the way that Xamarin.Forms automatically marshalls binding events to Main Thread + Device.BeginInvokeOnMainThread(() => weakEventManager.RaiseEvent(this, EventArgs.Empty, nameof(CanExecuteChanged))); + } + } +} diff --git a/XamarinCommunityToolkit/ObjectModel/IAsyncCommand.shared.cs b/XamarinCommunityToolkit/ObjectModel/IAsyncCommand.shared.cs new file mode 100644 index 00000000..4e3daa2c --- /dev/null +++ b/XamarinCommunityToolkit/ObjectModel/IAsyncCommand.shared.cs @@ -0,0 +1,71 @@ +// Inspired by AsyncAwaitBestPractices.MVVM.IAsyncCommand: https://github.com/brminnick/AsyncAwaitBestPractices +namespace Xamarin.CommunityToolkit.ObjectModel +{ + /// + /// An Async implementation of ICommand for Task + /// + public interface IAsyncCommand : IAsyncCommand + { + /// + /// Determines whether the command can execute in its current state + /// + /// true, if this command can be executed; otherwise, false. + /// Data used by the command. If the command does not require data to be passed, this object can be set to null. + bool CanExecute(TCanExecute parameter); + } + + /// + /// An Async implementation of ICommand for Task + /// + public interface IAsyncCommand : System.Windows.Input.ICommand + { + /// + /// Returns true when the Command is currently executing. Returns false when the Command is not executing + /// + bool IsExecuting { get; } + + /// + /// Returns true if the Command allows simultaneous executions + /// + bool AllowsMultipleExecutions { get; } + + /// + /// Executes the Command as a Task + /// + /// The Task to execute + /// Data used by the command. If the command does not require data to be passed, this object can be set to null. + System.Threading.Tasks.Task ExecuteAsync(T parameter); + + /// + /// Raises the CanExecuteChanged event. + /// + void RaiseCanExecuteChanged(); + } + + /// + /// An Async implementation of ICommand for Task + /// + public interface IAsyncCommand : System.Windows.Input.ICommand + { + /// + /// Returns true when the Command is currently executing. Returns false when the Command is not executing + /// + bool IsExecuting { get; } + + /// + /// Returns true if the Command allows simultaneous executions + /// + bool AllowsMultipleExecutions { get; } + + /// + /// Executes the Command as a Task + /// + /// The Task to execute + System.Threading.Tasks.Task ExecuteAsync(); + + /// + /// Raises the CanExecuteChanged event. + /// + void RaiseCanExecuteChanged(); + } +} diff --git a/XamarinCommunityToolkit/ObjectModel/IAsyncValueCommand.shared.cs b/XamarinCommunityToolkit/ObjectModel/IAsyncValueCommand.shared.cs new file mode 100644 index 00000000..b8e39624 --- /dev/null +++ b/XamarinCommunityToolkit/ObjectModel/IAsyncValueCommand.shared.cs @@ -0,0 +1,71 @@ +// Inspired by AsyncAwaitBestPractices.MVVM.IAsyncValueCommand: https://github.com/brminnick/AsyncAwaitBestPractices +namespace Xamarin.CommunityToolkit.ObjectModel +{ + /// + /// An Async implementation of ICommand for ValueTask + /// + public interface IAsyncValueCommand : IAsyncValueCommand + { + /// + /// Determines whether the command can execute in its current state + /// + /// true, if this command can be executed; otherwise, false. + /// Data used by the command. If the command does not require data to be passed, this object can be set to null. + bool CanExecute(TCanExecute parameter); + } + + /// + /// An Async implementation of ICommand for ValueTask + /// + public interface IAsyncValueCommand : System.Windows.Input.ICommand + { + /// + /// Returns true when the Command is currently executing. Returns false when the Command is not executing + /// + bool IsExecuting { get; } + + /// + /// Returns true if the Command allows simultaneous executions + /// + bool AllowsMultipleExecutions { get; } + + /// + /// Executes the Command as a ValueTask + /// + /// The ValueTask to execute + /// Data used by the command. If the command does not require data to be passed, this object can be set to null. + System.Threading.Tasks.ValueTask ExecuteAsync(T parameter); + + /// + /// Raises the CanExecuteChanged event. + /// + void RaiseCanExecuteChanged(); + } + + /// + /// An Async implementation of ICommand for ValueTask + /// + public interface IAsyncValueCommand : System.Windows.Input.ICommand + { + /// + /// Returns true when the Command is currently executing. Returns false when the Command is not executing + /// + bool IsExecuting { get; } + + /// + /// Returns true if the Command allows simultaneous executions + /// + bool AllowsMultipleExecutions { get; } + + /// + /// Executes the Command as a ValueTask + /// + /// The ValueTask to execute + System.Threading.Tasks.ValueTask ExecuteAsync(); + + /// + /// Raises the CanExecuteChanged event. + /// + void RaiseCanExecuteChanged(); + } +} diff --git a/XamarinCommunityToolkit/Views/CameraView/CameraView.shared.cs b/XamarinCommunityToolkit/Views/CameraView/CameraView.shared.cs index 2ccda003..17b7d669 100644 --- a/XamarinCommunityToolkit/Views/CameraView/CameraView.shared.cs +++ b/XamarinCommunityToolkit/Views/CameraView/CameraView.shared.cs @@ -1,5 +1,4 @@ using System; -using System.ComponentModel; using Xamarin.Forms; namespace Xamarin.CommunityToolkit.UI.Views @@ -22,8 +21,7 @@ namespace Xamarin.CommunityToolkit.UI.Views set => SetValue(IsBusyProperty, value); } - public static readonly BindableProperty IsAvailableProperty = BindableProperty.Create(nameof(IsAvailable), typeof(bool), typeof(CameraView), false, - propertyChanged: (b, o, n) => ((CameraView)b).OnAvailable?.Invoke(b, (bool)n)); + public static readonly BindableProperty IsAvailableProperty = BindableProperty.Create(nameof(IsAvailable), typeof(bool), typeof(CameraView), false, propertyChanged: (b, o, n) => ((CameraView)b).OnAvailable?.Invoke(b, (bool)n)); public bool IsAvailable { diff --git a/XamarinCommunityToolkit/Views/Expander/Expander.shared.cs b/XamarinCommunityToolkit/Views/Expander/Expander.shared.cs index abfc0815..222ed898 100644 --- a/XamarinCommunityToolkit/Views/Expander/Expander.shared.cs +++ b/XamarinCommunityToolkit/Views/Expander/Expander.shared.cs @@ -1,5 +1,6 @@ using System; using System.Windows.Input; +using Xamarin.CommunityToolkit.Helpers; using Xamarin.Forms; using static System.Math; @@ -12,7 +13,13 @@ namespace Xamarin.CommunityToolkit.UI.Views const uint defaultAnimationLength = 250; - public event EventHandler Tapped; + readonly WeakEventManager tappedEventManager = new WeakEventManager(); + + public event EventHandler Tapped + { + add => tappedEventManager.AddEventHandler(value); + remove => tappedEventManager.RemoveEventHandler(value); + } ContentView contentHolder; @@ -208,7 +215,7 @@ namespace Xamarin.CommunityToolkit.UI.Views } IsExpanded = !IsExpanded; Command?.Execute(CommandParameter); - Tapped?.Invoke(this, EventArgs.Empty); + OnTapped(); }) }; control.Spacing = 0; @@ -444,5 +451,7 @@ namespace Xamarin.CommunityToolkit.UI.Views State = ExpandState.Expanded; }); } + + void OnTapped() => tappedEventManager.RaiseEvent(this, EventArgs.Empty, nameof(Tapped)); } } \ No newline at end of file diff --git a/XamarinCommunityToolkit/Xamarin.CommunityToolkit.csproj b/XamarinCommunityToolkit/Xamarin.CommunityToolkit.csproj index 20651d81..db4a9920 100644 --- a/XamarinCommunityToolkit/Xamarin.CommunityToolkit.csproj +++ b/XamarinCommunityToolkit/Xamarin.CommunityToolkit.csproj @@ -1,7 +1,7 @@ - netstandard1.0;netstandard2.0;Xamarin.iOS10;MonoAndroid90;MonoAndroid10.0;Xamarin.TVOS10;Xamarin.WatchOS10;Xamarin.Mac20;tizen40 + netstandard1.0;netstandard2.0;netstandard2.1;Xamarin.iOS10;MonoAndroid90;MonoAndroid10.0;Xamarin.TVOS10;Xamarin.WatchOS10;Xamarin.Mac20;tizen40 $(TargetFrameworks);uap10.0.16299 Xamarin.CommunityToolkit Xamarin.CommunityToolkit @@ -66,12 +66,21 @@ - + + + + - + + + + + + + @@ -92,6 +101,10 @@ + + + + @@ -104,6 +117,8 @@ + + @@ -114,7 +129,8 @@ + - + \ No newline at end of file diff --git a/XamarinCommunityToolkitSample/Pages/Base/BasePage.cs b/XamarinCommunityToolkitSample/Pages/Base/BasePage.cs index 53c38e11..2be0fcda 100644 --- a/XamarinCommunityToolkitSample/Pages/Base/BasePage.cs +++ b/XamarinCommunityToolkitSample/Pages/Base/BasePage.cs @@ -1,5 +1,6 @@ using System; using System.Windows.Input; +using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.CommunityToolkit.Sample.Models; using Xamarin.Forms; @@ -11,8 +12,8 @@ namespace Xamarin.CommunityToolkit.Sample.Pages public Color DetailColor { get; set; } - public ICommand NavigateCommand => navigateCommand ??= new Command(parameter - => Navigation.PushAsync(PreparePage((SectionModel)parameter))); + public ICommand NavigateCommand => navigateCommand ??= new AsyncCommand(sectionModel + => Navigation.PushAsync(PreparePage(sectionModel))); Page PreparePage(SectionModel model) { diff --git a/XamarinCommunityToolkitSample/ViewModels/AboutViewModel.cs b/XamarinCommunityToolkitSample/ViewModels/AboutViewModel.cs index 06836b65..1553aca3 100644 --- a/XamarinCommunityToolkitSample/ViewModels/AboutViewModel.cs +++ b/XamarinCommunityToolkitSample/ViewModels/AboutViewModel.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using System.Windows.Input; using Octokit; +using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.CommunityToolkit.Sample.Resx; using Xamarin.Essentials; using Xamarin.Forms; @@ -39,7 +40,7 @@ namespace Xamarin.CommunityToolkit.Sample.ViewModels set => Set(ref emptyViewText, value); } - public ICommand SelectedContributorCommand => selectedContributorCommand ??= new Command(async () => + public ICommand SelectedContributorCommand => selectedContributorCommand ??= new AsyncCommand(async () => { if (SelectedContributor is null) return; @@ -56,9 +57,10 @@ namespace Xamarin.CommunityToolkit.Sample.ViewModels try { var contributors = await gitHubClient.Repository.GetAllContributors("xamarin", "XamarinCommunityToolkit"); - //Initiate poor mans randomizer for lists - //Note: there are better options for real production worthy large lists : https://stackoverflow.com/questions/273313/randomize-a-listt - //But for now this linq version will do + + // Initiate poor mans randomizer for lists + // Note: there are better options for real production worthy large lists : https://stackoverflow.com/questions/273313/randomize-a-listt + // But for now this linq version will do var random = new Random(); var result = contributors?.OrderBy(x => random.Next()).ToArray(); if (result != null) diff --git a/XamarinCommunityToolkitSample/ViewModels/Base/BaseViewModel.cs b/XamarinCommunityToolkitSample/ViewModels/Base/BaseViewModel.cs index c88a3cd9..05df87c5 100644 --- a/XamarinCommunityToolkitSample/ViewModels/Base/BaseViewModel.cs +++ b/XamarinCommunityToolkitSample/ViewModels/Base/BaseViewModel.cs @@ -1,12 +1,19 @@ using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; +using Xamarin.CommunityToolkit.Helpers; namespace Xamarin.CommunityToolkit.Sample.ViewModels { public class BaseViewModel : INotifyPropertyChanged { - public event PropertyChangedEventHandler PropertyChanged; + readonly WeakEventManager propertyChangedEventManager = new WeakEventManager(); + + event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged + { + add => propertyChangedEventManager.AddEventHandler(value); + remove => propertyChangedEventManager.RemoveEventHandler(value); + } protected bool Set(ref T backingStore, T value, [CallerMemberName] string name = null) { @@ -18,7 +25,7 @@ namespace Xamarin.CommunityToolkit.Sample.ViewModels return true; } - protected virtual void OnPropertyChanged([CallerMemberName] string name = null) - => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + protected virtual void OnPropertyChanged([CallerMemberName] string name = "") + => propertyChangedEventManager.RaiseEvent(this, new PropertyChangedEventArgs(name), nameof(INotifyPropertyChanged.PropertyChanged)); } } \ No newline at end of file diff --git a/XamarinCommunityToolkitSample/ViewModels/Converters/ItemSelectedEventArgsViewModel.cs b/XamarinCommunityToolkitSample/ViewModels/Converters/ItemSelectedEventArgsViewModel.cs index 90c83c90..e6af624d 100644 --- a/XamarinCommunityToolkitSample/ViewModels/Converters/ItemSelectedEventArgsViewModel.cs +++ b/XamarinCommunityToolkitSample/ViewModels/Converters/ItemSelectedEventArgsViewModel.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Windows.Input; +using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.CommunityToolkit.Sample.Resx; using Xamarin.Forms; @@ -15,7 +16,7 @@ namespace Xamarin.CommunityToolkit.Sample.ViewModels.Converters new Person() { Id = 3, Name = "Person 3" } }; - public ICommand ItemSelectedCommand { get; private set; } = new Command(async (person) - => await Application.Current.MainPage.DisplayAlert($"{AppResources.ItemTapped}: ", person.Name, AppResources.Cancel)); + public ICommand ItemSelectedCommand { get; private set; } = new AsyncCommand(person + => Application.Current.MainPage.DisplayAlert($"{AppResources.ItemTapped}: ", person.Name, AppResources.Cancel)); } } \ No newline at end of file diff --git a/XamarinCommunityToolkitSample/ViewModels/Converters/ItemTappedEventArgsViewModel.cs b/XamarinCommunityToolkitSample/ViewModels/Converters/ItemTappedEventArgsViewModel.cs index 62773f2d..816da730 100644 --- a/XamarinCommunityToolkitSample/ViewModels/Converters/ItemTappedEventArgsViewModel.cs +++ b/XamarinCommunityToolkitSample/ViewModels/Converters/ItemTappedEventArgsViewModel.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Windows.Input; +using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.CommunityToolkit.Sample.Resx; using Xamarin.Forms; @@ -15,13 +16,14 @@ namespace Xamarin.CommunityToolkit.Sample.ViewModels.Converters new Person() { Id = 3, Name = "Person 3" } }; - public ICommand ItemTappedCommand { get; private set; } = new Command(async (person) - => await Application.Current.MainPage.DisplayAlert($"{AppResources.ItemTapped}: ", person.Name, AppResources.Cancel)); + public ICommand ItemTappedCommand { get; private set; } = new AsyncCommand(person + => Application.Current.MainPage.DisplayAlert($"{AppResources.ItemTapped}: ", person.Name, AppResources.Cancel)); } public class Person { public int Id { get; set; } + public string Name { get; set; } } } \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 48677eab..80444750 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -7,7 +7,7 @@ variables: MONO_VERSION: 6_4_0 XCODE_VERSION: 11.4 NETCORE_VERSION: '3.1.x' - NETCORE_TEST_VERSION: '2.2.x' + NETCORE_TEST_VERSION: '3.1.x' RunPoliCheck: 'false' PathToCsproj: 'XamarinCommunityToolkit/Xamarin.CommunityToolkit.csproj'