Skip to content

View Models

Run the complete page example.

A view shows things on the screen: buttons, text boxes, lists. Testing it usually means driving a real window, which is slow and brittle. A view model is a plain class that holds the same values and the same commands, with no button or window in sight. A test can create one and check its properties directly. The view's job shrinks to binding its controls to the view model's properties and commands; the view model's job is everything else. This split is the Model-View-ViewModel pattern (MVVM), and ReactiveUI is built around it.

A view model raises a change notification each time one of its properties changes. The view listens for that notification and updates the control bound to the property. ReactiveObject is the base class ReactiveUI gives you for this.

%%{init: {"theme": "base", "themeVariables": {"fontFamily": "Roboto, Helvetica, Arial, sans-serif", "fontSize": "15px", "primaryColor": "#DCE9FF", "primaryBorderColor": "#6C8EC4", "primaryTextColor": "#0B2447", "secondaryColor": "#E3F2E8", "secondaryBorderColor": "#7FA88C", "secondaryTextColor": "#12301C", "tertiaryColor": "#F3E5F5", "tertiaryBorderColor": "#A98BB0", "tertiaryTextColor": "#2E1437", "lineColor": "#7B8699", "textColor": "#1B1F27", "noteBkgColor": "#FFF4D6", "noteBorderColor": "#C9A94F", "noteTextColor": "#3A2A00", "actorBkg": "#DCE9FF", "actorBorder": "#6C8EC4", "actorTextColor": "#0B2447", "signalColor": "#7B8699", "signalTextColor": "#1B1F27", "labelBoxBkgColor": "#F1F3F8", "labelBoxBorderColor": "#A7AEBB", "edgeLabelBackground": "#F7F9FC", "clusterBkg": "#F7F9FC", "clusterBorder": "#C9D1DE"}}}%%
flowchart LR
    classDef view fill:#DCE9FF,stroke:#6C8EC4,color:#0B2447
    classDef vm fill:#E3F2E8,stroke:#7FA88C,color:#12301C
    classDef model fill:#F3E5F5,stroke:#A98BB0,color:#2E1437
    View(["View"]):::view -- "binds to properties and commands" --> VM(["View model"]):::vm
    VM -- "raises a change notification" --> View
    VM -- "reads and writes" --> Model(["Model"]):::model

The view model sits between the view and the model, the data your app works with (a database row, a web service response). It never references a control or a window, so the same view model works on every platform and runs in a plain unit test.

Keep the view model free of view types

A view model never references a control, a window or a UI framework type. It only describes values and commands, and how they relate to each other: "the add button is enabled when the title is not blank", "the error message clears ten seconds after it appears". The view maps those values and commands onto its own controls. This separation is what makes a view model reusable across platforms, and what lets a test create one without a window on screen.

TodoListViewModel on this page loads items through ITodoStore, an interface, rather than talking to a database or a file directly. Keep a view model's dependencies behind interfaces the same way, so a test can supply a fake one. TodoListViewModel also implements IActivatableViewModel, so it can start and stop work while the screen is shown or hidden; Activation covers that lifecycle.

Write a property the view can bind to

1. Derive from ReactiveObject. TodoListViewModel, the example view model on this page, drives a to-do list screen: a title box for a new item, a filter box, and the list of items that match it.

2. Give a read-write property a setter that calls RaiseAndSetIfChanged. Write every settable property this way: it sets the backing field and raises the change notification, but only when the value actually changes.

public string NewTitle
{
    get;
    set => this.RaiseAndSetIfChanged(ref field, value);
} = string.Empty;

3. Subscribe, then change the property. This test creates the view model, counts how many times NewTitle raises its change, then sets it three times, twice with the same value.

using TodoListViewModel viewModel = new(InMemoryTodoStore.CreateSeeded());
int raised = 0;
viewModel.PropertyChanged += (_, e) =>
{
    if (e.PropertyName != nameof(TodoListViewModel.NewTitle))
    {
        return;
    }

    raised++;
};

viewModel.NewTitle = GroceriesTitle;
viewModel.NewTitle = GroceriesTitle;
viewModel.NewTitle = DentistTitle;

Console.WriteLine(raised);
2

