Skip to content

ReactiveUI.Binding

Run the complete page example.

A screen shows data that changes. When the title of a to-do item changes, the label that shows it must change too. When the user types in a box, the view model must learn the new text. Writing that code by hand means an event handler for every pair of properties, and a handler you forget to remove.

ReactiveUI.Binding keeps a view and a view model in step. You write one line that names the two properties, and the library does the listening, the copying and the cleanup.

A source generator is a compiler add-on that writes code while your project builds. ReactiveUI.Binding reads each place where you observe or bind a property and writes the code for that place. Nothing looks up a property by name at run time. The result is safe to trim and to publish with Native AOT, and a binding costs little to create and to run.

The generated code takes part in your build like any file you wrote. You can read it, step through it in a debugger, and the compiler checks it against your types.

Install

Install the ReactiveUI.Binding NuGet package in your view model project and in your view project. The package carries the runtime library, the source generator and an analyzer. The analyzer reports binding calls that the generator cannot handle, as you type.

dotnet add package ReactiveUI.Binding

The example on this page shows a label from .NET MAUI, so its project also references the MAUI package, ReactiveUI.Binding.Maui. Threading and platforms explains what the platform packages add. Setup covers the builder, the analyzer and Native AOT.

If your app uses ReactiveUI, skip this step. The ReactiveUI packages bring ReactiveUI.Binding and its platform package with them. Installation shows which package brings which.

These examples use .NET 10 and C# 14. They run against a store that keeps its data in memory, so you can run them as they are.

Your first binding

The walkthrough below uses a to-do screen. TodoListViewModel loads items from a store and counts how many are left to do. TodoView holds the controls, among them a RemainingLabel that should show that count. A real UI framework builds the controls from markup. The example view creates them in code.

1. Load a view model. The code below creates the view model with an in-memory store and loads its items. A binding needs data to show, so this gives the later steps four to-do items to work with.

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

await viewModel.LoadAsync();

Console.WriteLine(viewModel.Items.Count);
4

2. Observe a property. WhenChanged takes a lambda that names a property and returns a stream of that property's values. The stream is an IObservable<T>. Nothing happens until you subscribe. A subscription starts the listening and tells the stream what to do with each value.

The code below watches the title of the first item and prints every title the stream delivers. It then renames the item, so you can see the stream report the change without any polling.

var viewModel = new TodoListViewModel(InMemoryTodoStore.CreateSeeded());
await viewModel.LoadAsync();
var registration = viewModel.Items[0];

using (registration.WhenChanged(static x => x.Title).Subscribe(Console.WriteLine))
{
    registration.Title = RenamedTitle;
}

The first value is the current title. Later values arrive each time the title changes. The example prints the current title, then the new one.

Renew car registration
Renew car registration online

The lambda is static x => x.Title. Mark a lambda static when it uses nothing from the method around it. The generator reads the lambda's text at build time, so the lambda has to be written inline at the call. The Subscribe overload that takes a delegate comes from ReactiveUI.Primitives, which the example imports with a using.

Subscribe returns an IDisposable. Disposing it ends the subscription. The using statement disposes it at the closing brace, so the subscription is gone when the block ends. Dispose every subscription you create. A subscription you keep alive holds the object it listens to in memory.

The Observing page covers watching one to sixteen properties at once, paths that cross several objects, and the before-change form, WhenChanging.

3. Bind a property to a control. BindOneWay copies a view model property to a view property and keeps the copy up to date. The code below shows the number of unfinished items in a label, then finishes an item. You never write the update yourself: the label follows the view model.

var viewModel = new TodoListViewModel(InMemoryTodoStore.CreateSeeded());
var view = new TodoView();
await viewModel.LoadAsync();

using (viewModel.BindOneWay(view, static x => x.RemainingCount, static v => v.RemainingLabel.Text, static count => count.ToString(CultureInfo.InvariantCulture)))
{
    Console.WriteLine(view.RemainingLabel.Text);

    viewModel.SelectedItem = viewModel.Items[0];
    await viewModel.CompleteAsync();

    Console.WriteLine(view.RemainingLabel.Text);
}

The label shows the count the moment the binding is created. It shows a new count each time an item is finished. The example selects the first item and finishes it, so the count falls from three to two.

3
2

The call takes the view model, which is the source, and the view, which is the target. Two lambdas name the properties. RemainingCount is an int and the label's Text is a string, so the last argument converts one to the other. The example adds using System.Globalization; for CultureInfo.

The target lambda, static v => v.RemainingLabel.Text, is a path. A property path is a chain of properties, here RemainingLabel and then Text. A binding follows each link of the chain. If a link on the way changes, the binding moves to the new object.

The binding also picks the thread it writes on. A UI framework lets only one thread change a control. BindOneWay writes to the target on the thread that owns it. Threading and platforms shows how.

4. Stop the binding. BindOneWay returns an IDisposable, the same as Subscribe. Dispose it to stop the binding. The code below creates a binding, disposes it at once, and then finishes an item. It shows that a disposed binding stops writing to the view, so a screen that closes leaves nothing behind.

var binding = viewModel.BindOneWay(view, static x => x.RemainingCount, static v => v.RemainingLabel.Text, static count => count.ToString(CultureInfo.InvariantCulture));
binding.Dispose();

