Skip to content

Guidelines

Run the complete page example.

A ReactiveUI app is built from ordinary C# and a handful of library types: ReactiveObject, ReactiveCommand, and streams you subscribe to. Nothing stops you from using them the wrong way, so this section collects the habits that keep an app maintainable, testable and correct. Each guideline links to the handbook page that explains the type or operator behind it, and to the framework and platform pages that go deeper.

Start the app with RxAppBuilder

Configuring dependency injection, schedulers and platform services one call at a time is easy to get wrong: a service registered too late, a scheduler set on the wrong thread. RxAppBuilder.CreateReactiveUIBuilder() gathers all of it into one fluent chain instead.

1. Build an instance. The example below builds an isolated instance, so it does not touch the schedulers your app's own RxAppBuilder call already set up for this process.

ReactiveUIBuilder builder = RxAppBuilder.CreateReactiveUIBuilder();
builder.WithMainThreadScheduler(Sequencer.Immediate, setRxApp: false);
builder.WithTaskPoolScheduler(TaskPoolSequencer.Default, setRxApp: false);
builder.WithCoreServices();
IReactiveUIInstance instance = builder.BuildApp();

Console.WriteLine(instance.MainThreadScheduler?.GetType().Name);
ImmediateSequencer

A real app also calls WithViewsFromAssembly, WithRegistration to register its own services, and the platform extension for its UI framework, such as WithWpf or WithMaui. RxAppBuilder walks through every one of those calls.

Read schedulers and the exception handler through RxSchedulers and RxState

The static RxApp class does not exist. Read the two schedulers RxAppBuilder configured, and the exception handler it installed, through RxSchedulers and RxState instead.

Console.WriteLine(RxSchedulers.MainThreadScheduler.GetType().Name);
Console.WriteLine(RxSchedulers.TaskpoolScheduler.GetType().Name);
Console.WriteLine(RxState.DefaultExceptionHandler is not null);
ImmediateSequencer
TaskPoolSequencer
True

RxSchedulers.MainThreadScheduler and TaskpoolScheduler are ISequencer values: a sequencer decides when and where work runs. Scheduling covers them and RxState.DefaultExceptionHandler, the observer that receives an exception a subscription would otherwise drop silently.

Remove boilerplate with source generators

ReactiveUI.SourceGenerators comes with ReactiveUI. It writes a property or a command for you from a decorated property, field or method: [Reactive] for a settable property and [ReactiveCommand] for a command. [ObservableAsProperty], for a read-only property backed by a stream, comes from ReactiveUI.Binding, which ReactiveUI also brings. Reduce boilerplate code shows every attribute, and Declare the property with an attribute covers [ObservableAsProperty], which comes from ReactiveUI.Binding.

Turn events into streams

A view raises events instead of exposing streams: a button click, a text box's TextChanged. Subscribing to the event directly means unsubscribing it yourself, usually from an override such as OnClosed, and a forgotten unsubscribe leaks the view. ReactiveUI.Primitives.ObservableEvents, a source generator, writes a stream for every public event a type raises. Call Events() on the object, then read the event as a property on the result. Events is the full walkthrough, and the WPF and Windows Forms pages show it wired into a real control.

Dispose every subscription

A subscription you never dispose keeps running for as long as the object it watches is alive, even after the screen that created it is gone. Dispose your subscriptions covers WhenActivated and DisposeWith, and the one case that needs neither.

Bind to a command instead of wiring a click handler

Calling a view model's method straight from a click handler leaves nothing to disable the control once the action no longer applies. Commands covers binding to a ReactiveCommand with BindCommand instead, and Command names covers naming the command and the method behind it.

Let an async command do its own work

Starting a task from a synchronous command's Subscribe hides that work from IsExecuting and ThrownExceptions. Nothing then stops a second click from starting the work again while the first run is still going. Asynchronous commands shows ReactiveCommand.CreateFromTask doing the same work in a way both properties can see.

Handle a command's failures through ThrownExceptions

A command's ThrownExceptions stream is where a subscriber that only watches, such as the screen, learns about a failure. The await that starts the command still sees the exception; ThrownExceptions is for everyone else.

