# Commanding
ReactiveCommand
class implements the following two interfaces.
ICommand
interfaceIObservable<T>
# Basic usage
This class can be created using ToReactiveCommand
extension method from IObservable<bool>
instance.
When the IObservable<bool>
instance is updated, the CanExecuteChanged
event is raised.
If you always want an executable command, then you can create a ReactiveCommand
instance using the default constructor.
IObservable<bool> canExecuteSource = ...;
ReactiveCommand someCommand = canExecuteSource.ToReactiveCommand(); // non command parameter version.
ReactiveCommand<string> hasCommandParameterCommand = canExecuteSource.ToReactiveCommand<string>(); // has command parameter version
ReactiveCommand alwaysExecutableCommand = new ReactiveCommand(); // non command parameter and always can execute version.
ReactiveCommand<string> alwaysExecutableAndHasCommandParameterCommand = new ReactiveCommand<string>(); // has command parameter and always can execute version.
You can set the initial return value of CanExecute
method using the factory extension method's initalValue
argument.
The default value is true
.
IObservable<bool> canExecuteSource = ...;
ReactiveCommand someCommand = canExecuteSource.ToReactiveCommand(false);
ReactiveCommand<string> hasCommandParameterCommand = canExecuteSource.ToReactiveCommand<string>(false);
When the Execute
method is called, ReactiveCommand
calls the OnNext
callback.
You can register execute logic using the Subscribe
method.
ReactiveCommand someCommand = new ReactiveCommand();
someCommand.Subscribe(_ => { ... some logic ... }); // set an OnNext callback
someCommand.Execute(); // OnNext callback is called.
# Using in ViewModel class
The first example, just use ReactiveCommand
class.
public class ViewModel
{
public ReactiveCommand UpdateTimeCommand { get; }
public ReactiveProperty<string> Time { get; }
public ViewModel()
{
Time = new ReactiveProperty<string>();
UpdateTimeCommand = new ReactiveCommand();
UpdateTimeCommand.Subscribe(_ => Time.Value = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"));
}
}
UWP platform example.
public sealed partial class MainPage : Page
{
private ViewModel ViewModel { get; } = new ViewModel();
public MainPage()
{
this.InitializeComponent();
}
}
<Page x:Class="App1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:App1"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Button Content="Update the time"
Command="{x:Bind ViewModel.UpdateTimeCommand}"
Margin="5" />
<TextBlock Text="{x:Bind ViewModel.Time.Value, Mode=OneWay}"
Style="{ThemeResource BodyTextBlockStyle}"
Margin="5" />
</StackPanel>
</Page>
# Work with LINQ
ReactiveCommand
class implements the IObservable<T>
interface.
Can use LINQ methods, and ReactiveProperty<T>
class can be created from IObservable<T>
.
The previous example code can be changed to this:
public class ViewModel
{
public ReactiveCommand UpdateTimeCommand { get; }
// Don't need that set Value property. So can change to ReadOnlyReactiveProperty.
public ReadOnlyReactiveProperty<string> Time { get; }
public ViewModel()
{
UpdateTimeCommand = new ReactiveCommand();
Time = UpdateTimeCommand
.Select(_ => DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"))
.ToReadOnlyReactiveProperty();
}
}
# Create from IObservable<bool>
Change so that the UpdateTimeCommand
doesn't invoke for 5 secs after the command is invoked.
public class ViewModel
{
public ReactiveCommand UpdateTimeCommand { get; }
public ReadOnlyReactiveProperty<string> Time { get; }
public ViewModel()
{
var updateTimeTrigger = new Subject<Unit>();
UpdateTimeCommand = Observable.Merge(
updateTimeTrigger.Select(_ => false),
updateTimeTrigger.Delay(TimeSpan.FromSeconds(5)).Select(_ => true))
.ToReactiveCommand();
Time = UpdateTimeCommand
.Select(_ => DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"))
.Do(_ => updateTimeTrigger.OnNext(Unit.Default))
.ToReadOnlyReactiveProperty();
}
}
# Create command and subscribe, in one statement
In the case that you aren't using LINQ methods, you can create a command and subscribe in one statement.
WithSubscribe
extension method subscribes and returns the ReactiveCommand
instance:
public class ViewModel
{
public ReactiveCommand UpdateTimeCommand { get; }
public ReactiveProperty<string> Time { get; }
public ViewModel()
{
Time = new ReactiveProperty<string>();
var updateTimeTrigger = new Subject<Unit>();
UpdateTimeCommand = Observable.Merge(
updateTimeTrigger.Select(_ => false),
updateTimeTrigger.Delay(TimeSpan.FromSeconds(5)).Select(_ => true))
.ToReactiveCommand()
.WithSubscribe(() => Time.Value = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")); // here
}
}
WithSubscribe
method is just a shortcut:
// No use the WithSubscribe
var command = new ReactiveCommand();
command.Subscribe(_ => { ... some actions ... });
// Use the WithSubscribe
var command = new ReactiveCommand()
.WithSubscribe(() => { ... some actions ... });
If you use LINQ methods, then separate statements create an instance and subscribe.
# Unsubscribe actions
If need to unsubscribe actions, then use the Dispose
method of IDisposable
which the Subscribe
method returned.
var command = new ReactiveCommand();
var subscription1 = command.Subscribe(_ => { ... some actions ... });
var subscription2 = command.Subscribe(_ => { ... some actions ... });
// Unsubscribe per Subscribe method.
subscription1.Dispose();
subscription2.Dispose();
// Unsubscribe all
command.Dispose();
WithSubscribe
extension method has override methods which have an IDisposable
argument.
IDisposable subscription = null;
var command = new ReactiveCommand().WithSubscribe(() => { ... some actions ... }, out subscription);
// Unsubscribe
subscription.Dispose();
And has another override of Action<IDisposable>
argument.
It is used together with CompositeDisposable
class.
var subscriptions = new CompositeDisposable();
var command = new ReactiveCommand()
.WithSubscribe(() => { ... some actions ... }, subscriptions.Add)
.WithSubscribe(() => { ... some actions ... }, subscriptions.Add);
// Unsubscribe
subscription.Dispose();
In other instance's events subscribe, then you should call the Dispose
method of ReactiveCommand
class at the end of the ViewModel lifecycle.
# Async version ReactiveCommand
AsyncReactiveCommand
class is an async
version ReactiveCommand
class.
This class can subscribe using async
methods, and when executing an async method then CanExecute
method returns false
.
So, this class can't re-execute while the async method is running.
And ExecuteAsync
method is an async version Execute
method. The method is able to wait finishing all async proccesses are added to the command. This method is useful for unit testing and call commands on C#.
# Basic usage
Nearly the same as a ReactiveCommand
class.
The only difference is that it accepts an async
method in Subscribe
method argument, and doesn't implement the IObservable<T>
interface.
public class ViewModel
{
public AsyncReactiveCommand HeavyCommand { get; }
public ReactiveProperty<string> Message { get; } = new ReactiveProperty<string>();
public ViewModel()
{
HeavyCommand = new AsyncReactiveCommand()
.WithSubscribe(async () =>
{
Message.Value = "Heavy command started.";
await Task.Delay(TimeSpan.FromSeconds(5));
Message.Value = "Heavy command finished.";
});
}
}
<Page x:Class="App1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:App1"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Button Content="Heavy command"
Command="{x:Bind ViewModel.HeavyCommand}"
Margin="5" />
<TextBlock Text="{x:Bind ViewModel.Message.Value, Mode=OneWay}"
Margin="5" />
</StackPanel>
</Page>
Of course, AsyncReactiveCommand
is created from IObservable<bool>
.
public class ViewModel
{
public AsyncReactiveCommand HeavyCommand { get; }
public ReactiveProperty<string> Message { get; } = new ReactiveProperty<string>();
public ViewModel()
{
HeavyCommand = Observable.Interval(TimeSpan.FromSeconds(1))
.Select(x => x % 2 == 0)
.ToAsyncReactiveCommand()
.WithSubscribe(async () =>
{
Message.Value = "Heavy command started.";
await Task.Delay(TimeSpan.FromSeconds(5));
Message.Value = "Heavy command finished.";
});
}
}
And AsyncReactiveCommand
implements the IDisposable
interface.
You should call the Dispose
method when the another instance's event subscribe.
# Share CanExecute
state
Sometimes you want only one async method is to be executing in a page.
In this case, you can share CanExecute
state between AsyncReactiveCommand
instances.
When it's created from a same IReactiveProperty<bool>
instance, you can synchronize the CanExecute
state.
public class ViewModel
{
private ReactiveProperty<bool> HeavyCommandCanExecuteState { get; } = new ReactiveProperty<bool>(true);
public AsyncReactiveCommand HeavyCommand1 { get; }
public AsyncReactiveCommand HeavyCommand2 { get; }
public ReactiveProperty<string> Message { get; } = new ReactiveProperty<string>();
public ViewModel()
{
HeavyCommand1 = HeavyCommandCanExecuteState
.ToAsyncReactiveCommand()
.WithSubscribe(async () =>
{
Message.Value = "Heavy command 1 started.";
await Task.Delay(TimeSpan.FromSeconds(5));
Message.Value = "Heavy command 1 finished.";
});
HeavyCommand2 = HeavyCommandCanExecuteState
.ToAsyncReactiveCommand()
.WithSubscribe(async () =>
{
Message.Value = "Heavy command 2 started.";
await Task.Delay(TimeSpan.FromSeconds(5));
Message.Value = "Heavy command 2 finished.";
});
}
}
<Page x:Class="App1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:App1"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Button Content="Heavy command 1"
Command="{x:Bind ViewModel.HeavyCommand1}"
Margin="5" />
<Button Content="Heavy command 2"
Command="{x:Bind ViewModel.HeavyCommand2}"
Margin="5" />
<TextBlock Text="{x:Bind ViewModel.Message.Value, Mode=OneWay}"
Margin="5" />
</StackPanel>
</Page>
Of course, you can combine IObservable<bool>
and IReactiveProperty<bool>
. IObservable<bool>
for source of AsyncReactiveCommand
, IReactiveProperty<bool>
is for sharing state across AsyncReactiveCommand
s.
You can use ToAsyncReactiveCommand(this IObservable<bool> source, IReactiveProperty<bool> sharedCanExecute = null)
method, like this:
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
namespace RPSample
{
public class MainPageViewModel
{
// for shared state
private ReactivePropertySlim<bool> SharedCanExecute { get; }
// for command source
[Required]
public ReactiveProperty<string> Input { get; }
// commands
public AsyncReactiveCommand CommandA { get; }
public AsyncReactiveCommand CommandB { get; }
public MainPageViewModel()
{
Input = new ReactiveProperty<string>().SetValidateAttribute(() => Input);
// create AsyncReactiveCommands from same source and same IReactiveProperty<bool> for sharing CanExecute status.
SharedCanExecute = new ReactivePropertySlim<bool>(true);
CommandA = Input.ObserveHasErrors
.Inverse()
.ToAsyncReactiveCommand(SharedCanExecute)
.WithSubscribe(() => Task.Delay(3000));
CommandB = Input.ObserveHasErrors
.Inverse()
.ToAsyncReactiveCommand(SharedCanExecute)
.WithSubscribe(() => Task.Delay(3000));
}
}
}
After binding the ViewModel class to view:
// code behind
using Windows.UI.Xaml.Controls;
namespace RPSample
{
public sealed partial class MainPage : Page
{
private MainPageViewModel ViewModel { get; } = new MainPageViewModel();
public MainPage()
{
InitializeComponent();
}
}
}
<Page
x:Class="RPSample.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<StackPanel>
<TextBox Text="{x:Bind ViewModel.Input.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Button
HorizontalAlignment="Stretch"
Command="{x:Bind ViewModel.CommandA}"
Content="CommandA" />
<Button
HorizontalAlignment="Stretch"
Command="{x:Bind ViewModel.CommandB}"
Content="CommandB" />
</StackPanel>
</Page>
It works like this:
# Threading
# ReactiveCommand
class
When using the ReactiveCommand
class, the class raises the CanExecute
event on a scheduler(default is UI thread scheduler.) If you want to change the behaviour then use the overload method of ToReactiveCommand
which has an IScheduler
argument.
See below:
canExecuteSource.ToReactiveCommand(theSchedulerInstanceYouWant);
# AsyncReactiveCommand
class
AsyncReactiveCommand
class doesn't change thread automatically. If you want to change the thread, then use ObserveOn
method.
See below:
canExecuteSource.ObserveOn(theSchedulerInstanceYouWant).ToAsyncReactiveCommand();
# ReactiveCommandSlim
This is a lightweight version of ReactiveCommand
. The main difference from the traditional ReactiveCommand
is that the CanExecuteChanged
event is no longer dispatched on the UI thread. If it is necessary to always trigger the event on the UI thread, use the ObserveOn
method on the IObservable<bool>
source of ReactiveCommandSlim
to explicitly set it.
Additionally, by implementing it in the same way as ReactivePropertySlim
, various performance improvements have been made. The benchmarks are as follows:
Method | Mean | Error | StdDev | Median |
---|---|---|---|---|
CreateReactiveCommand | 291.931 ns | 5.6965 ns | 10.5589 ns | 291.178 ns |
CreateReactiveCommandSlim | 4.313 ns | 0.1293 ns | 0.1080 ns | 4.269 ns |
BasicUsecaseForReactiveCommand | 1,187.294 ns | 22.8930 ns | 21.4141 ns | 1,179.896 ns |
BasicUsecaseForReactiveCommandSlim | 91.861 ns | 1.8934 ns | 3.5096 ns | 91.750 ns |
From top to bottom, these represent the creation of a ReactiveCommand
, the creation of a ReactiveCommandSlim
, the basic use case for ReactiveCommand
, and the basic use case for ReactiveCommandSlim
. There is a performance difference of over 70 times in the instantiation case and 13 times in the basic functionality usage case.
Furthermore, like AsyncReactiveCommand
, it is possible to easily share the execution state across multiple commands by sharing an IReactiveProperty<bool>
.