using System; using System.Collections.Generic; using System.Threading; #if CSHARP_7_OR_LATER || (UNITY_2018_3_OR_NEWER && (NET_STANDARD_2_0 || NET_4_6)) using System.Threading.Tasks; using UniRx.InternalUtil; #endif namespace UniRx { public interface IReactiveCommand : IObservable { IReadOnlyReactiveProperty CanExecute { get; } bool Execute(T parameter); } public interface IAsyncReactiveCommand { IReadOnlyReactiveProperty CanExecute { get; } IDisposable Execute(T parameter); IDisposable Subscribe(Func> asyncAction); } /// /// Represents ReactiveCommand<Unit> /// public class ReactiveCommand : ReactiveCommand { /// /// CanExecute is always true. /// public ReactiveCommand() : base() { } /// /// CanExecute is changed from canExecute sequence. /// public ReactiveCommand(IObservable canExecuteSource, bool initialValue = true) : base(canExecuteSource, initialValue) { } /// Push null to subscribers. public bool Execute() { return Execute(Unit.Default); } /// Force push parameter to subscribers. public void ForceExecute() { ForceExecute(Unit.Default); } } public class ReactiveCommand : IReactiveCommand, IDisposable { readonly Subject trigger = new Subject(); readonly IDisposable canExecuteSubscription; ReactiveProperty canExecute; public IReadOnlyReactiveProperty CanExecute { get { return canExecute; } } public bool IsDisposed { get; private set; } /// /// CanExecute is always true. /// public ReactiveCommand() { this.canExecute = new ReactiveProperty(true); this.canExecuteSubscription = Disposable.Empty; } /// /// CanExecute is changed from canExecute sequence. /// public ReactiveCommand(IObservable canExecuteSource, bool initialValue = true) { this.canExecute = new ReactiveProperty(initialValue); this.canExecuteSubscription = canExecuteSource .DistinctUntilChanged() .SubscribeWithState(canExecute, (b, c) => c.Value = b); } /// Push parameter to subscribers when CanExecute. public bool Execute(T parameter) { if (canExecute.Value) { trigger.OnNext(parameter); return true; } else { return false; } } /// Force push parameter to subscribers. public void ForceExecute(T parameter) { trigger.OnNext(parameter); } /// Subscribe execute. public IDisposable Subscribe(IObserver observer) { return trigger.Subscribe(observer); } /// /// Stop all subscription and lock CanExecute is false. /// public void Dispose() { if (IsDisposed) return; IsDisposed = true; canExecute.Dispose(); trigger.OnCompleted(); trigger.Dispose(); canExecuteSubscription.Dispose(); } } /// /// Variation of ReactiveCommand, when executing command then CanExecute = false after CanExecute = true. /// public class AsyncReactiveCommand : AsyncReactiveCommand { /// /// CanExecute is automatically changed when executing to false and finished to true. /// public AsyncReactiveCommand() : base() { } /// /// CanExecute is automatically changed when executing to false and finished to true. /// public AsyncReactiveCommand(IObservable canExecuteSource) : base(canExecuteSource) { } /// /// CanExecute is automatically changed when executing to false and finished to true. /// The source is shared between other AsyncReactiveCommand. /// public AsyncReactiveCommand(IReactiveProperty sharedCanExecute) : base(sharedCanExecute) { } public IDisposable Execute() { return base.Execute(Unit.Default); } } /// /// Variation of ReactiveCommand, canExecute is changed when executing command then CanExecute = false after CanExecute = true. /// public class AsyncReactiveCommand : IAsyncReactiveCommand { UniRx.InternalUtil.ImmutableList>> asyncActions = UniRx.InternalUtil.ImmutableList>>.Empty; readonly object gate = new object(); readonly IReactiveProperty canExecuteSource; readonly IReadOnlyReactiveProperty canExecute; public IReadOnlyReactiveProperty CanExecute { get { return canExecute; } } public bool IsDisposed { get; private set; } /// /// CanExecute is automatically changed when executing to false and finished to true. /// public AsyncReactiveCommand() { this.canExecuteSource = new ReactiveProperty(true); this.canExecute = canExecuteSource; } /// /// CanExecute is automatically changed when executing to false and finished to true. /// public AsyncReactiveCommand(IObservable canExecuteSource) { this.canExecuteSource = new ReactiveProperty(true); this.canExecute = this.canExecuteSource.CombineLatest(canExecuteSource, (x, y) => x && y).ToReactiveProperty(); } /// /// CanExecute is automatically changed when executing to false and finished to true. /// The source is shared between other AsyncReactiveCommand. /// public AsyncReactiveCommand(IReactiveProperty sharedCanExecute) { this.canExecuteSource = sharedCanExecute; this.canExecute = sharedCanExecute; } /// Push parameter to subscribers when CanExecute. public IDisposable Execute(T parameter) { if (canExecute.Value) { canExecuteSource.Value = false; var a = asyncActions.Data; if (a.Length == 1) { try { var asyncState = a[0].Invoke(parameter) ?? Observable.ReturnUnit(); return asyncState.Finally(() => canExecuteSource.Value = true).Subscribe(); } catch { canExecuteSource.Value = true; throw; } } else { var xs = new IObservable[a.Length]; try { for (int i = 0; i < a.Length; i++) { xs[i] = a[i].Invoke(parameter) ?? Observable.ReturnUnit(); } } catch { canExecuteSource.Value = true; throw; } return Observable.WhenAll(xs).Finally(() => canExecuteSource.Value = true).Subscribe(); } } else { return Disposable.Empty; } } /// Subscribe execute. public IDisposable Subscribe(Func> asyncAction) { lock (gate) { asyncActions = asyncActions.Add(asyncAction); } return new Subscription(this, asyncAction); } /// /// Stop all subscription and lock CanExecute is false. /// public void Dispose() { if (IsDisposed) return; IsDisposed = true; asyncActions = UniRx.InternalUtil.ImmutableList>>.Empty; } class Subscription : IDisposable { readonly AsyncReactiveCommand parent; readonly Func> asyncAction; public Subscription(AsyncReactiveCommand parent, Func> asyncAction) { this.parent = parent; this.asyncAction = asyncAction; } public void Dispose() { lock (parent.gate) { parent.asyncActions = parent.asyncActions.Remove(asyncAction); } } } } public static class ReactiveCommandExtensions { /// /// Create non parameter commands. CanExecute is changed from canExecute sequence. /// public static ReactiveCommand ToReactiveCommand(this IObservable canExecuteSource, bool initialValue = true) { return new ReactiveCommand(canExecuteSource, initialValue); } /// /// Create parametered comamnds. CanExecute is changed from canExecute sequence. /// public static ReactiveCommand ToReactiveCommand(this IObservable canExecuteSource, bool initialValue = true) { return new ReactiveCommand(canExecuteSource, initialValue); } #if CSHARP_7_OR_LATER || (UNITY_2018_3_OR_NEWER && (NET_STANDARD_2_0 || NET_4_6)) static readonly Action Callback = CancelCallback; static void CancelCallback(object state) { var tuple = (Tuple)state; tuple.Item2.Dispose(); tuple.Item1.TrySetCanceled(); } public static Task WaitUntilExecuteAsync(this IReactiveCommand source, CancellationToken cancellationToken = default(CancellationToken)) { var tcs = new CancellableTaskCompletionSource(); var disposable = new SingleAssignmentDisposable(); disposable.Disposable = source.Subscribe(x => { disposable.Dispose(); // finish subscription. tcs.TrySetResult(x); }, ex => tcs.TrySetException(ex), () => tcs.TrySetCanceled()); cancellationToken.Register(Callback, Tuple.Create(tcs, disposable.Disposable), false); return tcs.Task; } public static System.Runtime.CompilerServices.TaskAwaiter GetAwaiter(this IReactiveCommand command) { return command.WaitUntilExecuteAsync(CancellationToken.None).GetAwaiter(); } #endif #if !UniRxLibrary // for uGUI(from 4.6) #if !(UNITY_4_0 || UNITY_4_1 || UNITY_4_2 || UNITY_4_3 || UNITY_4_4 || UNITY_4_5) /// /// Bind ReactiveCommand to button's interactable and onClick. /// public static IDisposable BindTo(this IReactiveCommand command, UnityEngine.UI.Button button) { var d1 = command.CanExecute.SubscribeToInteractable(button); var d2 = button.OnClickAsObservable().SubscribeWithState(command, (x, c) => c.Execute(x)); return StableCompositeDisposable.Create(d1, d2); } /// /// Bind ReactiveCommand to button's interactable and onClick and register onClick action to command. /// public static IDisposable BindToOnClick(this IReactiveCommand command, UnityEngine.UI.Button button, Action onClick) { var d1 = command.CanExecute.SubscribeToInteractable(button); var d2 = button.OnClickAsObservable().SubscribeWithState(command, (x, c) => c.Execute(x)); var d3 = command.Subscribe(onClick); return StableCompositeDisposable.Create(d1, d2, d3); } /// /// Bind canExecuteSource to button's interactable and onClick and register onClick action to command. /// public static IDisposable BindToButtonOnClick(this IObservable canExecuteSource, UnityEngine.UI.Button button, Action onClick, bool initialValue = true) { return ToReactiveCommand(canExecuteSource, initialValue).BindToOnClick(button, onClick); } #endif #endif } public static class AsyncReactiveCommandExtensions { public static AsyncReactiveCommand ToAsyncReactiveCommand(this IReactiveProperty sharedCanExecuteSource) { return new AsyncReactiveCommand(sharedCanExecuteSource); } public static AsyncReactiveCommand ToAsyncReactiveCommand(this IReactiveProperty sharedCanExecuteSource) { return new AsyncReactiveCommand(sharedCanExecuteSource); } #if CSHARP_7_OR_LATER || (UNITY_2018_3_OR_NEWER && (NET_STANDARD_2_0 || NET_4_6)) static readonly Action Callback = CancelCallback; static void CancelCallback(object state) { var tuple = (Tuple)state; tuple.Item2.Dispose(); tuple.Item1.TrySetCanceled(); } public static Task WaitUntilExecuteAsync(this IAsyncReactiveCommand source, CancellationToken cancellationToken = default(CancellationToken)) { var tcs = new CancellableTaskCompletionSource(); var subscription = source.Subscribe(x => { tcs.TrySetResult(x); return Observable.ReturnUnit(); }); cancellationToken.Register(Callback, Tuple.Create(tcs, subscription), false); return tcs.Task; } public static System.Runtime.CompilerServices.TaskAwaiter GetAwaiter(this IAsyncReactiveCommand command) { return command.WaitUntilExecuteAsync(CancellationToken.None).GetAwaiter(); } #endif #if !UniRxLibrary // for uGUI(from 4.6) #if !(UNITY_4_0 || UNITY_4_1 || UNITY_4_2 || UNITY_4_3 || UNITY_4_4 || UNITY_4_5) /// /// Bind AsyncRaectiveCommand to button's interactable and onClick. /// public static IDisposable BindTo(this IAsyncReactiveCommand command, UnityEngine.UI.Button button) { var d1 = command.CanExecute.SubscribeToInteractable(button); var d2 = button.OnClickAsObservable().SubscribeWithState(command, (x, c) => c.Execute(x)); return StableCompositeDisposable.Create(d1, d2); } /// /// Bind AsyncRaectiveCommand to button's interactable and onClick and register async action to command. /// public static IDisposable BindToOnClick(this IAsyncReactiveCommand command, UnityEngine.UI.Button button, Func> asyncOnClick) { var d1 = command.CanExecute.SubscribeToInteractable(button); var d2 = button.OnClickAsObservable().SubscribeWithState(command, (x, c) => c.Execute(x)); var d3 = command.Subscribe(asyncOnClick); return StableCompositeDisposable.Create(d1, d2, d3); } /// /// Create AsyncReactiveCommand and bind to button's interactable and onClick and register async action to command. /// public static IDisposable BindToOnClick(this UnityEngine.UI.Button button, Func> asyncOnClick) { return new AsyncReactiveCommand().BindToOnClick(button, asyncOnClick); } /// /// Create AsyncReactiveCommand and bind sharedCanExecuteSource source to button's interactable and onClick and register async action to command. /// public static IDisposable BindToOnClick(this UnityEngine.UI.Button button, IReactiveProperty sharedCanExecuteSource, Func> asyncOnClick) { return sharedCanExecuteSource.ToAsyncReactiveCommand().BindToOnClick(button, asyncOnClick); } #endif #endif } }