ReactiveUI.Binding.Analyzer 7.1.0

Prefix Reserved
dotnet add package ReactiveUI.Binding.Analyzer --version 7.1.0
                    
NuGet\Install-Package ReactiveUI.Binding.Analyzer -Version 7.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ReactiveUI.Binding.Analyzer" Version="7.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ReactiveUI.Binding.Analyzer" Version="7.1.0" />
                    
Directory.Packages.props
<PackageReference Include="ReactiveUI.Binding.Analyzer" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ReactiveUI.Binding.Analyzer --version 7.1.0
                    
#r "nuget: ReactiveUI.Binding.Analyzer, 7.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ReactiveUI.Binding.Analyzer@7.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ReactiveUI.Binding.Analyzer&version=7.1.0
                    
Install as a Cake Addin
#tool nuget:?package=ReactiveUI.Binding.Analyzer&version=7.1.0
                    
Install as a Cake Tool

NuGet Stats Build Code Coverage License <br> <a href="https://www.nuget.org/packages/ReactiveUI.Binding"> <img src="https://img.shields.io/nuget/dt/ReactiveUI.Binding.svg"> </a> <a href="https://reactiveui.net/slack"> <img src="https://img.shields.io/badge/chat-slack-blue.svg"> </a> <a href="https://github.com/reactiveui/ReactiveUI.Binding.SourceGenerators/labels/good%20first%20issue"> <img src="https://img.shields.io/badge/first--timers--only-friendly-blue.svg"> </a> <a href="https://github.com/reactiveui/ReactiveUI.Binding.SourceGenerators/stargazers"> <img src="https://img.shields.io/github/stars/reactiveui/ReactiveUI.Binding.SourceGenerators.svg?style=social"> </a>

<img src="images/logo.png" width="200">

ReactiveUI.Binding.SourceGenerators

You have a property. When it changes, something else needs to know. That might be a label, a validation rule or another property. This library lets you say that in one line. It writes the code for you when you build.

This page covers this binding engine. It does not teach reactive programming. The WhenAny handbook teaches property observation. The data binding guide covers the binding methods in depth.

Table of Contents

Core Team

<table> <tbody> <tr> <td align="center" valign="top"> <img width="100" height="100" src="https://github.com/ChrisPulman.png?s=150"> <br> <a href="https://github.com/ChrisPulman">Chris Pulman</a> <p>London, UK</p> </td> <td align="center" valign="top"> <img width="100" height="100" src="https://github.com/glennawatson.png?s=150"> <br> <a href="https://github.com/glennawatson">Glenn Watson</a> <p>Melbourne, Australia</p> </td> </tr> </tbody> </table>

The problem it solves

C# already tells you when a property changes. A class raises PropertyChanged. You attach a handler, check which property changed, and read the new value.

That works for one property. It gets hard when you follow a path through two properties:

// Tell me when the city changes.
viewModel.PropertyChanged += (sender, args) =>
{
    if (args.PropertyName != nameof(viewModel.Address))
    {
        return;
    }

    // Address was replaced. Detach from the old Address, attach to the new one,
    // and start watching its City. Then undo all of it when you are finished.
};

With this library, you write the path as a lambda instead:

IObservable<string> city = viewModel.WhenChanged(x => x.Address.City);

This attaches to Address and to City. When Address is replaced, it moves to the new Address. When you dispose the subscription, it detaches from everything.

The lambda only names the path. A source generator is a compiler add-on that writes C# code while your project builds. This library's generator reads the properties in the lambda, Address and then City. It writes code that reads each one and attaches to the event that reports its change.

Your first binding

1. Install the package.

dotnet add package ReactiveUI.Binding

2. Raise PropertyChanged from your view model. Any class that does this can be observed. You do not need a base class.

public class PersonViewModel : INotifyPropertyChanged
{
    private string _name = "";

    public event PropertyChangedEventHandler? PropertyChanged;

    public string Name
    {
        get => _name;
        set
        {
            _name = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
        }
    }
}

