From 96b59b9426b9d69b3189a8df0495056391349686 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Sun, 8 Jun 2014 16:39:58 -0700 Subject: [PATCH 1/7] Add a Command method more familiar to the TPL proletariat --- ReactiveUI/ReactiveCommand.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ReactiveUI/ReactiveCommand.cs b/ReactiveUI/ReactiveCommand.cs index 03d29120ef..29f447d18e 100644 --- a/ReactiveUI/ReactiveCommand.cs +++ b/ReactiveUI/ReactiveCommand.cs @@ -174,6 +174,11 @@ public IObservable ExecuteAsync(object parameter = null) return ret.Publish().RefCount(); } + public Task ExecuteAsyncTask(object parameter = null, CancellationToken ct = default(CancellationToken)) + { + return ExecuteAsync(parameter).ToTask(ct); + } + /// /// Fires whenever an exception would normally terminate ReactiveUI /// internal state. From 17b642e9991da04c6e8349728d705393ccc8a8b2 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Sun, 8 Jun 2014 16:40:24 -0700 Subject: [PATCH 2/7] Add Cancellation-friendly versions of CreateAsyncTask --- ReactiveUI/ReactiveCommand.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ReactiveUI/ReactiveCommand.cs b/ReactiveUI/ReactiveCommand.cs index 29f447d18e..cac46d8219 100644 --- a/ReactiveUI/ReactiveCommand.cs +++ b/ReactiveUI/ReactiveCommand.cs @@ -75,6 +75,26 @@ public static ReactiveCommand CreateAsyncTask(IObservable canExecute return new ReactiveCommand(canExecute, x => executeAsync(x).ToObservable(), scheduler); } + public static ReactiveCommand CreateAsyncTask(IObservable canExecute, Func> executeAsync, IScheduler scheduler = null) + { + return new ReactiveCommand(canExecute, x => Observable.StartAsync(ct => executeAsync(x, ct)), scheduler); + } + + public static ReactiveCommand CreateAsyncTask(Func> executeAsync, IScheduler scheduler = null) + { + return new ReactiveCommand(Observable.Return(true), x => Observable.StartAsync(ct => executeAsync(x,ct)), scheduler); + } + + public static ReactiveCommand CreateAsyncTask(Func executeAsync, IScheduler scheduler = null) + { + return new ReactiveCommand(Observable.Return(true), x => Observable.StartAsync(ct => executeAsync(x,ct)), scheduler); + } + + public static ReactiveCommand CreateAsyncTask(IObservable canExecute, Func executeAsync, IScheduler scheduler = null) + { + return new ReactiveCommand(canExecute, x => Observable.StartAsync(ct => executeAsync(x,ct)), scheduler); + } + /// /// This creates a ReactiveCommand that calls several child From 19c64700474a747e123d22198dd076cef8e776b3 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Sun, 8 Jun 2014 16:44:42 -0700 Subject: [PATCH 3/7] Hide poorly designed ICommand methods --- ReactiveUI/ReactiveCommand.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ReactiveUI/ReactiveCommand.cs b/ReactiveUI/ReactiveCommand.cs index cac46d8219..b60eefff86 100644 --- a/ReactiveUI/ReactiveCommand.cs +++ b/ReactiveUI/ReactiveCommand.cs @@ -233,7 +233,7 @@ public IDisposable Subscribe(IObserver observer) return executeResults.Subscribe(observer); } - public bool CanExecute(object parameter) + bool ICommand.CanExecute(object parameter) { if (canExecuteDisp == null) canExecuteDisp = canExecute.Connect(); return canExecuteLatest; @@ -248,7 +248,7 @@ public event EventHandler CanExecuteChanged remove { CanExecuteChangedEventManager.RemoveHandler(this, value); } } - public void Execute(object parameter) + void ICommand.Execute(object parameter) { ExecuteAsync(parameter).Subscribe(); } From d5f2e4e4cc16b8139b4c8fb24b30e5f3cf07a92d Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Sun, 8 Jun 2014 16:52:44 -0700 Subject: [PATCH 4/7] Just for you, @onovotny <3 --- ReactiveUI/ReactiveCommand.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ReactiveUI/ReactiveCommand.cs b/ReactiveUI/ReactiveCommand.cs index b60eefff86..ba92904b6a 100644 --- a/ReactiveUI/ReactiveCommand.cs +++ b/ReactiveUI/ReactiveCommand.cs @@ -314,5 +314,18 @@ public static IDisposable InvokeCommand(this IObservable This, TT x.cmd.Execute(x.val); }); } + + /// + /// A convenience method for subscribing and creating ReactiveCommands + /// in the same call. Equivalent to Subscribing to the command. + /// + public static IDisposable OnExecuteCompleted(this ReactiveCommand This, Action onNext, Action onError = null) + { + if (onError != null) { + return This.Subscribe(onNext, onError); + } else { + return This.Subscribe(onNext); + } + } } } \ No newline at end of file From 922d77042e074adfc97895c81c997f8f9a418ccd Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Sun, 8 Jun 2014 17:05:01 -0700 Subject: [PATCH 5/7] Someday I will be good at Fluency --- ReactiveUI/ReactiveCommand.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ReactiveUI/ReactiveCommand.cs b/ReactiveUI/ReactiveCommand.cs index ba92904b6a..570af1ea39 100644 --- a/ReactiveUI/ReactiveCommand.cs +++ b/ReactiveUI/ReactiveCommand.cs @@ -317,14 +317,17 @@ public static IDisposable InvokeCommand(this IObservable This, TT /// /// A convenience method for subscribing and creating ReactiveCommands - /// in the same call. Equivalent to Subscribing to the command. + /// in the same call. Equivalent to Subscribing to the command, except + /// there's no way to release your Subscription but that's probably fine. /// - public static IDisposable OnExecuteCompleted(this ReactiveCommand This, Action onNext, Action onError = null) + public static ReactiveCommand OnExecuteCompleted(this ReactiveCommand This, Action onNext, Action onError = null) { if (onError != null) { - return This.Subscribe(onNext, onError); + This.Subscribe(onNext, onError); + return This; } else { - return This.Subscribe(onNext); + This.Subscribe(onNext); + return This; } } } From 4d63d251475e0909f73ed40878cae04b505e033d Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Sun, 8 Jun 2014 17:19:19 -0700 Subject: [PATCH 6/7] Add in RxUI 5.x ReactiveCommand for compatibility purposes --- ReactiveUI/Legacy/ReactiveCommand.cs | 341 +++++++++++++++++++++++++++ ReactiveUI/ReactiveUI.csproj | 4 + ReactiveUI/ReactiveUI_Android.csproj | 1 + ReactiveUI/ReactiveUI_Mac.csproj | 1 + ReactiveUI/ReactiveUI_Net45.csproj | 1 + ReactiveUI/ReactiveUI_WP8.csproj | 1 + ReactiveUI/ReactiveUI_WinRT.csproj | 3 +- ReactiveUI/ReactiveUI_WinRT80.csproj | 3 +- ReactiveUI/ReactiveUI_iOS.csproj | 1 + 9 files changed, 354 insertions(+), 2 deletions(-) create mode 100644 ReactiveUI/Legacy/ReactiveCommand.cs diff --git a/ReactiveUI/Legacy/ReactiveCommand.cs b/ReactiveUI/Legacy/ReactiveCommand.cs new file mode 100644 index 0000000000..169e108967 --- /dev/null +++ b/ReactiveUI/Legacy/ReactiveCommand.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Reactive; +using System.Reactive.Concurrency; +using System.Reactive.Threading.Tasks; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; +using System.Linq.Expressions; +using ReactiveUI; + +using LegacyRxCmd = ReactiveUI.Legacy.ReactiveCommand; + +namespace ReactiveUI.Legacy +{ + /// + /// ReactiveCommand is the default Command implementation in ReactiveUI, which + /// conforms to the spec described in IReactiveCommand. + /// + public class ReactiveCommand : IReactiveCommand, IObservable + { + IDisposable innerDisp; + + readonly Subject inflight = new Subject(); + readonly ScheduledSubject exceptions; + readonly Subject executed = new Subject(); + readonly IScheduler defaultScheduler; + + public ReactiveCommand() : this(null, false, null) { } + public ReactiveCommand(IObservable canExecute, bool initialCondition = true) : this(canExecute, false, null, initialCondition) { } + + public ReactiveCommand(IObservable canExecute, bool allowsConcurrentExecution, IScheduler scheduler, bool initialCondition = true) + { + canExecute = canExecute ?? Observable.Return(true); + defaultScheduler = scheduler ?? RxApp.MainThreadScheduler; + AllowsConcurrentExecution = allowsConcurrentExecution; + + canExecute = canExecute.Catch(ex => { + exceptions.OnNext(ex); + return Observable.Empty(); + }); + + ThrownExceptions = exceptions = new ScheduledSubject(defaultScheduler, RxApp.DefaultExceptionHandler); + + var isExecuting = inflight + .Scan(0, (acc, x) => acc + (x ? 1 : -1)) + .Select(x => x > 0) + .Publish(false) + .PermaRef() + .DistinctUntilChanged(); + + IsExecuting = isExecuting.ObserveOn(defaultScheduler); + + var isBusy = allowsConcurrentExecution ? Observable.Return(false) : isExecuting; + var canExecuteAndNotBusy = Observable.CombineLatest(canExecute, isBusy, (ce, b) => ce && !b); + + var canExecuteObs = canExecuteAndNotBusy + .Publish(initialCondition) + .RefCount(); + + CanExecuteObservable = canExecuteObs + .DistinctUntilChanged() + .ObserveOn(defaultScheduler); + + innerDisp = canExecuteObs.Subscribe(x => { + if (canExecuteLatest == x) return; + + canExecuteLatest = x; + defaultScheduler.Schedule(() => this.raiseCanExecuteChanged(EventArgs.Empty)); + }, exceptions.OnNext); + } + + /// + /// This creates a ReactiveCommand that calls several child + /// ReactiveCommands when invoked. Its CanExecute will match the + /// combined result of the child CanExecutes (i.e. if any child + /// commands cannot execute, neither can the parent) + /// + /// An Observable that determines whether the + /// parent command can execute + /// The commands to combine. + public static LegacyRxCmd CreateCombined(IObservable canExecute, params ReactiveCommand[] commands) + { + var childrenCanExecute = commands + .Select(x => x.CanExecuteObservable) + .CombineLatest(latestCanExecute => latestCanExecute.All(x => x != false)); + + var canExecuteSum = Observable.CombineLatest( + canExecute.StartWith(true), + childrenCanExecute, + (parent, child) => parent && child); + + var ret = new LegacyRxCmd(canExecuteSum); + ret.Subscribe(x => { + foreach (var cmd in commands) cmd.Execute(x); + }); + + return ret; + } + + public static ReactiveCommand CreateCombined(params ReactiveCommand[] commands) + { + return CreateCombined(Observable.Return(true), commands); + } + + /// + /// Registers an asynchronous method to be called whenever the command + /// is Executed. This method returns an IObservable representing the + /// asynchronous operation, and is allowed to OnError / should OnComplete. + /// + /// A filtered version of the Observable which is marshaled + /// to the UI thread. This Observable should only report successes and + /// instead send OnError messages to the ThrownExceptions property. + /// The asynchronous method to call. + /// The 1st type parameter. + public IObservable RegisterAsync(Func> asyncBlock) + { + var ret = executed.Select(x => { + return asyncBlock(x) + .Catch(ex => { + exceptions.OnNext(ex); + return Observable.Empty(); + }) + .Finally(() => { lock (inflight) { inflight.OnNext(false); } }); + }); + + return ret + .Do(_ => { lock (inflight) { inflight.OnNext(true); } }) + .Merge() + .ObserveOn(defaultScheduler) + .Publish().RefCount(); + } + + /// + /// Gets a value indicating whether this instance is executing. This + /// Observable is guaranteed to always return a value immediately (i.e. + /// it is backed by a BehaviorSubject), meaning it is safe to determine + /// the current state of the command via IsExecuting.First() + /// + /// true + /// false + public IObservable IsExecuting { get; protected set; } + + public bool AllowsConcurrentExecution { get; protected set; } + + /// + /// Fires whenever an exception would normally terminate ReactiveUI + /// internal state. + /// + /// The thrown exceptions. + public IObservable ThrownExceptions { get; protected set; } + + public IDisposable Subscribe(IObserver observer) + { + return executed.Subscribe( + Observer.Create( + x => marshalFailures(observer.OnNext, x), + ex => marshalFailures(observer.OnError, ex), + () => marshalFailures(observer.OnCompleted))); + } + + bool canExecuteLatest; + public bool CanExecute(object parameter) + { + return canExecuteLatest; + } + + public event EventHandler CanExecuteChanged; + + public void Execute(object parameter) + { + lock(inflight) { inflight.OnNext(true); } + executed.OnNext(parameter); + lock(inflight) { inflight.OnNext(false); } + } + + public IObservable CanExecuteObservable { get; protected set; } + + public void Dispose() + { + var disp = Interlocked.Exchange(ref innerDisp, null); + if (disp != null) disp.Dispose(); + } + + void marshalFailures(Action block, T param) + { + try { + block(param); + } catch (Exception ex) { + exceptions.OnNext(ex); + } + } + + void marshalFailures(Action block) + { + marshalFailures(_ => block(), Unit.Default); + } + + protected virtual void raiseCanExecuteChanged(EventArgs e) + { + var handler = this.CanExecuteChanged; + + if (handler != null) { + handler(this, e); + } + } + } + + public static class ReactiveCommandMixins + { + /// + /// ToCommand is a convenience method for returning a new + /// ReactiveCommand based on an existing Observable chain. + /// + /// The scheduler to publish events on - default + /// is RxApp.MainThreadScheduler. + /// A new ReactiveCommand whose CanExecute Observable is the + /// current object. + public static ReactiveCommand ToCommand(this IObservable This, bool allowsConcurrentExecution = false, IScheduler scheduler = null) + { + return new ReactiveCommand(This, allowsConcurrentExecution, scheduler); + } + + /// + /// A utility method that will pipe an Observable to an ICommand (i.e. + /// it will first call its CanExecute with the provided value, then if + /// the command can be executed, Execute() will be called) + /// + /// The command to be executed. + /// An object that when disposes, disconnects the Observable + /// from the command. + public static IDisposable InvokeCommand(this IObservable This, ICommand command) + { + return This.ObserveOn(RxApp.MainThreadScheduler).Subscribe(x => { + if (!command.CanExecute(x)) { + return; + } + command.Execute(x); + }); + } + + /// + /// A utility method that will pipe an Observable to an ICommand (i.e. + /// it will first call its CanExecute with the provided value, then if + /// the command can be executed, Execute() will be called) + /// + /// The root object which has the Command. + /// The expression to reference the Command. + /// An object that when disposes, disconnects the Observable + /// from the command. + public static IDisposable InvokeCommand(this IObservable This, TTarget target, Expression> commandProperty) + { + return This.CombineLatest(target.WhenAnyValue(commandProperty), (val, cmd) => new { val, cmd }) + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(x => { + if (!x.cmd.CanExecute(x.val)) { + return; + } + + x.cmd.Execute(x.val); + }); + } + + /// + /// RegisterAsyncFunction registers an asynchronous method that returns a result + /// to be called whenever the Command's Execute method is called. + /// + /// The function to be run in the + /// background. + /// + /// An Observable that will fire on the UI thread once per + /// invoecation of Execute, once the async method completes. Subscribe to + /// this to retrieve the result of the calculationFunc. + public static IObservable RegisterAsyncFunction(this LegacyRxCmd This, + Func calculationFunc, + IScheduler scheduler = null) + { + Contract.Requires(calculationFunc != null); + + var asyncFunc = calculationFunc.ToAsync(scheduler ?? RxApp.TaskpoolScheduler); + return This.RegisterAsync(asyncFunc); + } + + /// + /// RegisterAsyncAction registers an asynchronous method that runs + /// whenever the Command's Execute method is called and doesn't return a + /// result. + /// + /// The function to be run in the + /// background. + public static IObservable RegisterAsyncAction(this LegacyRxCmd This, + Action calculationFunc, + IScheduler scheduler = null) + { + Contract.Requires(calculationFunc != null); + + // NB: This PermaRef isn't exactly correct, but the people using + // this method probably are Doing It Wrong, so let's let them + // continue to do so. + return This.RegisterAsyncFunction(x => { calculationFunc(x); return new Unit(); }, scheduler) + .Publish().PermaRef(); + } + + /// + /// RegisterAsyncTask registers an TPL/Async method that runs when a + /// Command gets executed and returns the result + /// + /// An Observable that will fire on the UI thread once per + /// invoecation of Execute, once the async method completes. Subscribe to + /// this to retrieve the result of the calculationFunc. + public static IObservable RegisterAsyncTask(this LegacyRxCmd This, Func> calculationFunc) + { + Contract.Requires(calculationFunc != null); + return This.RegisterAsync(x => calculationFunc(x).ToObservable()); + } + + /// + /// RegisterAsyncTask registers an TPL/Async method that runs when a + /// Command gets executed and returns no result. + /// + /// The function to be run in the + /// background. + /// An Observable that signals when the Task completes, on + /// the UI thread. + public static IObservable RegisterAsyncTask(this LegacyRxCmd This, Func calculationFunc) + { + Contract.Requires(calculationFunc != null); + + // NB: This PermaRef isn't exactly correct, but the people using + // this method probably are Doing It Wrong, so let's let them + // continue to do so. + return This.RegisterAsync(x => calculationFunc(x).ToObservable()) + .Publish().PermaRef(); + } + } +} diff --git a/ReactiveUI/ReactiveUI.csproj b/ReactiveUI/ReactiveUI.csproj index ba44e12aa4..e5b9c38a08 100644 --- a/ReactiveUI/ReactiveUI.csproj +++ b/ReactiveUI/ReactiveUI.csproj @@ -123,6 +123,7 @@ + @@ -135,4 +136,7 @@ --> + + + diff --git a/ReactiveUI/ReactiveUI_Android.csproj b/ReactiveUI/ReactiveUI_Android.csproj index ddfe476988..b1184e57dd 100644 --- a/ReactiveUI/ReactiveUI_Android.csproj +++ b/ReactiveUI/ReactiveUI_Android.csproj @@ -140,6 +140,7 @@ + diff --git a/ReactiveUI/ReactiveUI_Mac.csproj b/ReactiveUI/ReactiveUI_Mac.csproj index 669057f4ba..2cdc60913e 100644 --- a/ReactiveUI/ReactiveUI_Mac.csproj +++ b/ReactiveUI/ReactiveUI_Mac.csproj @@ -143,6 +143,7 @@ + diff --git a/ReactiveUI/ReactiveUI_Net45.csproj b/ReactiveUI/ReactiveUI_Net45.csproj index 1f99d6895a..76dd304110 100644 --- a/ReactiveUI/ReactiveUI_Net45.csproj +++ b/ReactiveUI/ReactiveUI_Net45.csproj @@ -153,6 +153,7 @@ + diff --git a/ReactiveUI/ReactiveUI_WP8.csproj b/ReactiveUI/ReactiveUI_WP8.csproj index 4dc95d94b8..c7630fe25a 100644 --- a/ReactiveUI/ReactiveUI_WP8.csproj +++ b/ReactiveUI/ReactiveUI_WP8.csproj @@ -145,6 +145,7 @@ + diff --git a/ReactiveUI/ReactiveUI_WinRT.csproj b/ReactiveUI/ReactiveUI_WinRT.csproj index dfe18f5e46..c1c5efd8a0 100644 --- a/ReactiveUI/ReactiveUI_WinRT.csproj +++ b/ReactiveUI/ReactiveUI_WinRT.csproj @@ -107,6 +107,7 @@ + @@ -159,4 +160,4 @@ --> - \ No newline at end of file + diff --git a/ReactiveUI/ReactiveUI_WinRT80.csproj b/ReactiveUI/ReactiveUI_WinRT80.csproj index b179c47429..694d0dc847 100644 --- a/ReactiveUI/ReactiveUI_WinRT80.csproj +++ b/ReactiveUI/ReactiveUI_WinRT80.csproj @@ -107,6 +107,7 @@ + @@ -162,4 +163,4 @@ --> - \ No newline at end of file + diff --git a/ReactiveUI/ReactiveUI_iOS.csproj b/ReactiveUI/ReactiveUI_iOS.csproj index 02c2a6ead9..b051f8aaa3 100644 --- a/ReactiveUI/ReactiveUI_iOS.csproj +++ b/ReactiveUI/ReactiveUI_iOS.csproj @@ -155,6 +155,7 @@ + From 5d44ea9441e0a7fbe36957e3874e21d4376c5c3f Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Sun, 8 Jun 2014 17:36:10 -0700 Subject: [PATCH 7/7] These methods are just :trollface:ing, make people use Task.Run --- ReactiveUI/ReactiveCommand.cs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/ReactiveUI/ReactiveCommand.cs b/ReactiveUI/ReactiveCommand.cs index 570af1ea39..d11cad504f 100644 --- a/ReactiveUI/ReactiveCommand.cs +++ b/ReactiveUI/ReactiveCommand.cs @@ -35,26 +35,6 @@ public static ReactiveCommand CreateAsyncObservable(Func(Observable.Return(true), executeAsync, scheduler); } - public static ReactiveCommand CreateAsyncFunction(IObservable canExecute, Action executeAsync, IScheduler scheduler = null) - { - return new ReactiveCommand(canExecute, x => Observable.Start(() => executeAsync(x), RxApp.TaskpoolScheduler), scheduler); - } - - public static ReactiveCommand CreateAsyncFunction(IObservable canExecute, Func executeAsync, IScheduler scheduler = null) - { - return new ReactiveCommand(canExecute, x => Observable.Start(() => executeAsync(x), RxApp.TaskpoolScheduler), scheduler); - } - - public static ReactiveCommand CreateAsyncFunction(Func executeAsync, IScheduler scheduler = null) - { - return new ReactiveCommand(Observable.Return(true), x => Observable.Start(() => executeAsync(x), RxApp.TaskpoolScheduler), scheduler); - } - - public static ReactiveCommand CreateAsyncFunction(Action executeAsync, IScheduler scheduler = null) - { - return new ReactiveCommand(Observable.Return(true), x => Observable.Start(() => executeAsync(x), RxApp.TaskpoolScheduler), scheduler); - } - public static ReactiveCommand CreateAsyncTask(IObservable canExecute, Func> executeAsync, IScheduler scheduler = null) { return new ReactiveCommand(canExecute, x => executeAsync(x).ToObservable(), scheduler);