The second assignment sets the value the property already holds, so RaiseAndSetIfChanged raises nothing for it; only two of the three assignments raise a notification. Reactive Object covers RaiseAndSetIfChanged, the Changing/Changed streams and suppressing notifications in full.

You do not have to write this setter yourself. ReactiveUI brings ReactiveUI.SourceGenerators, and its [Reactive] attribute writes the setter for you. Let a source generator write the boilerplate shows how.

Read a property as a stream with WhenAnyValue

A view model often needs to react to its own property changes, not just announce them: enable a command once a box is filled in, or recompute one property from another. WhenAnyValue turns a property into a stream, the opposite direction from RaiseAndSetIfChanged. A stream is an IObservable<T>, a source of values that arrive over time; you subscribe to it to receive them.

using TodoListViewModel viewModel = new(InMemoryTodoStore.CreateSeeded());

using IDisposable subscription = viewModel.WhenAnyValue(x => x.NewTitle)
    .Subscribe(static title => Console.WriteLine($"[{title}]"));

viewModel.NewTitle = GroceriesTitle;
viewModel.NewTitle = DentistTitle;
[]
[Buy groceries]
[Book dentist appointment]

WhenAnyValue delivers the current value as soon as you subscribe, then a new value after each change. Skip the first value with the Select-family operator Skip(1) to get changes only.

using TodoListViewModel viewModel = new(InMemoryTodoStore.CreateSeeded());

using IDisposable subscription = viewModel.WhenAnyValue(x => x.NewTitle)
    .Skip(1)
    .Subscribe(Console.WriteLine);

viewModel.NewTitle = GroceriesTitle;
Buy groceries

Name two or more properties to recompute a value whenever either one changes. This example builds a hint under the add box from both NewTitle and FilterText.

using TodoListViewModel viewModel = new(InMemoryTodoStore.CreateSeeded());

using IDisposable subscription = viewModel.WhenAnyValue(
        x => x.NewTitle,
        x => x.FilterText,
        static (title, filter) => title.Length > 0 && filter.Length > 0
            ? "Clear the filter to see the new item"
            : "Ready")
    .Subscribe(Console.WriteLine);

viewModel.NewTitle = GroceriesTitle;
viewModel.FilterText = "bill";
viewModel.FilterText = string.Empty;
Ready
Ready
Clear the filter to see the new item
Ready

WhenAnyValue also follows a path through another object, so an outer object being replaced and an inner property changing both report. SelectionHolder.Selected below holds a TodoItem; the path x => x.Selected!.Title follows whichever item is currently selected.

TodoItem groceries = new() { Title = GroceriesTitle };
TodoItem dentist = new() { Title = DentistTitle };
SelectionHolder holder = new() { Selected = groceries };

using IDisposable subscription = holder.WhenAnyValue(x => x.Selected!.Title)
    .Subscribe(Console.WriteLine);

groceries.Title = "Buy groceries and milk";
holder.Selected = dentist;
Buy groceries
Buy groceries and milk
Book dentist appointment

WhenAnyValue is one of six related ways to observe a property or a path, and ObservableForProperty observes the same thing by reflection instead of source-generated code. Observing, part of ReactiveUI.Binding, covers all six in full, including how a path behaves when an object in the middle is replaced or null.

Turn a stream back into a property with ToProperty

Some properties should not be set directly at all: a filtered list, a count of unfinished items, a command's busy flag. These are output properties, and ToProperty builds one from a stream. It returns an ObservableAsPropertyHelper<T>, which you keep in a field and read from the property getter.

The example's filtered list combines the loaded items with the filter text, and recomputes whenever either changes.

using TodoListViewModel viewModel = new(InMemoryTodoStore.CreateSeeded());
await viewModel.Load.Execute();

Console.WriteLine(viewModel.Items.Count);

viewModel.FilterText = "book";

Console.WriteLine(viewModel.Items.Count);
Console.WriteLine(viewModel.Items[0].Title);
4
1
Book dentist appointment

RemainingCount follows the loaded items alone, and raises its own change notification, so a view bound to it updates without knowing anything about the underlying list.