3. Observe a property. You get an IObservable<T>. It gives you the current value, then every later value.

var vm = new PersonViewModel { Name = "Ada" };

IDisposable subscription = vm.WhenChanged(x => x.Name)
    .Subscribe(name => Console.WriteLine(name));   // prints Ada

vm.Name = "Grace";                                // prints Grace

4. Or bind it straight to a control. This writes Name into the label now, and again on every change.

IDisposable binding = vm.BindOneWay(view, x => x.Name, v => v.NameLabel.Text);

5. Dispose when you are done. Both calls return an IDisposable. Disposing it detaches every handler the binding attached.

Keep your subscriptions in a CompositeDisposable. Dispose it when the view goes away. A subscription you never dispose keeps the view model alive.

WhenChanging, BindTwoWay, BindCommand and the other methods work the same way.

How a property reports a change

PropertyChanged is one way for a property to report a change. There are others. A WPF TextBox.Text does not raise PropertyChanged. An iOS view does not raise it at all.

Each way of reporting a change is a mechanism. The generator first finds the mechanisms a type offers. Each mechanism uses a different event, and attaches to it a different way.

Mechanism Supported properties Before the change
INotifyPropertyChanged Properties that raise PropertyChanged, including MAUI bindable properties. no
INotifyPropertyChanging Properties that raise PropertyChanging. yes
IReactiveObject ReactiveUI properties that raise change notifications. yes
WPF dependency property Dependency properties such as TextBox.Text. see below
WinUI and Uno dependency property Properties backed by a framework dependency property. no
WinForms component Properties with a public {PropertyName}Changed event. no
Apple KVO Native NSObject properties and exported properties. yes
UIKit and AppKit notifications Supported control text, value, date and selection properties. no
Android view Supported widget properties such as TextView.Text. no

A type can offer more than one mechanism. A ReactiveObject in a WPF window implements INotifyPropertyChanged. It may also have dependency properties. The generator uses the most specific mechanism that can reach the property you named. So a plain C# property on a DependencyObject still uses PropertyChanged. Which mechanism wins lists the order.

Observing before a change needs the type to raise an event before the value is replaced. Some types do not. For those, WhenChanging reads the value once and then stays silent. RXUIBIND004 warns you about this when you build. A WPF dependency property is the exception. It keeps a live subscription and delivers each new value.

The generator checks each property in a path

x => x.Address.City names two properties. They can use different mechanisms. Address might be a plain property on a view model that raises PropertyChanged. City might be a dependency property on a WPF control.

The generator checks each property on its own when you build. It writes the code to attach to each one.

// Address: INotifyPropertyChanged on the view model.
// City:    a dependency property on the control it holds.
vm.WhenChanged(x => x.Address.City);

When Address is replaced, the subscription detaches from the old Address and attaches to the new one. You would otherwise write this part by hand, and it is easy to get wrong.

If a property in the path raises no change event, replacing it cannot be detected. Properties below it can still be observed on the initial object. The analyzer reports RXUIBIND010 when you build.

Trimming and NativeAOT

For example, observing a WinForms text box uses its own change event:

var changes = textBox.WhenChanged(x => x.Text);

The central framework calls are direct property access and event subscription:

EventHandler handler = (_, _) => observer.OnNext(textBox.Text);
textBox.TextChanged += handler;
// When the subscription is disposed:
textBox.TextChanged -= handler;

The complete observation also delivers the initial value and handles disposal. Ordinary JIT projects benefit from avoiding runtime expression analysis and property lookup, keeping values typed, and catching unsupported bindings during the build. Trimming and NativeAOT support are additional benefits.

Bindings with property paths known at build time support PublishTrimmed and PublishAot. Use the normal binding APIs with inline property lambdas. The Unsafe APIs use reflection and carry trimming warnings. Custom providers remain responsible for their own trimming and NativeAOT requirements.

Compiler requirements

Use supported build tools and C# 7.3 or later.

Which compiler you have

