Skip to content
NLNuvyntra Labs

Documentation

Commands

ModelCommand and AsyncModelCommand: CanExecute, concurrency, timeout, retry, and how they sit on the operation pipeline.

Why commands live on the ViewModel

A command is the ViewModel-facing handle for a user action. The page binds Button.Command; the ViewModel owns the work, CanExecute, and cancellation. That keeps code-behind empty and lets tests call ExecuteAsync without a visual tree.

  • ModelCommand / ModelCommand<T> — synchronous ICommand; weak CanExecuteChanged.
  • AsyncModelCommand / AsyncModelCommand<T> — async, IsRunning, Cancel, ExecuteAsync; weak CanExecuteChanged.
  • ICommand.Execute never throws. Failures go to IErrorSink / IDialogs. ExecuteAsync still rethrows.
  • CanExecuteChanged, IsRunning, and State raise on IMainThread (0.6.0+). Bind Button.Command only on 0.6.0+.

AsyncModelCommand

AsyncModelCommand runs through IOperationExecutor: CanExecute, a concurrency gate, optional timeout, optional retry, optional debounce / throttle, then the delegate. IsRunning is atomic. Cancel and ViewModel.Dispose cancel in-flight work. ConcurrencyMode values are Prevent, CancelPrevious, Queue, Allow, and Replace.

SaveCommand = new AsyncModelCommand(
    SaveAsync,
    () => CanSave,
    new AsyncCommandOptions
    {
        Concurrency = ConcurrencyMode.Prevent,
        Timeout = TimeSpan.FromSeconds(15),
        RetryCount = 2,
        RetryDelay = TimeSpan.FromSeconds(1),
    });

Binding and CanExecute

Call NotifyCanExecuteChanged when the condition changes (a property setter, a navigation stack change, an auth flip). CanExecuteChanged is a weak event so a Button on a popped page does not pin the command. Do not allocate a new command per execute. Do not store a static command on a long-lived service — that pins the ViewModel graph.