1. Subscribe to ThrownExceptions before the command runs. The subscriber below turns a failure into a message a screen could show.

using ReactiveCommand<RxVoid, RxVoid> save = ReactiveCommand.CreateFromTask(
    static () => throw new InvalidOperationException("No connection"));
string errorMessage = string.Empty;
using IDisposable subscription = save.ThrownExceptions.Subscribe(_ => errorMessage = "Unable to save. Please try again.");

try
{
    await save.Execute();
}
catch (InvalidOperationException)
{
    // The awaiting caller also sees the failure; ThrownExceptions is for a subscriber that only watches, such as the screen.
}

Console.WriteLine(errorMessage);
Unable to save. Please try again.

Await async work instead of blocking on it

Reading .Result or calling .Wait() on a Task blocks the calling thread until the task finishes. On a UI thread that freezes the screen. On a thread-pool thread it can starve the pool of other work. await keeps the thread free while the work runs.

int grade = await FetchGradeAsync();
Console.WriteLine(grade);
91

Use descriptive names in a WhenAny selector

x and y compile as selector parameters, but they make the reader open the lambda to know which property is which. Use descriptive variables with WhenAny shows named parameters next to the same rule written with x and y.

Put this on the left of WhenAny

this.WhenAny(...) ties a pipeline's lifetime to the view model that owns it. Reading a dependency's property with dependency.WhenAny(...) instead ties the pipeline to how long that dependency lives, which can outlast the view model. Put this on the left of WhenAny covers it, including the one case that still needs DisposeWith.

Read a derived value from an ObservableAsPropertyHelper, not a setter

A plain settable property can be written to from anywhere, including by mistake. An ObservableAsPropertyHelper property has no setter, so its pipeline is the only place its value can come from. Prefer ObservableAsPropertyHelper over properties shows both.

Marshal to the UI thread at the boundary, not after every step

Adding WitnessOn after every operator in a pipeline works, but it schedules far more than it needs to. One WitnessOn(RxSchedulers.MainThreadScheduler), placed where the result reaches a bound property, is enough. UI thread and schedulers shows both forms, and an async command that marshals its result on its own.

Share a chain that more than one subscriber reads

Subscribing to the same WhenAnyValue chain more than once repeats its work for every subscriber. Sharing covers ShareLatest and the other operators that share one subscription among many.

Reach for DynamicData for reactive collections

An ObservableCollection<T> has no stream of its own additions and removals. DynamicData adds a SourceCache<T, TKey> and operators that turn changes into a bound, sorted, filtered list. For the simpler case, a stream of adds and removes from an ObservableCollection<T>, ReactiveUI's own Collections helpers need no extra package.

Validate input with ReactiveUI.Validation

ReactiveUI.Validation, a separate package, adds ReactiveValidationObject and ValidationRule for turning a property's rules into a stream of validation state a command's CanExecute can use. See Validation.

Test with ReactiveUI.Testing

ReactiveUI.Testing gives a test a VirtualClock, a sequencer that moves only when the test tells it to. A test that uses a time operator then runs at once, and the same way every time. Testing covers it.

Keep a view model platform-agnostic, focused, and built through its constructor

A view model that references a Window or another platform type cannot run on a different platform, or in a test, without one. Depend on an interface such as IDialogService instead, and let the platform project supply the implementation.

A view model with one clear job is easier to read, test and reuse than one with several. Give each view model a narrow set of commands and properties, and split one that grows past that into smaller view models.

Take dependencies through the constructor rather than reaching for a service locator inside it. RxAppBuilder.WithRegistration is where those dependencies get registered; Registration covers it, and AppLocator.Current.GetService<T>() remains the escape hatch for code that cannot take a constructor parameter.

Continue reading

The framework/ pages cover the command, property and subscription guidelines above in more detail, with a passing and a failing example for each. The platform/ pages cover the pattern each UI framework layers on top: ReactiveWindow<T>, ReactiveContentPage<T>, ReactiveComponentBase<T> and their equivalents. The debugging/ pages cover diagnosing a problem once you have one. Upgrading covers moving code written for an older ReactiveUI release onto the APIs this section describes.