You do not choose the compiler directly. It comes with your build tools.

  • Building in Visual Studio, or with Visual Studio's msbuild, uses the compiler that ships with that Visual Studio version.
  • Building with dotnet build uses the compiler that ships with that .NET SDK version.
Your build tools Support
Visual Studio 2022 17.13 or later, Visual Studio 2026, or .NET SDK 9.0.200 or later Supported
Visual Studio 2022 17.8 to 17.12, or .NET SDK 8.0.100 to 9.0.1xx Supported, with call-site restrictions reported by RXUIBIND009
Anything older Not supported. The build fails with RXUIBIND100.

Microsoft's Roslyn version table lists the compiler in each Visual Studio version.

.NET Framework projects

A .NET Framework project gets the same features as a .NET project. It needs the SDK-style project format and new enough build tools. An SDK-style project file names the SDK on its first line and sets a target framework:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net472</TargetFramework>
  </PropertyGroup>
</Project>

Build that project with Visual Studio 2022 17.13 or later, or with dotnet build on .NET SDK 9.0.200 or later. The C# 7.3 language version used by .NET Framework projects is supported.

An old-style project file has no Sdk attribute and lists its source files one by one. It still works. It uses the compiler from the Visual Studio that builds it.

Build properties

Two build properties change this.

  • Set ReactiveUIBindingUseInterceptors to false to use the overloads on a compiler that can intercept.
  • Set ReactiveUIBindingEmitGeneratedCodeMarkers to false to drop the // <auto-generated/> header from generated files. Analyzer and compiler warnings inside those files then show up.

When nothing claims the call

Some call sites cannot be read when you build. Examples are a lambda stored in a variable, an expression built at run time, and a call on a generic type parameter T. None of them name a path the generator can see.

For those call sites, call the Unsafe overload. It uses reflection. Reflection looks up each property by name while the app runs. A trimmer cannot see those lookups, so an Unsafe overload may break after trimming or ahead-of-time publishing. The other overloads keep working.

When nothing was generated for a call site, the plain method throws. The error message names the Unsafe overload to use:

Expression<Func<MyViewModel, string>> selector = x => x.Name;

vm.WhenChanged(selector);        // throws, and names WhenChangedUnsafe
vm.WhenChangedUnsafe(selector);  // walks the path by reflection

Thirteen methods have an Unsafe twin.

Resolved when you build Resolved by reflection
WhenChanged WhenChangedUnsafe
WhenChanging WhenChangingUnsafe
WhenAnyValue WhenAnyValueUnsafe
WhenAny WhenAnyUnsafe
WhenAnyObservable WhenAnyObservableUnsafe
BindOneWay BindOneWayUnsafe
BindTwoWay BindTwoWayUnsafe
OneWayBind OneWayBindUnsafe
Bind BindUnsafe
BindTo BindToUnsafe
BindCommand BindCommandUnsafe
BindInteraction BindInteractionUnsafe
InvokeCommand InvokeCommandUnsafe

Every Unsafe overload carries [RequiresUnreferencedCode]. The plain overloads carry none. So a PublishTrimmed or PublishAot build warns about each call that uses reflection, and about nothing else. RXUIBIND001, RXUIBIND006 and RXUIBIND009 point out those call sites when you build, before anything throws.

The scheduler overloads split the same way. Both kinds live on ReactiveSchedulerExtensions.

Two members always use reflection, so their plain names carry the attribute.

Installing

dotnet add package ReactiveUI.Binding

The generator and the analyzer ship inside that package. You do not need to reference anything else.

If your app uses System.Reactive, install ReactiveUI.Binding.Reactive instead. It is the same library, built against System.Reactive.Concurrency.IScheduler. Its namespaces start with ReactiveUI.Binding.Reactive.

Reference one package or the other, never both. They share no type names. So both together put two copies of every method in scope.

Supported frameworks

Target Versions Microsoft support ends
.NET 10.0, 11.0 November 2028 for .NET 10
.NET 8.0, 9.0 10 November 2026
.NET Framework 4.7, 4.7.1, 4.7.2, 4.8, 4.8.1 tied to the Windows version
.NET Framework 4.6.2 12 January 2027

