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
<PackageReference Include="ReactiveUI.Binding.Analyzer" Version="7.1.0" />
<PackageVersion Include="ReactiveUI.Binding.Analyzer" Version="7.1.0" />
<PackageReference Include="ReactiveUI.Binding.Analyzer" />
paket add ReactiveUI.Binding.Analyzer --version 7.1.0
#r "nuget: ReactiveUI.Binding.Analyzer, 7.1.0"
#:package ReactiveUI.Binding.Analyzer@7.1.0
#addin nuget:?package=ReactiveUI.Binding.Analyzer&version=7.1.0
#tool nuget:?package=ReactiveUI.Binding.Analyzer&version=7.1.0
<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
- The problem it solves
- Your first binding
- How a property reports a change
- The generator checks each property in a path
- Trimming and NativeAOT
- Compiler requirements
- When nothing claims the call
- Installing
- Supported frameworks
- Packages
- Supported APIs
- Examples
- The view locator
- Which mechanism wins
- Which thread a binding writes on
- Rx library compatibility
- Performance
- Diagnostics
- Where this differs from ReactiveUI
- Core team
- Contribute
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 builduses 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
ReactiveUIBindingUseInterceptorstofalseto use the overloads on a compiler that can intercept. - Set
ReactiveUIBindingEmitGeneratedCodeMarkerstofalseto 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.
WhenAnyDynamic. See Observing a path built at run time.- The object-typed
ResolveViewon the view locator. See The view locator.
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.
- The generated lookup. It is a type switch with no reflection.
- A mapping you added at run time with
Map<TViewModel, TView>(). - 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.
- The service locator.
- The cached instance, when the view is marked
[SingleInstanceView]. The first resolution creates it. - 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:
- Answer questions on GitHub Discussions
- Share what you know and teach other developers
- Improve the docs where something is missing or unclear.
- Contribute code.
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.