viewModel.SelectedItem = viewModel.Items[0];
await viewModel.CompleteAsync();

Console.WriteLine(view.RemainingLabel.Text);

The label keeps the text it had when the binding was disposed. Finishing an item no longer changes it.

3

What happens at build time

For each call to WhenChanged, BindOneWay and the other binding methods, the generator writes a method that does the work for that call. It reads the property names from your lambdas and picks the way to listen for changes from the types of the objects. A type that implements INotifyPropertyChanged announces a change by raising its PropertyChanged event. Other types announce changes in other ways. The Mechanisms page explains how the generator picks one.

The generated method reaches your call in one of two ways. On a compiler that supports interceptors, the compiler replaces your call with the generated method. Otherwise the generator adds an overload that the compiler picks in place of the library's own method. Both ways run the generated code. Your source stays as you wrote it.

The library's own methods, such as the WhenChanged you call, are stubs. A stub throws an InvalidOperationException when it runs, and the message names the Unsafe overload to use instead. You meet the stub when a call site is one the generator cannot read, for example when you pass a lambda that you built earlier and stored in a variable. The analyzer reports such a call while you edit, with a diagnostic whose id starts with RXUIBIND. Setup lists the diagnostics.

The Unsafe overloads find properties by reflection while the program runs. They accept expressions the generator cannot read, and they cost more and are not safe for trimming. Unsafe twins and the runtime fallback covers them.

The five sample apps

Every page in this section uses the same sample apps, so a type you meet on one page is the same type on the next.

AppWhat it models
To-do listA list with a filter box, a count of unfinished items and commands to add and finish an item.
GitHub issuesAn issue board with repositories, issues, comments and users.
Cloud storageA storage browser with buckets, objects, uploads and a connection state.
SchoolA gradebook with students, courses, enrolments, assignments and grades.
BankAccounts, payees and transfers with validation.

The apps live in the Common folder of the examples. They talk to in-memory services, so no example needs a network or a database.

Find a topic

PageWhat you can do
ObservingWatch one to sixteen properties with WhenChanged, WhenChanging, WhenAnyValue, WhenAny and WhenAnyObservable.
Properties backed by observablesBack a read-only property with a stream using ToProperty, ObservableAsPropertyHelper<T> and [ObservableAsProperty].
BindingsConnect properties one way and two ways, bind to a command or an interaction, and handle errors.
ConvertersUse the built-in converters for numbers, dates, booleans, strings and nullable values.
Custom convertersWrite and register your own converter and choose a fallback.
MechanismsSee how a type announces a change and how the generator picks a way to listen.
ViewsUse IViewFor<T>, the view locator and view mappings.
Threading and platformsWrite on the owning thread, choose a scheduler, and use the WPF, WinForms, MAUI and Avalonia packages.
SetupStart the builder, find the System.Reactive package's names, publish with Native AOT and read the analyzer's diagnostics.
Unsafe twins and the runtime fallbackBind a call site that the generator cannot read.
API referenceLook up every public type and member with its parameters and return value.

Run the examples

The documentation examples live in folders named for their page. Each page links to its folder. Each folder is a small program that prints what the page shows. The page examples reference the library source projects, including the generator that writes the binding code.

The code on a page is an excerpt of the real example. Setup and helper code stay in the project when a page does not need them. An example prints its results with Console.WriteLine, and the page shows those lines in a block under the code.

From the repository's src folder, run the example for this page:

dotnet run --project examples/Documentation/Pages/index/index.csproj -c Release

API reference

MemberWhat it doesType and valuesNotes
WhenChangedWatches a property. It delivers the current value when you subscribe, then the new value after each change.Extension method on TObj : class. Takes Expression<Func<TObj, T1>> property1. Returns IObservable<T1>. Other forms take up to 16 properties and return PropertyValues<T1..T16>.The lambda has to be written inline at the call. A call the generator cannot read throws InvalidOperationException when it runs. The Observing page covers the other forms.
WhenChangedUnsafeWatches a property by reflection, for a lambda the generator cannot read.Extension method on TObj : class. Takes Expression<Func<TObj, T1>> property1. Returns IObservable<T1>. Other forms take up to 16 properties.Carries RequiresUnreferencedCode. Throws ArgumentNullException for a null object or lambda.
BindOneWayCopies a source property to a target property, then again after each change. It writes on the thread that owns the target.Extension method on TSource : class. Takes TTarget : class target, a source lambda and a target lambda. The target lambda may be a property path. Add a Func<TSourceProp, TTargetProp> conversionFunc when the two types differ. Returns IDisposable.Dispose the result to stop the binding. A call the generator cannot read throws InvalidOperationException when it runs. Forms that take an ISequencer scheduler or an IBindingTypeConverter are declared in ReactiveSchedulerExtensions. The Bindings page covers them.
BindOneWayUnsafeBinds one way by reflection, for a lambda the generator cannot read.Extension method on TSource : class. Takes the same arguments as BindOneWay, with or without a conversionFunc. Returns IDisposable.Carries RequiresUnreferencedCode. Unsafe twins and the runtime fallback covers it.