.NET 8 and .NET 9 leave Microsoft support on 10 November 2026. This library drops them on that date. .NET Framework 4.6.2 leaves support on 12 January 2027, and this library drops it then. Before those dates, move to .NET 10 or later, or to .NET Framework 4.7 or later. See the .NET support policy and the .NET Framework support policy.

The WPF and WinForms packages target the .NET Framework versions and the Windows targets of .NET 8 to 11.

The MAUI packages start at .NET 10. They add Android, iOS, macOS, Mac Catalyst and tvOS targets. The Apple targets build only on Windows and macOS.

NativeAOT works on .NET 8 and later. Only the generated code runs there. The Unsafe overloads compile expressions at run time, and NativeAOT cannot do that.

Packages

Ten packages ship. Each runtime package carries the generator and the analyzer. The platform packages get them through the runtime package.

Package What it is
ReactiveUI.Binding The runtime library. It has lightweight observables and no System.Reactive dependency.
ReactiveUI.Binding.Reactive The same library, built against System.Reactive's IScheduler.
ReactiveUI.Binding.Wpf WPF dependency-property observation.
ReactiveUI.Binding.Wpf.Reactive The same, for a System.Reactive app.
ReactiveUI.Binding.WinForms WinForms component observation.
ReactiveUI.Binding.WinForms.Reactive The same, for a System.Reactive app.
ReactiveUI.Binding.Maui MAUI bindable-property observation.
ReactiveUI.Binding.Maui.Reactive The same, for a System.Reactive app.
ReactiveUI.Binding.SourceGenerators MSBuild props and targets only. A compatibility package.
ReactiveUI.Binding.Analyzer The analyzer project. Its files ship inside the runtime packages.

Supported APIs

API What it does Properties per call
WhenChanged Observes a property after it changes. 16
WhenChanging Observes a property before it changes. 16
WhenAnyValue Observes a property after it changes, like WhenChanged. 16
WhenAny Observes properties and hands each change to a selector. 12
WhenAnyObservable Observes properties that hold observables, and switches between them. 12
WhenAnyDynamic Observes a path you built as an Expression. 12
BindOneWay Writes a source property to a target property. 1 each side
BindTwoWay Carries a property both ways. 1 each side
OneWayBind A one-way binding, written view first. 1 each side
Bind A two-way binding, written view first. 1 each side
BindTo Writes an observable's values to a target property. 1
BindCommand Binds a command to a control's event. 1
BindInteraction Registers a handler for an interaction a property holds. 1
InvokeCommand Runs a command with each value an observable produces. 1

Each of them reads a single property, a path such as x => x.Address.City, or several properties at once. When an object in the middle of a path is replaced, the subscription moves to the new object.

BindOneWay, BindTwoWay, OneWayBind and Bind also accept a scheduler. The observation methods do not.

Examples

Observing a property

// One property.
IObservable<string> name = vm.WhenChanged(x => x.Name);

// A path. Replacing Address moves the subscription to the new Address.
IObservable<string> city = vm.WhenChanged(x => x.Address.City);

// Several properties, combined by a selector.
IObservable<string> fullName = vm.WhenChanged(
    x => x.FirstName,
    x => x.LastName,
    (first, last) => $"{first} {last}");

// Before the change. The type has to raise PropertyChanging.
IObservable<string> before = vm.WhenChanging(x => x.Name);

Binding

// One way.
IDisposable binding = vm.BindOneWay(view, x => x.Name, x => x.NameLabel);

// One way, converting on the way through.
IDisposable converted = vm.BindOneWay(view, x => x.Age, x => x.AgeLabel, age => $"Age: {age}");

// Both ways.
IDisposable twoWay = vm.BindTwoWay(view, x => x.Name, x => x.NameTextBox);

// Both ways, with a converter per direction.
IDisposable pair = vm.BindTwoWay(
    view,
    x => x.Age,
    x => x.AgeTextBox,
    age => age.ToString(),
    text => int.TryParse(text, out var n) ? n : 0);

