After having updated all the MVVM commands to follow the ReactiveUI 7 style as described in my previous blog post, there a few more things in the code I wasn’t fully content with: All the code lines for command declaration and initizalition are type-safe and quite verbose by now, which unfortunately does not make them more readable. For example, consider the following four command property declarations:
public ReactiveCommand<Unit, Unit> SimpleCommand { get; } public ReactiveCommand<int, Unit> CommandWithParameter { get; } public ReactiveCommand<Unit, bool> CommandWithResult { get; } public ReactiveCommand<int, bool> AdvancedCommand { get; }
They all look pretty much the same, although represents different types of commands: Only some of them accept a parameter or return a result value, which (in my opinion) is an important difference which affects how to consume these commands. One of the reasons for bad readability is the obligatory use of the Reactive-internal Unit
type, which acts as placeholder to be interpreted as nothing or void. (By the way, note that this actually needs to be System.Reactive.Unit
if not including using System.Reactive
. One reason for me disliking this type in general is that I’m using an additional dependency that also declares a type called Unit
, so (to avoid mismatch, which had cost me hours of debugging time more than once) I’m in general using the full namespace everywhere – a practice that generates even longer lines of code, making the property declarations even less readable.)
To overcome all those little disadvantages, I came up with a solution that includes a few wrapper classes: The idea is to create explicit command classes for each type of command, and even explicitly name these after their purpose. For example, we can create a ResultReactiveCommand
class as follows:
public class ResultReactiveCommand<TResult> : ReactiveCommand<Unit, TResult> { protected internal ResultReactiveCommand(Func<Unit, IObservable<TResult>> execute, IObservable<bool> canExecute = null, IScheduler outputScheduler = null) : base(execute, canExecute ?? Observable.Return(true), outputScheduler ?? RxApp.MainThreadScheduler) { } public static ResultReactiveCommand<TResult> Create(Func<TResult> execute, IObservable<bool> canExecute = null, IScheduler outputScheduler = null) { if (execute == null) throw new ArgumentNullException(nameof(execute)); Func<Unit, IObservable<TResult>> func = unit => Observable.Start(execute); return new ResultReactiveCommand<TResult>(func, canExecute, outputScheduler); } public static ResultReactiveCommand<TResult> CreateFromTask(Func<Task<TResult>> execute, IObservable<bool> canExecute = null, IScheduler outputScheduler = null) { if (execute == null) throw new ArgumentNullException(nameof(execute)); Func<Unit, IObservable<TResult>> func = unit => Observable.StartAsync(execute); return new ResultReactiveCommand<TResult>(func, canExecute, outputScheduler); } }
This code snippet might look confusing at first sight, but is actually not so difficult to understand. Note the following features:
- The
ResultReactiveCommand
class has only one generic parameter (which represents the type of the result the command will generate) – the command does not accept a parameter, so there is no need to specify aTParam
type parameter as in the originalReactiveCommand
. - It is derived from the original
ReactiveCommand
class, so the only thing we need to overload is the command’s creation, for everything else the underlyingReactiveCommand
-internal logic will be used. This also means our custom command can be used in combination withReactiveCommand
instances if desired, since under hood both represent the same type. - The former means: We obviously need to create our own constructor, however all it does is calling the underlying
ReactiveCommand
constructor (the only magic in the constructor method is the use of optional parameters, which are replaced by default values if null – this is necessary to be able to use our custom command without specifyingcanExecute
condition and scheduler). - To mimic the original
ReactiveCommand
behavior and make it as easy as possible to use our custom command wrapper class, we define the same staticCreate
methods that are well-known fromReactiveCommand
(note that in this sample code snippet, I only included theCreate
andCreateFromTask
methods because these are the only ones needed in my current project, but of course aCreateFromObservable
method could be added in a similar way if necessary). - All these
Create...
methods do is initialize an anonymous method to execute the command’s business logic, and call the constructor (see above!) to instantiate the underlyingReactiveCommand
!
In a similar way, a ParamReactiveCommand<TParam>
that specifies a parameter type but no return value, and a SimpleReactiveCommand
that does neither accept parameters not return a result could be implemented. Of course, it would also be possible to create a ParamResultReactiveCommand<TParam, TResult>
, however I chose to simply use the original ReactiveCommand<TParam, TResult>
for that purpose.
With this set of wrapper classes at hand, we can create shorter and more verbose command declarations through
- using self-explanatory class names, and
- omitting type parameters completely, instead of filling them with the unnecessary
System.Reactive.Unit
type,
as the following example snippet (a duplicate of the snippet presented at the beginning of this article) shows:
public SimpleReactiveCommand SimpleCommand { get; } public ParamReactiveCommand<int> CommandWithParameter { get; } public ResultReactiveCommand<bool> CommandWithResult { get; } public ReactiveCommand<int, bool> AdvancedCommand { get; }
Also the usage of our custom command classes is rather easy since it does not differ much from the default ReactiveCommand
:
public MyViewModel() { SimpleCommand = SimpleReactiveCommand.Create(SimpleCommandImpl); CommandWithParameter = ParamReactiveCommand<int>.CreateFromTask(CommandWithParameterImpl); CommandWithResult = ResultReactiveCommand<bool>.CreateFromTask(CommandWithResult); AdvancedCommand = ReactiveCommand.CreateAsyncTask(AdvancedCommandImpl); }