Skip to content

Async Commands

Nicolás Seijas edited this page Jul 28, 2026 · 1 revision

Async Commands

An async operation has a state: it is in operation, or it stopped with an error, or a person cancelled it. Usually you write that state manually. Rambla writes it for you.

Put [StateCommand] on an async method. The generator writes four members.

using Rambla;

public partial class SearchViewModel : RamblaState
{
    [StateCommand(CancelPrevious = true)]
    private async Task SearchAsync(CancellationToken token)
        => Results = await _api.SearchAsync(Query, token);
}

The example makes these members:

Member Type Function
SearchCommand AsyncStateCommand Bind a button to this command.
IsSearching bool true while the run is in operation.
SearchError Exception? The error of the last run, or null.
CancelSearchCommand ICommand Cancels the run.

IsSearching and SearchError are properties of your view model. They notify through the same flush as your other properties.

The signature of the method

The method must obey these rules:

  • It must give a Task. A Task<T> is not permitted in V1.
  • It must have no parameters, or one CancellationToken parameter.
  • It must not be static. It must not be generic.

If the method does not obey these rules, the generator makes an error. The errors are RMB006 to RMB009.

The two concurrency policies

By default, the command refuses a second run while a run is in operation. CanExecute gives false, thus the bound button disables itself.

[StateCommand]                       // Default: refuse a second run.
private Task SaveAsync() => _repository.SaveAsync();

With CancelPrevious, the policy is latest-wins. A new run cancels the run in operation and replaces it. Use this policy for a search that runs while a person types.

[StateCommand(CancelPrevious = true)]
private Task SearchAsync(CancellationToken token) => _api.SearchAsync(Query, token);

The cancelled run cannot change the state of the new run. Only the last run gives a value to IsSearching and to SearchError.

Errors and cancellation

An error in the method does not go to the caller. The command puts the error in the error property. A person starts a command from the UI, and no code can catch an error from that operation.

<TextBlock Text="{Binding SearchError.Message}"
           Visibility="{Binding SearchError, Converter={StaticResource NullToCollapsed}}" />

The command deletes the error when the next run starts.

Cancellation is not an error. If your method makes an OperationCanceledException from the token of the command, the error property stays null.

Bind the buttons

<Button Content="Search" Command="{Binding SearchCommand}" />
<Button Content="Cancel" Command="{Binding CancelSearchCommand}" />
<ProgressBar IsIndeterminate="True"
             Visibility="{Binding IsSearching, Converter={StaticResource BoolToVisible}}" />

The cancel command is enabled only while the run is in operation. Do not write code that enables or disables these buttons.

Change the generated names

The generator removes the suffix Async. Then it makes the name of the busy property with the English -ing form: Save gives IsSaving, and Submit gives IsSubmitting.

The -ing rule cannot be correct for all verbs. If the name is not correct, give the name:

[StateCommand(Name = "SignIn", BusyName = "IsSigningIn", ErrorName = "SignInFailure")]
private Task LoginAsync() => _auth.LoginAsync();

Use AsyncStateCommand without the generator

You can make the command manually:

using Rambla;

var command = new AsyncStateCommand(
    execute: token => RefreshAsync(token),
    canExecute: () => IsConnected,
    cancelPrevious: false,
    scheduler: Scheduler);

await command.ExecuteAsync();     // Await the run. It does not throw.
command.Cancel();                 // Cancel the run.
command.NotifyCanExecuteChanged();  // Call this if the canExecute data changed.
Member Function
IsRunning true while the run is in operation.
Error The error of the last run, or null.
CancelCommand Cancels the run.
CanBeCanceled true while there is a run to cancel.
StateChanged Occurs at each change of the run state.
CanExecuteChanged Occurs when CanExecute can be different.

Threads

Start a command from the UI thread. The command then gives the values of IsRunning, Error and CanExecuteChanged on that same thread, for all schedulers.

Warning: version 0.5.0 has a defect in this behavior. The run continued on a thread pool thread. With the ImmediateStateScheduler, the button then stayed disabled after the first run that was truly asynchronous. Version 0.5.1 corrects this defect. Use version 0.5.1 or a later version.

Refer to Core Semantics for the full contract.

Clone this wiki locally