// On a scheduler.
IDisposable scheduled = vm.BindOneWay(
    view, x => x.Name, x => x.NameLabel, scheduler: RxApp.MainThreadScheduler);

OneWayBind and Bind are the same bindings, written with the view first:

IDisposable oneWay = view.OneWayBind(vm, x => x.Name, x => x.NameLabel);
IDisposable twoWay = view.Bind(vm, x => x.Name, x => x.NameTextBox);

Running a command

// The command lives on a view model property. Each value becomes the command parameter,
// and a value CanExecute refuses is dropped.
IDisposable invocation = searchText.InvokeCommand(vm, x => x.Search);

// The caller holds the command, so there is no property to observe.
IDisposable direct = searchText.InvokeCommand(vm.Search);

A ReactiveCommand works like any other ICommand. The parameter arrives as object, not as the command's declared input type.

Observing a path built at run time

WhenAnyDynamic takes the path as an Expression, not a lambda. So you can build the path from a property name or a setting. It observes 1 to 12 paths at once. Each count has an overload that reports only changed values, and one that reports every value.

Expression chain = ((Expression<Func<MyViewModel, string>>)(x => x.Address.City)).Body;

IObservable<string?> city = vm.WhenAnyDynamic(chain, static c => (string?)c.Value);

The generator cannot read an expression built at run time. So WhenAnyDynamic walks the path by reflection. Every overload carries [RequiresUnreferencedCode], so a PublishAot build reports each call. When you know the path before you build, WhenChanged and WhenAny observe the same thing without reflection.

The view locator

A view locator finds the view for a view model. Implement IViewFor<T>, and the generator registers the view for you.

public class LoginView : IViewFor<LoginViewModel>
{
    public LoginViewModel ViewModel { get; set; }

    object IViewFor.ViewModel
    {
        get => ViewModel;
        set => ViewModel = (LoginViewModel)value;
    }
}

IViewFor? view = ViewLocator.GetCurrent().ResolveView(myViewModel);

Three attributes change what is registered.

Attribute Effect
[ViewContract("name")] Registers the view under a contract, so one view model can have several views. Pass the contract to ResolveView.
[SingleInstanceView] Keeps one instance instead of creating a view each time. Do not use it for a view that appears more than once in the tree.
[ExcludeFromViewRegistration] Leaves the view out of the generated registration.
[ViewContract("compact")]
public class CompactDashboardView : IViewFor<DashboardViewModel> { }

public class FullDashboardView : IViewFor<DashboardViewModel> { }

var compact = ViewLocator.GetCurrent().ResolveView(vm, "compact");
var full = ViewLocator.GetCurrent().ResolveView(vm);

ResolveView looks in three places, in order.

  1. The generated lookup. It is a type switch with no reflection.
  2. A mapping you added at run time with Map<TViewModel, TView>().
  3. The service locator, Splat.AppLocator.Current.

In the generated lookup, a view registered under the requested contract comes before the default view. The view instance comes from the first of these that has one.

  1. The service locator.
  2. The cached instance, when the view is marked [SingleInstanceView]. The first resolution creates it.
  3. A new instance from the view's parameterless constructor.

The cache is set with Interlocked.CompareExchange. So two threads resolving at once share one instance.

Use the generic ResolveView overload where you can. The object-typed overload closes IViewFor<> over a type known only at run time. So it carries [RequiresDynamicCode] and is not safe for ahead-of-time publishing.

Which mechanism wins

Each mechanism has a score. The highest-scoring mechanism that can reach the property you named wins.

Mechanism Type it keys on Score
UIKit control notifications Supported text, selection, date and switch properties 30
UIKit value changes UIKit.UIControl.Value with ValueChanged 20
AppKit control notifications Supported AppKit.NSControl value properties 20
Apple KVO Foundation.NSObject 15
IReactiveObject ReactiveUI.IReactiveObject 10
WinForms component System.ComponentModel.Component 8
WinUI bindable property Microsoft.UI.Xaml.DependencyObject 6
Uno dependency property Windows.UI.Xaml.DependencyObject 6
INotifyPropertyChanged System.ComponentModel.INotifyPropertyChanged 5
Android view Android.Views.View 5
WPF dependency property System.Windows.DependencyObject 4
Plain property A readable property without notifications 1