using TodoListViewModel viewModel = new(InMemoryTodoStore.CreateSeeded());
using IDisposable subscription = viewModel.WhenAnyValue(x => x.RemainingCount)
    .Subscribe(static count => Console.WriteLine($"{count} left"));

await viewModel.Load.Execute();
await viewModel.Complete.Execute(viewModel.Items[0]);
0 left
3 left
2 left

A command's own IsExecuting stream backs an output property too, here to drive a loading indicator while Load runs.

using TodoListViewModel viewModel = new(InMemoryTodoStore.CreateSeeded());
using IDisposable subscription = viewModel.WhenAnyValue(x => x.IsLoading)
    .Subscribe(static loading => Console.WriteLine(loading ? "Loading..." : "Idle"));

_ = await viewModel.Load.Execute();
Idle
Loading...
Idle

A search box shows the same pattern with a slower source: typing runs a command, and the command's results and busy flag both become output properties. Short text is ignored, so typing "pl" starts no search.

InMemoryGitHubApi api = new();
using RepositorySearchViewModel viewModel = new(api);
Task<IReadOnlyList<Repository>> answered = viewModel.Search.FirstAsync().ToTask();

viewModel.SearchText = "pl";
Console.WriteLine(api.RequestCount);

viewModel.SearchText = "platform";
await answered;

Console.WriteLine(api.RequestCount);
foreach (Repository repository in viewModel.Results)
{
    Console.WriteLine($"{repository.FullName} ({repository.Stars})");
}
0
1
reactiveui/ReactiveUI (8400)
reactiveui/splat (1000)

Write an output property the same way every time: a private ObservableAsPropertyHelper<T> field, a public getter that returns its Value, and a ToProperty call in the constructor. Never put logic in a settable property's setter beyond RaiseAndSetIfChanged; use WhenAnyValue and ToProperty to describe how one property follows another instead. Properties backed by observables covers ToProperty in full, including deferSubscription and how the helper raises your type's change notification.

ObservableAsPropertyHelper<T> is itself IDisposable, and so is every subscription Subscribe returns. Dispose both when the view model is done, the same way TodoListViewModel.Dispose does above. See Dispose your subscriptions for the reasoning.

Let a source generator write the boilerplate

Every read-write property on this page follows the same shape, and every output property follows the same shape too. ReactiveUI.SourceGenerators writes that shape for you, and ReactiveUI brings it, so there is nothing extra to install. Mark a partial property with [Reactive] for a read-write property, or a method with [ReactiveCommand] for a command. The generator writes the property or command while your project builds. For an output property, mark a partial get-only property with [ObservableAsProperty] from ReactiveUI.Binding, and assign its generated helper field with ToProperty. Boilerplate code maps each hand-written shape to its attribute.

Where commands fit

TodoListViewModel.Load, Add, Complete and Delete are all ReactiveCommand, the type a view model uses for an action the view triggers: pressing a button, submitting a form. A command is itself a stream of its results, which is why Load.IsExecuting can back the IsLoading output property above. Commands covers creating one, enabling and disabling it, and reporting its progress and errors.

Namespaces for System.Reactive

ReactiveUI also ships as ReactiveUI.Reactive, compiled from the same source, for apps that use System.Reactive instead of ReactiveUI.Primitives.

View Models at a glance

MemberWhat it does
ReactiveObjectBase class for a view model; see Reactive Object for its full surface.
RaiseAndSetIfChanged<TObj, TRet>Sets a backing field and raises a change notification, only when the value differs.
WhenAnyValueTurns one or more properties, or a path through several objects, into a stream of their values; see Observing.
ObservableForPropertyObserves a property by reflection instead of source-generated code; see Observing.
ObservableAsPropertyHelper<T>Holds the latest value of a stream and raises the property's change notification when it arrives.
ToPropertyBuilds an ObservableAsPropertyHelper<T> from a stream; see Properties backed by observables.
ReactiveProperty<T>A settable, validating, bindable property in one object; see Reactive Property.
[Reactive] / [ReactiveCommand]ReactiveUI.SourceGenerators attributes, which come with ReactiveUI, that write a read-write property or a command at compile time; see ReactiveUI.SourceGenerators.