From 00497f64962925f89517859f66c1eba14c2425c4 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Wed, 10 Jul 2024 01:04:15 +0800 Subject: [PATCH] feat: add command and observable base and tests. --- .../Common/IRIHI_CommandBase.cs | 42 +++++++ .../Common/IRIHI_CommandBase`T.cs | 81 ++++++++++++ .../Common/IRIHI_ObservableBase.cs | 36 ++++++ .../Irihi.Avalonia.Shared.Public.projitems | 3 + .../Common/IRIHI_CommandBaseGenericTests.cs | 116 ++++++++++++++++++ .../Common/IRIHI_CommandBaseTests.cs | 50 ++++++++ .../Common/IRIHI_ObservableBaseTests.cs | 77 ++++++++++++ ....Avalonia.Shared.UnitTest.Public.projitems | 3 + 8 files changed, 408 insertions(+) create mode 100644 src/Irihi.Avalonia.Shared.Public/Common/IRIHI_CommandBase.cs create mode 100644 src/Irihi.Avalonia.Shared.Public/Common/IRIHI_CommandBase`T.cs create mode 100644 src/Irihi.Avalonia.Shared.Public/Common/IRIHI_ObservableBase.cs create mode 100644 test/Irihi.Avalonia.Shared.UnitTest.Public/Common/IRIHI_CommandBaseGenericTests.cs create mode 100644 test/Irihi.Avalonia.Shared.UnitTest.Public/Common/IRIHI_CommandBaseTests.cs create mode 100644 test/Irihi.Avalonia.Shared.UnitTest.Public/Common/IRIHI_ObservableBaseTests.cs diff --git a/src/Irihi.Avalonia.Shared.Public/Common/IRIHI_CommandBase.cs b/src/Irihi.Avalonia.Shared.Public/Common/IRIHI_CommandBase.cs new file mode 100644 index 0000000..6c9352a --- /dev/null +++ b/src/Irihi.Avalonia.Shared.Public/Common/IRIHI_CommandBase.cs @@ -0,0 +1,42 @@ +using System.Runtime.CompilerServices; +using System.Windows.Input; + +namespace Irihi.Avalonia.Shared.Common; + +/// +/// This is a ICommand implementation for internal use, to avoid irrelevant dependencies. +/// This should not be used by your application. +/// +public class IRIHI_CommandBase: ICommand +{ + private readonly Action _execute; + private readonly Func? _canExecute; + public event EventHandler? CanExecuteChanged; + + public IRIHI_CommandBase(Action execute) + { + _execute = execute; + } + + public IRIHI_CommandBase(Action execute, Func canExecute) + { + _execute = execute; + _canExecute = canExecute; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool CanExecute(object parameter) + { + return _canExecute is null || _canExecute.Invoke(); + } + + public void Execute(object parameter) + { + _execute.Invoke(); + } + + public void NotifyCanExecuteChanged() + { + CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } +} \ No newline at end of file diff --git a/src/Irihi.Avalonia.Shared.Public/Common/IRIHI_CommandBase`T.cs b/src/Irihi.Avalonia.Shared.Public/Common/IRIHI_CommandBase`T.cs new file mode 100644 index 0000000..238905d --- /dev/null +++ b/src/Irihi.Avalonia.Shared.Public/Common/IRIHI_CommandBase`T.cs @@ -0,0 +1,81 @@ +using System.Runtime.CompilerServices; +using System.Windows.Input; + +namespace Irihi.Avalonia.Shared.Common; + +/// +/// This is a ICommand implementation for internal use, to avoid irrelevant dependencies. +/// This should not be used by your application. +/// +public class IRIHI_CommandBase: ICommand +{ + private readonly Action _execute; + private readonly Predicate? _canExecute; + public event EventHandler? CanExecuteChanged; + + public IRIHI_CommandBase(Action execute) + { + _execute = execute; + } + + public IRIHI_CommandBase(Action execute, Predicate canExecute) + { + _execute = execute; + _canExecute = canExecute; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool CanExecute(T? parameter) + { + return _canExecute is null || _canExecute.Invoke(parameter); + } + + public bool CanExecute(object parameter) + { + if (!TryGetCommandArgument(parameter, out var result)) + { + throw new ArgumentException(); + } + return CanExecute(result); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Execute(T? parameter) + { + _execute(parameter); + } + + public void Execute(object parameter) + { + if (!TryGetCommandArgument(parameter, out var result)) + { + throw new ArgumentException(); + } + Execute(result); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool TryGetCommandArgument(object? parameter, out T? result) + { + if (parameter is null && default(T) is null) + { + result = default(T); + return true; + } + if (parameter is T obj) + { + result = obj; + return true; + } + result = default (T); + return false; + } + + public void NotifyCanExecuteChanged() + { + EventHandler canExecuteChanged = this.CanExecuteChanged; + if (canExecuteChanged is null) + return; + canExecuteChanged((object) this, EventArgs.Empty); + } +} \ No newline at end of file diff --git a/src/Irihi.Avalonia.Shared.Public/Common/IRIHI_ObservableBase.cs b/src/Irihi.Avalonia.Shared.Public/Common/IRIHI_ObservableBase.cs new file mode 100644 index 0000000..54f6082 --- /dev/null +++ b/src/Irihi.Avalonia.Shared.Public/Common/IRIHI_ObservableBase.cs @@ -0,0 +1,36 @@ +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Irihi.Avalonia.Shared.Common; + +/// +/// This is a INotifyPropertyChanged and INotifyPropertyChanging implementation for internal use, to avoid irrelevant dependencies. +/// This should not be used by your application. +/// +public class IRIHI_ObservableBase: INotifyPropertyChanged, INotifyPropertyChanging +{ + + public event PropertyChangedEventHandler? PropertyChanged; + + public event PropertyChangingEventHandler? PropertyChanging; + + protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) + { + PropertyChanged ?.Invoke((object) this, e); + } + + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); + } + + protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(field, newValue)) + return false; + field = newValue; + this.OnPropertyChanged(propertyName); + return true; + } +} \ No newline at end of file diff --git a/src/Irihi.Avalonia.Shared.Public/Irihi.Avalonia.Shared.Public.projitems b/src/Irihi.Avalonia.Shared.Public/Irihi.Avalonia.Shared.Public.projitems index e07f2d7..26949ed 100644 --- a/src/Irihi.Avalonia.Shared.Public/Irihi.Avalonia.Shared.Public.projitems +++ b/src/Irihi.Avalonia.Shared.Public/Irihi.Avalonia.Shared.Public.projitems @@ -9,6 +9,9 @@ Irihi.Avalonia.Shared.ShareProject + + + diff --git a/test/Irihi.Avalonia.Shared.UnitTest.Public/Common/IRIHI_CommandBaseGenericTests.cs b/test/Irihi.Avalonia.Shared.UnitTest.Public/Common/IRIHI_CommandBaseGenericTests.cs new file mode 100644 index 0000000..cef35d3 --- /dev/null +++ b/test/Irihi.Avalonia.Shared.UnitTest.Public/Common/IRIHI_CommandBaseGenericTests.cs @@ -0,0 +1,116 @@ +using System; +using Xunit; + +namespace Irihi.Avalonia.Shared.Common.Tests; + +public class IRIHI_CommandBaseGenericTests +{ + [Fact] + public void ExecuteInvokesActionWithParameter() + { + int executedParam = 0; + var command = new IRIHI_CommandBase(param => executedParam = param); + + command.Execute(5); + + Assert.Equal(5, executedParam); + } + + [Fact] + public void CanExecuteReturnsTrueByDefault() + { + var command = new IRIHI_CommandBase(_ => { }); + + bool canExecute = command.CanExecute(null); + + Assert.True(canExecute); + } + + [Fact] + public void CanExecuteReturnsFalseWhenPredicateSpecifiedAndReturnsFalse() + { + var command = new IRIHI_CommandBase(_ => { }, _ => false); + + bool canExecute = command.CanExecute(0); + + Assert.False(canExecute); + } + + [Fact] + public void CanExecuteThrowsArgumentExceptionForInvalidParameterType() + { + var command = new IRIHI_CommandBase(_ => { }); + + Assert.Throws(() => command.CanExecute("invalid")); + } + + [Fact] + public void ExecuteThrowsArgumentExceptionForInvalidParameterType() + { + var command = new IRIHI_CommandBase(_ => { }); + + Assert.Throws(() => command.Execute("invalid")); + } + + [Fact] + public void NotifyCanExecuteChangedRaisesEvent() + { + var command = new IRIHI_CommandBase(_ => { }); + bool eventRaised = false; + command.CanExecuteChanged += (sender, e) => eventRaised = true; + + command.NotifyCanExecuteChanged(); + + Assert.True(eventRaised); + } + + [Fact] + public void CanExecuteWithValidParameterTypeReturnsTrue() + { + var command = new IRIHI_CommandBase(_ => { }, _ => true); + bool canExecute = command.CanExecute(1); + Assert.True(canExecute); + } + + [Fact] + public void ExecuteWithValidParameterInvokesAction() + { + int executedParam = 0; + var command = new IRIHI_CommandBase(param => executedParam = param); + command.Execute(10); + Assert.Equal(10, executedParam); + } + + [Fact] + public void UnsubscribingFromCanExecuteChangedEventWorks() + { + var command = new IRIHI_CommandBase(_ => { }); + bool eventRaised = false; + + EventHandler handler = (sender, e) => eventRaised = true; + command.CanExecuteChanged += handler; + command.CanExecuteChanged -= handler; + + command.NotifyCanExecuteChanged(); + Assert.False(eventRaised); + } + + [Fact] + public void CanExecuteWithRightTypeReturnsTrue() + { + object o = 1; + var command = new IRIHI_CommandBase(_ => { }); + bool canExecute = command.CanExecute(o); + Assert.True(canExecute); + } + + [Fact] + public void ExecuteWithRightTypeInvokesAction() + { + object o = 1; + int executedParam = 0; + var command = new IRIHI_CommandBase(param => executedParam = param); + command.Execute(o); + Assert.Equal(1, executedParam); + } +} \ No newline at end of file diff --git a/test/Irihi.Avalonia.Shared.UnitTest.Public/Common/IRIHI_CommandBaseTests.cs b/test/Irihi.Avalonia.Shared.UnitTest.Public/Common/IRIHI_CommandBaseTests.cs new file mode 100644 index 0000000..e93086d --- /dev/null +++ b/test/Irihi.Avalonia.Shared.UnitTest.Public/Common/IRIHI_CommandBaseTests.cs @@ -0,0 +1,50 @@ +using System; +using Xunit; + +namespace Irihi.Avalonia.Shared.Common.Tests; + +public class IRIHI_CommandBaseTests +{ + [Fact] + public void ExecuteInvokesAction() + { + bool executed = false; + var command = new IRIHI_CommandBase(() => executed = true); + + command.Execute(null); + + Assert.True(executed); + } + + [Fact] + public void CanExecuteReturnsTrueByDefault() + { + var command = new IRIHI_CommandBase(() => { }); + + bool canExecute = command.CanExecute(null); + + Assert.True(canExecute); + } + + [Fact] + public void CanExecuteReturnsFalseWhenSpecified() + { + var command = new IRIHI_CommandBase(() => { }, () => false); + + bool canExecute = command.CanExecute(null); + + Assert.False(canExecute); + } + + [Fact] + public void NotifyCanExecuteChangedRaisesEvent() + { + var command = new IRIHI_CommandBase(() => { }); + bool eventRaised = false; + command.CanExecuteChanged += (sender, e) => eventRaised = true; + + command.NotifyCanExecuteChanged(); + + Assert.True(eventRaised); + } +} \ No newline at end of file diff --git a/test/Irihi.Avalonia.Shared.UnitTest.Public/Common/IRIHI_ObservableBaseTests.cs b/test/Irihi.Avalonia.Shared.UnitTest.Public/Common/IRIHI_ObservableBaseTests.cs new file mode 100644 index 0000000..0d63e0b --- /dev/null +++ b/test/Irihi.Avalonia.Shared.UnitTest.Public/Common/IRIHI_ObservableBaseTests.cs @@ -0,0 +1,77 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Xunit; + +namespace Irihi.Avalonia.Shared.Common.Tests; + +public class IRIHI_ObservableBaseTests +{ + private class TestObservable : IRIHI_ObservableBase + { + internal string _testProperty; + + public string TestProperty + { + get => _testProperty; + set => SetProperty(ref _testProperty, value); + } + + public TestObservable() + { + _testProperty = string.Empty; + } + + public new bool SetProperty(ref T storage, T value, [CallerMemberName] string propertyName = "") + { + return base.SetProperty(ref storage, value, propertyName); + } + } + + [Fact] + public void PropertyChangedEventFiresOnPropertyChange() + { + var testObject = new TestObservable(); + bool eventFired = false; + testObject.PropertyChanged += (sender, e) => + { + if (e.PropertyName == nameof(TestObservable.TestProperty)) + eventFired = true; + }; + + testObject.TestProperty = "New Value"; + + Assert.True(eventFired); + } + + [Fact] + public void PropertyChangedEventDoesNotFireWhenValueIsUnchanged() + { + var testObject = new TestObservable(); + testObject.TestProperty = "Initial Value"; + bool eventFired = false; + testObject.PropertyChanged += (sender, e) => eventFired = true; + + testObject.TestProperty = "Initial Value"; + + Assert.False(eventFired); + } + + [Fact] + public void SetPropertyReturnsTrueWhenValueChanges() + { + var testObject = new TestObservable(); + bool result = testObject.SetProperty(ref testObject._testProperty, "New Value"); + + Assert.True(result); + } + + [Fact] + public void SetPropertyReturnsFalseWhenValueIsUnchanged() + { + var testObject = new TestObservable(); + testObject.TestProperty = "Initial Value"; + bool result = testObject.SetProperty(ref testObject._testProperty, "Initial Value"); + + Assert.False(result); + } +} \ No newline at end of file diff --git a/test/Irihi.Avalonia.Shared.UnitTest.Public/Irihi.Avalonia.Shared.UnitTest.Public.projitems b/test/Irihi.Avalonia.Shared.UnitTest.Public/Irihi.Avalonia.Shared.UnitTest.Public.projitems index 82d34ed..f5a7fb9 100644 --- a/test/Irihi.Avalonia.Shared.UnitTest.Public/Irihi.Avalonia.Shared.UnitTest.Public.projitems +++ b/test/Irihi.Avalonia.Shared.UnitTest.Public/Irihi.Avalonia.Shared.UnitTest.Public.projitems @@ -9,6 +9,9 @@ Irihi.Avalonia.Shared.UnitTest.Public + + +