A mechanism has to reach the property. A plain C# property on a dependency object is not a dependency property. A component property with no {PropertyName}Changed event has nothing to attach to. Both fall through to the next mechanism down.

A plain property emits its current value when you subscribe. It cannot report later changes.

INotifyPropertyChanged and Android.Views.View share a score. INotifyPropertyChanged wins that tie.

An ICreatesObservableForProperty you register yourself uses the same scores. It takes the property when it scores higher. The generated code wins a tie.

Call ReactiveUI.Binding.Fallback.ObservationAffinityChecker.Refresh() after changing observation-provider registrations so subsequent subscriptions use the updated registrations.

Platform adapters

Platform support uses the framework references in your application. No extra binding platform package or platform registration is needed for these APIs.

BindCommand supports Android Click, UIKit target/action touch handling, refresh-control ValueChanged, bar-button Clicked, and AppKit Target/Action. It follows command and control replacements, tracks streamed or property-based parameters, and detaches handlers when disposed. Registered command binders take over when their score exceeds the selected adapter's score.

Command mechanism Score
UIKit refresh control or bar button 10
UIKit touch target or Android click 9
Command and CommandParameter properties 5
AppKit target/action or an event with Enabled 4
An event without Enabled 3

Supplying toEvent selects that event instead of the control's default command mechanism.

BindTo and OneWayBind can populate WinForms panel and table-layout control collections from collections of derived controls. They update the existing collection, including a read-only Controls property. They suspend layout during the write and resume it even when a collection operation throws. The generated setter scores 10; a registered ISetMethodBindingConverter must score higher to replace it.

Generated conversions cover numeric, boolean, GUID, date and time strings; numeric nullable values; URIs; framework visibility enums; and Apple NSDate values. Visibility conversions honor the framework's inversion and hidden-value hints. Registered typed converters must beat the generated score. An explicit converter override takes precedence over conversion voting.

Which thread a binding writes on

A UI framework lets only one thread touch a view. That thread is the view's owning thread. A view model can raise a change on any thread. So a binding moves each write to the owning thread.

The binding asks the object it writes to. Each platform has its own check and its own way to queue work.

Target Check Queue
WPF DispatcherObject CheckAccess() Dispatcher.BeginInvoke
WinForms Control InvokeRequired Control.BeginInvoke
MAUI BindableObject Dispatcher.IsDispatchRequired Dispatcher.Dispatch

WPF can run several UI threads. Each window belongs to one of them. Asking the object sends each write to the right one.

The binding asks on every write.

  • A write on the owning thread runs straight away when no earlier write is waiting. Set a property on the UI thread, and the control has the new value on the next line.
  • A write from another thread waits for the owning thread. A write on the owning thread also waits while an earlier write is waiting.
  • Only the latest value waits. A newer change replaces the waiting one, so a burst of changes becomes one write.

Some objects have no owning thread. The binding writes to them straight away.

  • A frozen WPF Freezable.
  • A WinForms control with no window handle yet. Once the handle exists, writes go to the thread that created it.
  • A MAUI object with no dispatcher, such as a view in a unit test.
  • Any object that is not a WPF, WinForms or MAUI object, such as a plain view model.

Every binding API does this: BindOneWay, BindTwoWay, OneWayBind, Bind, BindTo, and BindCommand when it binds a new command to the control. Each Unsafe twin does the same through the registered invokers.

Invokers

An IViewThreadInvoker does the check and the queueing for one platform. Each platform package has a module that registers one: WpfBindingModule, WinFormsBindingModule or MauiBindingModule. You can register your own. An invoker you register is asked first.

A generated binding knows its target's type when it compiles. For a WPF, WinForms or MAUI target, it carries that platform's invoker. So it routes writes even when the platform module is not registered.

