diff --git a/README.md b/README.md index 4d02064..ec2a359 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,6 @@ A package that brings data-binding to your Unity project. - [Property](#propertyt--readonlypropertyt) - [Command](#command--commandt) - [AsyncCommand](#asynccommand--asynccommandt) - - [AsyncLazyCommand](#asynclazycommand--asynclazycommandt) - [PropertyValueConverter](#propertyvalueconvertertsourcetype-ttargettype) - [ParameterValueConverter](#parametervalueconverterttargettype) - [Quick start](#watch-quick-start) @@ -198,7 +197,6 @@ The included types are: - [Property\ & ReadOnlyProperty\](#propertyt--readonlypropertyt) - [Command & Command\](#command--commandt) - [AsyncCommand & AsyncCommand\](#asynccommand--asynccommandt) -- [AsyncLazyCommand & AsyncLazyCommand\](#asynclazycommand--asynclazycommandt) - [PropertyValueConverter\](#propertyvalueconvertertsourcetype-ttargettype) - [ParameterValueConverter\](#parametervalueconverterttargettype) - [IProperty\ & IReadOnlyProperty\](#propertyt--readonlypropertyt) @@ -425,6 +423,18 @@ public class ImageViewerViewModel : IBindingContext } ``` +To allow the same async command to be invoked concurrently multiple times, set the `AllowConcurrency` property of the `AsyncCommand` to `true`. + +```csharp +public class MainViewModel : IBindingContext +{ + public MainViewModel() + { + RunConcurrentlyCommand = new AsyncCommand(RunConcurrentlyAsync) { AllowConcurrency = true }; + } +} +``` + If you want to create an async command that supports cancellation, use the `WithCancellation` extension method. ```csharp @@ -446,22 +456,13 @@ public class MyViewModel : IBindingContext private void Cancel() { - // If the underlying command is not running, or - // if it does not support cancellation, this method will perform no action. + // If the underlying command is not running, this method will perform no action. MyAsyncCommand.Cancel(); } } ``` -If the command supports cancellation, previous invocations will automatically be canceled if a new one is started. - -> **Note:** You need to import the [UniTask](https://github.com/Cysharp/UniTask) package in order to use async commands. - -### AsyncLazyCommand & AsyncLazyCommand\ - -The `AsyncLazyCommand` and `AsyncLazyCommand` are have the same functionality as the `AsyncCommand`'s, except they prevent the same async command from being invoked concurrently multiple times. - -Let's imagine a scenario similar to the one described in the `AsyncCommand` sample, but a user clicks the `Download Image` button several times while the async operation is running. In this case, `AsyncLazyCommand` will ignore all clicks until previous async operation has completed. +If a command supports cancellation and the `AllowConcurrency` property is set to `true`, all running commands will be canceled. > **Note:** You need to import the [UniTask](https://github.com/Cysharp/UniTask) package in order to use async commands. diff --git a/src/UnityMvvmToolkit.Core/Internal/BindingContextMemberProvider.cs b/src/UnityMvvmToolkit.Core/Internal/BindingContextMemberProvider.cs index 6f4fe80..0e26964 100644 --- a/src/UnityMvvmToolkit.Core/Internal/BindingContextMemberProvider.cs +++ b/src/UnityMvvmToolkit.Core/Internal/BindingContextMemberProvider.cs @@ -63,40 +63,49 @@ private static bool TryGetFieldHashCode(Type contextType, FieldInfo fieldInfo, o return TryGetHashCode(contextType, fieldInfo.Name, fieldInfo.FieldType, out hashCode); } - if (TryGetPropertyNameFromAttribute(fieldInfo, out var fieldName)) + if (HasObservableAttribute(fieldInfo, out var propertyName) == false) { - return TryGetHashCode(contextType, fieldName, fieldInfo.FieldType, out hashCode); + hashCode = default; + return false; } - fieldName = fieldInfo.Name; + return string.IsNullOrWhiteSpace(propertyName) + ? TryGetHashCode(contextType, GetFieldName(fieldInfo.Name), fieldInfo.FieldType, out hashCode) + : TryGetHashCode(contextType, propertyName, fieldInfo.FieldType, out hashCode); + } - if (fieldName.Length > 1) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetPropertyHashCode(Type contextType, PropertyInfo propertyInfo, out int hashCode) + { + if (propertyInfo.GetMethod.IsPrivate) { - if (fieldName[0] == '_') - { - fieldName = fieldName[1..]; // TODO: Get rid of allocation. - } - - if (fieldName[0] == 'm' && fieldName[1] == '_') - { - fieldName = fieldName[2..]; // TODO: Get rid of allocation. - } + hashCode = default; + return false; } - if (string.IsNullOrEmpty(fieldName)) + return TryGetHashCode(contextType, propertyInfo.Name, propertyInfo.PropertyType, out hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetHashCode(Type contextType, string memberName, Type memberType, out int hashCode) + { + if (typeof(IBaseCommand).IsAssignableFrom(memberType) || + typeof(IBaseProperty).IsAssignableFrom(memberType)) { - throw new InvalidOperationException($"Field name '{fieldName}' is not supported."); + hashCode = HashCodeHelper.GetMemberHashCode(contextType, memberName); + return true; } - return TryGetHashCode(contextType, fieldName, fieldInfo.FieldType, out hashCode); + hashCode = default; + return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryGetPropertyNameFromAttribute(MemberInfo fieldInfo, out string propertyName) + private static bool HasObservableAttribute(MemberInfo fieldInfo, out string propertyName) { var observableAttribute = fieldInfo.GetCustomAttribute(); - if (observableAttribute == null || string.IsNullOrWhiteSpace(observableAttribute.PropertyName)) + if (observableAttribute == null) { propertyName = default; return false; @@ -107,29 +116,29 @@ private static bool TryGetPropertyNameFromAttribute(MemberInfo fieldInfo, out st } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryGetPropertyHashCode(Type contextType, PropertyInfo propertyInfo, out int hashCode) + private static string GetFieldName(string fieldName) { - if (propertyInfo.GetMethod.IsPrivate) + var resultName = fieldName; + + if (resultName.Length > 1) { - hashCode = default; - return false; - } + if (resultName[0] == '_') + { + resultName = resultName[1..]; // TODO: Get rid of allocation. + } - return TryGetHashCode(contextType, propertyInfo.Name, propertyInfo.PropertyType, out hashCode); - } + if (resultName[0] == 'm' && resultName[1] == '_') + { + resultName = resultName[2..]; // TODO: Get rid of allocation. + } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryGetHashCode(Type contextType, string memberName, Type memberType, out int hashCode) - { - if (typeof(IBaseCommand).IsAssignableFrom(memberType) || - typeof(IBaseProperty).IsAssignableFrom(memberType)) + if (string.IsNullOrEmpty(resultName)) { - hashCode = HashCodeHelper.GetMemberHashCode(contextType, memberName); - return true; + throw new InvalidOperationException($"Field name '{resultName}' is not supported."); } - hashCode = default; - return false; + return resultName; } } } \ No newline at end of file diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/Core/Internal/BindingContextMemberProvider.cs b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/Core/Internal/BindingContextMemberProvider.cs index 6f4fe80..0e26964 100644 --- a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/Core/Internal/BindingContextMemberProvider.cs +++ b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/Core/Internal/BindingContextMemberProvider.cs @@ -63,40 +63,49 @@ private static bool TryGetFieldHashCode(Type contextType, FieldInfo fieldInfo, o return TryGetHashCode(contextType, fieldInfo.Name, fieldInfo.FieldType, out hashCode); } - if (TryGetPropertyNameFromAttribute(fieldInfo, out var fieldName)) + if (HasObservableAttribute(fieldInfo, out var propertyName) == false) { - return TryGetHashCode(contextType, fieldName, fieldInfo.FieldType, out hashCode); + hashCode = default; + return false; } - fieldName = fieldInfo.Name; + return string.IsNullOrWhiteSpace(propertyName) + ? TryGetHashCode(contextType, GetFieldName(fieldInfo.Name), fieldInfo.FieldType, out hashCode) + : TryGetHashCode(contextType, propertyName, fieldInfo.FieldType, out hashCode); + } - if (fieldName.Length > 1) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetPropertyHashCode(Type contextType, PropertyInfo propertyInfo, out int hashCode) + { + if (propertyInfo.GetMethod.IsPrivate) { - if (fieldName[0] == '_') - { - fieldName = fieldName[1..]; // TODO: Get rid of allocation. - } - - if (fieldName[0] == 'm' && fieldName[1] == '_') - { - fieldName = fieldName[2..]; // TODO: Get rid of allocation. - } + hashCode = default; + return false; } - if (string.IsNullOrEmpty(fieldName)) + return TryGetHashCode(contextType, propertyInfo.Name, propertyInfo.PropertyType, out hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetHashCode(Type contextType, string memberName, Type memberType, out int hashCode) + { + if (typeof(IBaseCommand).IsAssignableFrom(memberType) || + typeof(IBaseProperty).IsAssignableFrom(memberType)) { - throw new InvalidOperationException($"Field name '{fieldName}' is not supported."); + hashCode = HashCodeHelper.GetMemberHashCode(contextType, memberName); + return true; } - return TryGetHashCode(contextType, fieldName, fieldInfo.FieldType, out hashCode); + hashCode = default; + return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryGetPropertyNameFromAttribute(MemberInfo fieldInfo, out string propertyName) + private static bool HasObservableAttribute(MemberInfo fieldInfo, out string propertyName) { var observableAttribute = fieldInfo.GetCustomAttribute(); - if (observableAttribute == null || string.IsNullOrWhiteSpace(observableAttribute.PropertyName)) + if (observableAttribute == null) { propertyName = default; return false; @@ -107,29 +116,29 @@ private static bool TryGetPropertyNameFromAttribute(MemberInfo fieldInfo, out st } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryGetPropertyHashCode(Type contextType, PropertyInfo propertyInfo, out int hashCode) + private static string GetFieldName(string fieldName) { - if (propertyInfo.GetMethod.IsPrivate) + var resultName = fieldName; + + if (resultName.Length > 1) { - hashCode = default; - return false; - } + if (resultName[0] == '_') + { + resultName = resultName[1..]; // TODO: Get rid of allocation. + } - return TryGetHashCode(contextType, propertyInfo.Name, propertyInfo.PropertyType, out hashCode); - } + if (resultName[0] == 'm' && resultName[1] == '_') + { + resultName = resultName[2..]; // TODO: Get rid of allocation. + } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryGetHashCode(Type contextType, string memberName, Type memberType, out int hashCode) - { - if (typeof(IBaseCommand).IsAssignableFrom(memberType) || - typeof(IBaseProperty).IsAssignableFrom(memberType)) + if (string.IsNullOrEmpty(resultName)) { - hashCode = HashCodeHelper.GetMemberHashCode(contextType, memberName); - return true; + throw new InvalidOperationException($"Field name '{resultName}' is not supported."); } - hashCode = default; - return false; + return resultName; } } } \ No newline at end of file diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncCommand.T.cs b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncCommand.T.cs index 28b6ba7..f85064e 100644 --- a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncCommand.T.cs +++ b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncCommand.T.cs @@ -18,6 +18,11 @@ public AsyncCommand(Func action, Func canEx public void Execute(T parameter) { + if (IsCommandRunning && AllowConcurrency == false) + { + return; + } + ExecuteAsync(parameter).Forget(); } @@ -25,12 +30,13 @@ public async UniTask ExecuteAsync(T parameter, CancellationToken cancellationTok { try { - IsRunning = true; + SetCommandRunning(true); + await _action(parameter, cancellationToken); } finally { - IsRunning = false; + SetCommandRunning(false); } } } diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncCommand.cs b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncCommand.cs index 41a8816..826c2bf 100644 --- a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncCommand.cs +++ b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncCommand.cs @@ -18,6 +18,11 @@ public AsyncCommand(Func action, Func canExecu public void Execute() { + if (IsCommandRunning && AllowConcurrency == false) + { + return; + } + ExecuteAsync().Forget(); } @@ -25,12 +30,13 @@ public async UniTask ExecuteAsync(CancellationToken cancellationToken = default) { try { - IsRunning = true; + SetCommandRunning(true); + await _action(cancellationToken); } finally { - IsRunning = false; + SetCommandRunning(false); } } } diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncLazyCommand.T.cs b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncLazyCommand.T.cs deleted file mode 100644 index ca9261f..0000000 --- a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncLazyCommand.T.cs +++ /dev/null @@ -1,44 +0,0 @@ -#if UNITYMVVMTOOLKIT_UNITASK_SUPPORT - -namespace UnityMvvmToolkit.UniTask -{ - using System; - using Interfaces; - using System.Threading; - using Cysharp.Threading.Tasks; - - public class AsyncLazyCommand : BaseAsyncLazyCommand, IAsyncCommand - { - private readonly Func _action; - - public AsyncLazyCommand(Func action, Func canExecute = null) - : base(canExecute) - { - _action = action; - } - - public void Execute(T parameter) - { - ExecuteAsync(parameter).Forget(); - } - - public async UniTask ExecuteAsync(T parameter, CancellationToken cancellationToken = default) - { - try - { - if (IsRunning == false) - { - ExecutionTask = _action(parameter, cancellationToken).ToAsyncLazy(); - } - - await ExecutionTask; - } - finally - { - ExecutionTask = null; - } - } - } -} - -#endif \ No newline at end of file diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncLazyCommand.T.cs.meta b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncLazyCommand.T.cs.meta deleted file mode 100644 index 09c8fe5..0000000 --- a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncLazyCommand.T.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: af7b03c148824ca6869d5c5a156916ed -timeCreated: 1681733082 \ No newline at end of file diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncLazyCommand.cs b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncLazyCommand.cs deleted file mode 100644 index 6f512a6..0000000 --- a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncLazyCommand.cs +++ /dev/null @@ -1,44 +0,0 @@ -#if UNITYMVVMTOOLKIT_UNITASK_SUPPORT - -namespace UnityMvvmToolkit.UniTask -{ - using System; - using Interfaces; - using System.Threading; - using Cysharp.Threading.Tasks; - - public class AsyncLazyCommand : BaseAsyncLazyCommand, IAsyncCommand - { - private readonly Func _action; - - public AsyncLazyCommand(Func action, Func canExecute = null) - : base(canExecute) - { - _action = action; - } - - public void Execute() - { - ExecuteAsync().Forget(); - } - - public async UniTask ExecuteAsync(CancellationToken cancellationToken = default) - { - try - { - if (IsRunning == false) - { - ExecutionTask = _action(cancellationToken).ToAsyncLazy(); - } - - await ExecutionTask; - } - finally - { - ExecutionTask = null; - } - } - } -} - -#endif \ No newline at end of file diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncLazyCommand.cs.meta b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncLazyCommand.cs.meta deleted file mode 100644 index cb3fa4d..0000000 --- a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/AsyncLazyCommand.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: d9290b3128874727840f314040754354 -timeCreated: 1659932508 \ No newline at end of file diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/BaseAsyncCommand.cs b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/BaseAsyncCommand.cs index 62c5634..fdc6451 100644 --- a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/BaseAsyncCommand.cs +++ b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/BaseAsyncCommand.cs @@ -5,28 +5,26 @@ namespace UnityMvvmToolkit.UniTask using Core; using System; using Interfaces; + using Extensions; using System.Runtime.CompilerServices; + using UnityMvvmToolkit.Core.Interfaces; public abstract class BaseAsyncCommand : BaseCommand, IBaseAsyncCommand { - private bool _isRunning; + private readonly IProperty _isRunning; protected BaseAsyncCommand(Func canExecute) : base(canExecute) { + _isRunning = new Property(); } - public virtual bool IsRunning - { - get => _isRunning; - protected set - { - _isRunning = value; - RaiseCanExecuteChanged(); - } - } - + public bool AllowConcurrency { get; set; } public virtual bool DisableOnExecution { get; set; } + public IReadOnlyProperty IsRunning => _isRunning; + + protected bool IsCommandRunning { get; private set; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override bool CanExecute() { @@ -35,11 +33,27 @@ public override bool CanExecute() return base.CanExecute(); } - return IsRunning == false && base.CanExecute(); + return IsCommandRunning == false && base.CanExecute(); } public virtual void Cancel() { + throw new InvalidOperationException( + $"To make the 'AsyncCommand' cancelable, use '{nameof(AsyncCommandExtensions.WithCancellation)}' extension."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void SetCommandRunning(bool isRunning) + { + _isRunning.Value = isRunning; + + if (IsCommandRunning == isRunning) + { + return; + } + + IsCommandRunning = isRunning; + RaiseCanExecuteChanged(); } } } diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/BaseAsyncLazyCommand.cs b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/BaseAsyncLazyCommand.cs deleted file mode 100644 index bcf8311..0000000 --- a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/BaseAsyncLazyCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -#if UNITYMVVMTOOLKIT_UNITASK_SUPPORT - -namespace UnityMvvmToolkit.UniTask -{ - using System; - using Cysharp.Threading.Tasks; - - public abstract class BaseAsyncLazyCommand : BaseAsyncCommand - { - private AsyncLazy _executionTask; - - protected BaseAsyncLazyCommand(Func canExecute) : base(canExecute) - { - } - - public override bool IsRunning => ExecutionTask is { Task: { Status: UniTaskStatus.Pending } }; - - protected AsyncLazy ExecutionTask - { - get => _executionTask; - set - { - _executionTask = value; - RaiseCanExecuteChanged(); - } - } - } -} - -#endif \ No newline at end of file diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/BaseAsyncLazyCommand.cs.meta b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/BaseAsyncLazyCommand.cs.meta deleted file mode 100644 index 315d0dd..0000000 --- a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/BaseAsyncLazyCommand.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: da255a6e56104010a980dc827602f7de -timeCreated: 1662099665 \ No newline at end of file diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Extensions/AsyncCommandExtensions.cs b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Extensions/AsyncCommandExtensions.cs index e10ca73..cccdcf4 100644 --- a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Extensions/AsyncCommandExtensions.cs +++ b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Extensions/AsyncCommandExtensions.cs @@ -15,7 +15,9 @@ public static IAsyncCommand WithCancellation(this IAsyncCommand asyncCommand) throw new NullReferenceException(nameof(asyncCommand)); } - return new AsyncCommandWithCancellation(asyncCommand); + return asyncCommand.AllowConcurrency + ? new AsyncCommandWithCancellation(asyncCommand) + : new AsyncLazyCommandWithCancellation(asyncCommand); } public static IAsyncCommand WithCancellation(this IAsyncCommand asyncCommand) @@ -25,7 +27,9 @@ public static IAsyncCommand WithCancellation(this IAsyncCommand asyncCo throw new NullReferenceException(nameof(asyncCommand)); } - return new AsyncCommandWithCancellation(asyncCommand); + return asyncCommand.AllowConcurrency + ? new AsyncCommandWithCancellation(asyncCommand) + : new AsyncLazyCommandWithCancellation(asyncCommand); } } } diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Interfaces/IBaseAsyncCommand.cs b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Interfaces/IBaseAsyncCommand.cs index 7bad6c6..6293dc5 100644 --- a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Interfaces/IBaseAsyncCommand.cs +++ b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Interfaces/IBaseAsyncCommand.cs @@ -2,11 +2,15 @@ namespace UnityMvvmToolkit.UniTask.Interfaces { + using UnityMvvmToolkit.Core.Interfaces; + public interface IBaseAsyncCommand { - bool IsRunning { get; } + bool AllowConcurrency { get; set; } bool DisableOnExecution { get; set; } + IReadOnlyProperty IsRunning { get; } + void Cancel(); } } diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncCommandWithCancellation.T.cs b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncCommandWithCancellation.T.cs index e71d693..ba01185 100644 --- a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncCommandWithCancellation.T.cs +++ b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncCommandWithCancellation.T.cs @@ -6,15 +6,20 @@ namespace UnityMvvmToolkit.UniTask.Internal using Interfaces; using System.Threading; using Cysharp.Threading.Tasks; + using System.Collections.Concurrent; + using System.Runtime.CompilerServices; internal class AsyncCommandWithCancellation : BaseAsyncCommand, IAsyncCommand { private readonly IAsyncCommand _asyncCommand; + private readonly ConcurrentQueue _runningCommands; + private CancellationTokenSource _cancellationTokenSource; public AsyncCommandWithCancellation(IAsyncCommand asyncCommand) : base(null) { _asyncCommand = asyncCommand; + _runningCommands = new ConcurrentQueue(); } public override bool DisableOnExecution @@ -31,20 +36,35 @@ public override event EventHandler CanExecuteChanged public void Execute(T parameter) { - ExecuteAsync(parameter).Forget(); + if (IsCommandRunning) + { + TryEnqueueAsyncCommand(parameter, _cancellationTokenSource.Token); + } + else + { + ExecuteAsync(parameter).Forget(); + } } public async UniTask ExecuteAsync(T parameter, CancellationToken cancellationToken = default) { - _cancellationTokenSource?.Cancel(); - _cancellationTokenSource = new CancellationTokenSource(); + _cancellationTokenSource ??= new CancellationTokenSource(); try { - await _asyncCommand.ExecuteAsync(parameter, _cancellationTokenSource.Token); + SetCommandRunning(true); + + TryEnqueueAsyncCommand(parameter, _cancellationTokenSource.Token); + + while (_runningCommands.TryDequeue(out var asyncCommand)) + { + await asyncCommand.SuppressCancellationThrow(); + } } finally { + SetCommandRunning(false); + _cancellationTokenSource?.Dispose(); _cancellationTokenSource = null; } @@ -54,6 +74,17 @@ public override void Cancel() { _cancellationTokenSource?.Cancel(); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void TryEnqueueAsyncCommand(T parameter, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + _runningCommands.Enqueue(_asyncCommand.ExecuteAsync(parameter, cancellationToken)); + } } } diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncCommandWithCancellation.cs b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncCommandWithCancellation.cs index bf2e81b..f13f01e 100644 --- a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncCommandWithCancellation.cs +++ b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncCommandWithCancellation.cs @@ -6,15 +6,20 @@ namespace UnityMvvmToolkit.UniTask.Internal using Interfaces; using System.Threading; using Cysharp.Threading.Tasks; + using System.Collections.Concurrent; + using System.Runtime.CompilerServices; internal class AsyncCommandWithCancellation : BaseAsyncCommand, IAsyncCommand { private readonly IAsyncCommand _asyncCommand; + private readonly ConcurrentQueue _runningCommands; + private CancellationTokenSource _cancellationTokenSource; public AsyncCommandWithCancellation(IAsyncCommand asyncCommand) : base(null) { _asyncCommand = asyncCommand; + _runningCommands = new ConcurrentQueue(); } public override bool DisableOnExecution @@ -31,20 +36,35 @@ public override event EventHandler CanExecuteChanged public void Execute() { - ExecuteAsync().Forget(); + if (IsCommandRunning) + { + TryEnqueueAsyncCommand(_cancellationTokenSource.Token); + } + else + { + ExecuteAsync().Forget(); + } } public async UniTask ExecuteAsync(CancellationToken cancellationToken = default) { - _cancellationTokenSource?.Cancel(); - _cancellationTokenSource = new CancellationTokenSource(); + _cancellationTokenSource ??= new CancellationTokenSource(); try { - await _asyncCommand.ExecuteAsync(_cancellationTokenSource.Token); + SetCommandRunning(true); + + TryEnqueueAsyncCommand(_cancellationTokenSource.Token); + + while (_runningCommands.TryDequeue(out var asyncCommand)) + { + await asyncCommand.SuppressCancellationThrow(); + } } finally { + SetCommandRunning(false); + _cancellationTokenSource?.Dispose(); _cancellationTokenSource = null; } @@ -54,6 +74,17 @@ public override void Cancel() { _cancellationTokenSource?.Cancel(); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void TryEnqueueAsyncCommand(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + _runningCommands.Enqueue(_asyncCommand.ExecuteAsync(cancellationToken)); + } } } diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncLazyCommandWithCancellation.T.cs b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncLazyCommandWithCancellation.T.cs new file mode 100644 index 0000000..9d4b743 --- /dev/null +++ b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncLazyCommandWithCancellation.T.cs @@ -0,0 +1,68 @@ +#if UNITYMVVMTOOLKIT_UNITASK_SUPPORT + +namespace UnityMvvmToolkit.UniTask.Internal +{ + using System; + using Interfaces; + using System.Threading; + using Cysharp.Threading.Tasks; + + internal class AsyncLazyCommandWithCancellation : BaseAsyncCommand, IAsyncCommand + { + private readonly IAsyncCommand _asyncCommand; + private CancellationTokenSource _cancellationTokenSource; + + public AsyncLazyCommandWithCancellation(IAsyncCommand asyncCommand) : base(null) + { + _asyncCommand = asyncCommand; + } + + public override bool DisableOnExecution + { + get => _asyncCommand.DisableOnExecution; + set => _asyncCommand.DisableOnExecution = value; + } + + public override event EventHandler CanExecuteChanged + { + add => _asyncCommand.CanExecuteChanged += value; + remove => _asyncCommand.CanExecuteChanged -= value; + } + + public void Execute(T parameter) + { + if (IsCommandRunning) + { + return; + } + + ExecuteAsync(parameter).Forget(); + } + + public async UniTask ExecuteAsync(T parameter, CancellationToken cancellationToken = default) + { + _cancellationTokenSource = new CancellationTokenSource(); + + try + { + SetCommandRunning(true); + + await _asyncCommand.ExecuteAsync(parameter, _cancellationTokenSource.Token); + } + finally + { + SetCommandRunning(false); + + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + } + } + + public override void Cancel() + { + _cancellationTokenSource?.Cancel(); + } + } +} + +#endif \ No newline at end of file diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncLazyCommandWithCancellation.T.cs.meta b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncLazyCommandWithCancellation.T.cs.meta new file mode 100644 index 0000000..07ef38d --- /dev/null +++ b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncLazyCommandWithCancellation.T.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3388c68d92484ea1b48bd5ab0294b3b5 +timeCreated: 1683167817 \ No newline at end of file diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncLazyCommandWithCancellation.cs b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncLazyCommandWithCancellation.cs new file mode 100644 index 0000000..53eebf6 --- /dev/null +++ b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncLazyCommandWithCancellation.cs @@ -0,0 +1,68 @@ +#if UNITYMVVMTOOLKIT_UNITASK_SUPPORT + +namespace UnityMvvmToolkit.UniTask.Internal +{ + using System; + using Interfaces; + using System.Threading; + using Cysharp.Threading.Tasks; + + internal class AsyncLazyCommandWithCancellation : BaseAsyncCommand, IAsyncCommand + { + private readonly IAsyncCommand _asyncCommand; + private CancellationTokenSource _cancellationTokenSource; + + public AsyncLazyCommandWithCancellation(IAsyncCommand asyncCommand) : base(null) + { + _asyncCommand = asyncCommand; + } + + public override bool DisableOnExecution + { + get => _asyncCommand.DisableOnExecution; + set => _asyncCommand.DisableOnExecution = value; + } + + public override event EventHandler CanExecuteChanged + { + add => _asyncCommand.CanExecuteChanged += value; + remove => _asyncCommand.CanExecuteChanged -= value; + } + + public void Execute() + { + if (IsCommandRunning) + { + return; + } + + ExecuteAsync().Forget(); + } + + public async UniTask ExecuteAsync(CancellationToken cancellationToken = default) + { + _cancellationTokenSource = new CancellationTokenSource(); + + try + { + SetCommandRunning(true); + + await _asyncCommand.ExecuteAsync(_cancellationTokenSource.Token); + } + finally + { + SetCommandRunning(false); + + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + } + } + + public override void Cancel() + { + _cancellationTokenSource?.Cancel(); + } + } +} + +#endif \ No newline at end of file diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncLazyCommandWithCancellation.cs.meta b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncLazyCommandWithCancellation.cs.meta new file mode 100644 index 0000000..a10b1a9 --- /dev/null +++ b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/External/UniTask/Internal/AsyncLazyCommandWithCancellation.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 63ba67040be546ef9ce6851cb120f643 +timeCreated: 1683167687 \ No newline at end of file diff --git a/tests/UnityMvvmToolkit.Test.Integration/TestBindingContext/MyBindingContext.cs b/tests/UnityMvvmToolkit.Test.Integration/TestBindingContext/MyBindingContext.cs index 1be98fc..854fe46 100644 --- a/tests/UnityMvvmToolkit.Test.Integration/TestBindingContext/MyBindingContext.cs +++ b/tests/UnityMvvmToolkit.Test.Integration/TestBindingContext/MyBindingContext.cs @@ -10,6 +10,7 @@ namespace UnityMvvmToolkit.Test.Integration.TestBindingContext; public class MyBindingContext : IBindingContext { + [Observable] private readonly IProperty _count = new Property(); [Observable(nameof(IntReadOnlyValue))] diff --git a/tests/UnityMvvmToolkit.Test.Unit/BindingContextMemberProviderTests.cs b/tests/UnityMvvmToolkit.Test.Unit/BindingContextMemberProviderTests.cs index c5c4457..09a8f08 100644 --- a/tests/UnityMvvmToolkit.Test.Unit/BindingContextMemberProviderTests.cs +++ b/tests/UnityMvvmToolkit.Test.Unit/BindingContextMemberProviderTests.cs @@ -111,7 +111,7 @@ public void GetBindingContextMembers_ShouldThrow_WhenTypeIsNotAssignableFromIBin public void GetBindingContextMembers_ShouldThrow_WhenResultsDictionaryIsNull() { // Arrange - var bindingContextType = typeof(PrivateFieldBindingContext); + var bindingContextType = typeof(ObservableFieldBindingContext); // Assert _memberProvider @@ -126,6 +126,7 @@ private static IEnumerable BindingContextDataSets() yield return GetObservableFieldBindingContextTestData(); yield return GetPublicFieldBindingContextTestData(); yield return GetPrivatePropertyBindingContextTestData(); + yield return GetSameFieldAndPropertyNamesBindingContextTestData(); yield return GetPublicPropertyBindingContextTestData(); yield return GetCommandBindingContextTestData(); yield return GetNoObservableFieldsBindingContextTestData(); @@ -134,17 +135,7 @@ private static IEnumerable BindingContextDataSets() private static object[] GetPrivateFieldBindingContextTestData() { var bindingContextType = typeof(PrivateFieldBindingContext); - return new object[] - { - bindingContextType, - new (int, MemberTypes)[] - { - (HashCodeHelper.GetMemberHashCode(bindingContextType, "BoolField"), MemberTypes.Field), - (HashCodeHelper.GetMemberHashCode(bindingContextType, "IntField"), MemberTypes.Field), - (HashCodeHelper.GetMemberHashCode(bindingContextType, "FloatField"), MemberTypes.Field), - (HashCodeHelper.GetMemberHashCode(bindingContextType, "StrField"), MemberTypes.Field) - } - }; + return new object[] { bindingContextType, Array.Empty<(int, MemberTypes)>() }; } private static object[] GetObservableFieldBindingContextTestData() @@ -186,6 +177,20 @@ private static object[] GetPrivatePropertyBindingContextTestData() return new object[] { typeof(PrivatePropertyBindingContext), Array.Empty<(int, MemberTypes)>() }; } + private static object[] GetSameFieldAndPropertyNamesBindingContextTestData() + { + var bindingContextType = typeof(SameFieldAndPropertyNamesBindingContext); + return new object[] + { + bindingContextType, + new (int, MemberTypes)[] + { + (HashCodeHelper.GetMemberHashCode(bindingContextType, nameof(SameFieldAndPropertyNamesBindingContext.Count)), + MemberTypes.Property) + } + }; + } + private static object[] GetPublicPropertyBindingContextTestData() { var bindingContextType = typeof(PublicPropertyBindingContext); diff --git a/tests/UnityMvvmToolkit.Test.Unit/TestBindingContext/InvalidFieldNameBindingContext.cs b/tests/UnityMvvmToolkit.Test.Unit/TestBindingContext/InvalidFieldNameBindingContext.cs index 375d327..243ed18 100644 --- a/tests/UnityMvvmToolkit.Test.Unit/TestBindingContext/InvalidFieldNameBindingContext.cs +++ b/tests/UnityMvvmToolkit.Test.Unit/TestBindingContext/InvalidFieldNameBindingContext.cs @@ -1,4 +1,5 @@ using UnityMvvmToolkit.Core; +using UnityMvvmToolkit.Core.Attributes; using UnityMvvmToolkit.Core.Interfaces; // ReSharper disable UnusedMember.Local @@ -8,6 +9,9 @@ namespace UnityMvvmToolkit.Test.Unit.TestBindingContext; public class InvalidFieldNameBindingContext : IBindingContext { + [Observable] private IProperty _ = new Property(); + + [Observable] private IProperty m_ = new Property(); } \ No newline at end of file diff --git a/tests/UnityMvvmToolkit.Test.Unit/TestBindingContext/SameFieldAndPropertyNamesBindingContext.cs b/tests/UnityMvvmToolkit.Test.Unit/TestBindingContext/SameFieldAndPropertyNamesBindingContext.cs new file mode 100644 index 0000000..ae82a45 --- /dev/null +++ b/tests/UnityMvvmToolkit.Test.Unit/TestBindingContext/SameFieldAndPropertyNamesBindingContext.cs @@ -0,0 +1,11 @@ +using UnityMvvmToolkit.Core; +using UnityMvvmToolkit.Core.Interfaces; + +namespace UnityMvvmToolkit.Test.Unit.TestBindingContext; + +public class SameFieldAndPropertyNamesBindingContext : IBindingContext +{ + private readonly IProperty _count = new Property(); + + public IReadOnlyProperty Count => _count; +} \ No newline at end of file