An Unsafe binding only finds its target's type while the app runs. It uses the registered invokers alone. Register the platform module when you use Unsafe bindings.

Choosing the thread yourself

Pass a scheduler to pick the thread for one binding: vm.BindOneWay(view, x => x.Name, x => x.NameLabel, scheduler: someScheduler). That binding skips the check.

To change it for every binding, set BindingSchedulers.MainThread, or call BindingSchedulers.UseSynchronizationContext(context). Only writes from another thread go through it. A write on the owning thread still runs straight away. So does a write to an object no invoker claims.

Rx library compatibility

Rx means the Reactive Extensions: libraries of operators for IObservable<T>. This library's observables and operators come from ReactiveUI.Primitives. ReactiveUI.Binding depends on no other Rx library.

ReactiveUI.Binding.Reactive uses System.Reactive through ReactiveUI.Primitives.Reactive. Neither package references System.Reactive directly.

Package Depends on Scheduler type
ReactiveUI.Binding ReactiveUI.Primitives ISequencer
ReactiveUI.Binding.Reactive ReactiveUI.Primitives.Reactive IScheduler

Every binding returns a standard .NET IObservable<T>. So you are not tied to either library.

Library How it works
ReactiveUI.Primitives The default. Reference ReactiveUI.Binding.
System.Reactive Reference ReactiveUI.Binding.Reactive.
R3 R3 has its own Observable<T> class. Convert with .ToObservable().
Anything else Any library that accepts IObservable<T> works as it is.

Performance

On .NET 10, a thousand property changes through a two-way binding take 53.6 us and allocate 42.2 KB. Observing one property takes 79.5 us and allocates 65.5 KB. Published ahead of time, the same code runs within a few per cent of the JIT build.

src/benchmarks/README.md lists the benchmarks, the machine they ran on, what each one covers, and how to run them.

Diagnostics

The analyzer ships inside the runtime packages. It reports these diagnostics.

ID Severity What it means
RXUIBIND001 Info The expression is not an inline lambda, so nothing is generated. Call the Unsafe overload, or the call will throw.
RXUIBIND002 Warning The type has no observable property and raises no change event.
RXUIBIND003 Warning The expression reads a private or protected member. A generated extension method cannot read it.
RXUIBIND004 Warning The type raises no before-change event. WhenChanging reads the value once and then stays silent.
RXUIBIND005 Info The source implements INotifyDataErrorInfo. No code is generated for validation state.
RXUIBIND006 Warning The path contains an indexer, a field or a method call. Only property reads are generated.
RXUIBIND007 Warning The control named by BindCommand has no default event to bind. Pass toEvent.
RXUIBIND008 Warning The property named by BindInteraction does not implement IInteraction<TInput, TOutput>.
RXUIBIND009 Warning The generated overload cannot be reached from this file, so the call throws. Not reported when an interceptor takes the call.
RXUIBIND010 Warning The path passes through a type that raises no change event. The value is read once, and the path is followed no further.
RXUIBIND011 Warning The call resolved to ReactiveUI's own extension method. Nothing is generated, and the call uses reflection.

The package's build targets report one error of their own.

ID What it means
RXUIBIND100 The compiler is older than Roslyn 4.8, so no generator would load. Upgrade to Visual Studio 2022 17.8 or the .NET 8.0.100 SDK.

Where this differs from ReactiveUI

The sections below list every way a binding here behaves differently from ReactiveUI.

Bindings are generated, not reflected

ReactiveUI does the generator's job while the app runs. It compiles the lambda into a delegate, then finds each property by name. That costs time on every binding. A trimmer cannot see which members those lookups need, so it may remove them.

The generated code is faster and allocates less. On .NET 10, a thousand property changes through a two-way binding take 53.6 us and 42.2 KB here. ReactiveUI's engine takes 686.5 us and 932.3 KB. Observing one property takes 79.5 us and 65.5 KB here, against 145.6 us and 105.5 KB. ReactiveUI's engine compiles expressions at run time, so it cannot run under NativeAOT.

ReactiveUI's method names are kept

WhenAnyValue, OneWayBind and Bind use ReactiveUI's names. They make code written for ReactiveUI easier to move across.

Observation delivery is serialized

Property observations deliver on the raising thread when the delivery gate is available. A competing producer waits up to 20 ms, then hands its notification to the delivering thread. Subscriber calls do not hold a lock, so a subscriber can wait for another thread to update the same property. Observation APIs leave scheduling to the caller.

After-change and custom-provider observations read inside the gate and collapse contended changes to the latest value. A change raised by the subscriber is delivered after that subscriber returns. Before-change observations capture values before the write and preserve their order; a nested notification on the delivering thread reaches the subscriber before the nested setter writes. Disposal stops pending delivery.

TriggerUpdate and signalViewUpdate use BindUnsafe

The view-first BindUnsafe overloads accept an update stream and TriggerUpdate, with either registered converters or explicit conversion delegates. These overloads resolve property paths by reflection and carry RequiresUnreferencedCode; calling them produces a trimming diagnostic. Generated bindings use the two property lambdas and do not offer these parameters.

ViewToViewModel is the default. A supplied stream replaces the view's change notifications, so editing the view writes back only when the stream signals. With ViewModelToView, the stream drives model-to-view updates after the initial model notification, and the view's own notifications always participate. A null stream observes both properties in either mode; pass a typed null such as (IObservable<int>?)null for type inference.

Both sides are wired before a single initial signal writes from view model to view. Each signal reads the current values when delivered, converts in its selected direction, and skips a write when the converted value equals the destination. Disposing the binding disconnects both directions and the supplied stream.

Every binding writes on the thread that owns the view

ReactiveUI moves a write to the UI thread only on WPF. It does so for a two-way Bind, and when it swaps a control's Command. Its other bindings write on the thread that raised the change. So do all its WinForms and MAUI bindings.

Here every binding moves its writes, on all three platforms. See Which thread a binding writes on. A background update that throws under ReactiveUI works here. An update from another thread that ReactiveUI applied at once arrives one turn of the message loop later.

Where ReactiveUI does move a write, the order is the same. A write on the owning thread runs straight away. A write from another thread goes through the main-thread scheduler. Set BindingSchedulers.MainThread to ReactiveUI's main-thread scheduler to use the same scheduler.

A burst of changes from another thread is handled differently. ReactiveUI's one-way bindings and BindTo write every value, on the thread that raised it. Its two-way Bind queues one signal per change and reads the current value when each signal runs. Here every binding writes only the latest value, once. A binding's change stream skips the values in between.

A binding made through a type parameter is not generated

A generated overload has to name the bound types. A call through a type parameter names none. The type is only known once code fills in the parameter. So the generator leaves those calls to the plain overload. A generic binding helper then compiles, but throws when it runs. An overload that named a type parameter would break your build instead. From the helper, call the Unsafe twin to bind by reflection.

A property with no change event is reported when you build

ReactiveUI logs a warning the first time it observes a property on a type that raises no change event. RXUIBIND010 reports the same thing when you build. A generator can only report it then. The observation behaves the same either way. The value is read when you subscribe; replacing that property cannot be detected.

A write that throws behaves the same

A write that throws is logged against the bound expression. It is rethrown as a TargetInvocationException only when it has an inner exception. The setter threw on the thread that raised the change, so no caller can catch it. Swallowing the exception would hide the failure.

Contribute

ReactiveUI.Binding.SourceGenerators uses an OSI-approved open source license. You can use and share it freely, including for commercial work. We value everyone who takes part. We would love to have you, even if you have never contributed to open source before.

Ways to help:

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
7.1.0 0 9/18/2026
7.0.1 81 9/14/2026
7.0.0 74 9/14/2026
6.0.0 79 9/14/2026
5.1.0 90 9/9/2026
5.0.0 108 9/6/2026
4.1.0 139 8/16/2026
4.0.0 212 8/2/2026
3.4.0 247 6/4/2026