Windows Presentation Foundation (WPF)
Package Installation
Install the following packages for ReactiveUI with WPF:
<!-- In your WPF application project -->
<PackageReference Include="ReactiveUI.WPF" Version="*" />
<PackageReference Include="ReactiveUI.SourceGenerators" Version="*" PrivateAssets="all" />
<PackageReference Include="ReactiveMarbles.ObservableEvents.SourceGenerator" Version="*" PrivateAssets="all" />
<!-- In your shared .NET Standard library -->
<PackageReference Include="ReactiveUI" Version="*" />
<PackageReference Include="ReactiveUI.SourceGenerators" Version="*" PrivateAssets="all" />
<!-- In your test project -->
<PackageReference Include="ReactiveUI.Testing" Version="*" />
Recommended Project Structure
- MyCoolApp (netstandard/net8.0 library - shared code)
- MyCoolApp.WPF (WPF application)
- MyCoolApp.UnitTests (test project)
Framework Requirements
Ensure you are targeting at least .NET 8.0 with Windows 10.0.19041.0:
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<!-- Or for .NET 9/10 -->
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
Getting Started with RxAppBuilder (Recommended)
The modern way to initialize ReactiveUI in WPF uses RxAppBuilder for dependency injection and platform setup.
1. Configure App.xaml.cs with RxAppBuilder
using ReactiveUI;
using Splat;
using System.Reflection;
using System.Windows;
namespace MyCoolApp.WPF;
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// Initialize ReactiveUI with RxAppBuilder
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithWpf()
.WithViewsFromAssembly(Assembly.GetExecutingAssembly())
.WithRegistration(locator =>
{
// Register your services here
locator.RegisterLazySingleton<IScreen>(() => new MainViewModel());
locator.RegisterLazySingleton<INavigationService>(() => new NavigationService());
locator.RegisterLazySingleton<IDataService>(() => new DataService());
})
.BuildApp();
// Optional: Store schedulers for later use
// var mainScheduler = app.MainThreadScheduler;
// var taskScheduler = app.TaskpoolScheduler;
// Create and show main window
var mainWindow = new MainWindow();
mainWindow.Show();
}
}
2. Create ViewModels with SourceGenerators
Use ReactiveUI.SourceGenerators for cleaner, compile-time generated reactive properties:
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using System.Reactive.Linq;
namespace MyCoolApp.ViewModels;
public partial class MainViewModel : ReactiveObject
{
[Reactive]
private string _searchText = string.Empty;
[Reactive]
private string _statusMessage = string.Empty;
[ObservableAsProperty]
private bool _isBusy;
[ObservableAsProperty]
private List<SearchResult> _searchResults;
public MainViewModel()
{
// Create reactive commands with automatic CanExecute
SearchCommand = ReactiveCommand.CreateFromTask(
async () => await PerformSearchAsync(),
this.WhenAnyValue(x => x.SearchText, text => !string.IsNullOrWhiteSpace(text)));
ClearCommand = ReactiveCommand.Create(
() => SearchText = string.Empty);
// Wire up IsBusy from command execution
SearchCommand.IsExecuting
.ToProperty(this, x => x.IsBusy);
// Wire up search results
SearchCommand
.ToProperty(this, x => x.SearchResults);
// React to search text changes with debouncing
this.WhenAnyValue(x => x.SearchText)
.Throttle(TimeSpan.FromMilliseconds(500))
.Where(text => !string.IsNullOrWhiteSpace(text))
.InvokeCommand(SearchCommand);
// Handle errors
SearchCommand.ThrownExceptions
.Subscribe(ex => StatusMessage = $"Error: {ex.Message}");
}
[ReactiveCommand]
private async Task<List<SearchResult>> PerformSearchAsync()
{
StatusMessage = "Searching...";
// Simulate search operation
await Task.Delay(1000);
var results = new List<SearchResult>
{
new() { Title = SearchText, Description = "Sample result" }
};
StatusMessage = $"Found {results.Count} results";
return results;
}
public ReactiveCommand<Unit, List<SearchResult>> SearchCommand { get; }
public ReactiveCommand<Unit, Unit> ClearCommand { get; }
}
3. Create Views that Implement IViewFor
MainWindow.xaml:
<rxui:ReactiveWindow
x:Class="MyCoolApp.WPF.MainWindow"
x:TypeArguments="vm:MainViewModel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:rxui="http://reactiveui.net"
xmlns:vm="clr-namespace:MyCoolApp.ViewModels;assembly=MyCoolApp"
Title="My Cool App"
Height="450"
Width="800">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Search Input -->
<DockPanel Grid.Row="0" Margin="0,0,0,10">
<Button x:Name="ClearButton"
DockPanel.Dock="Right"
Content="Clear"
Width="75"
Margin="5,0,0,0"/>
<TextBox x:Name="SearchTextBox"
DockPanel.Dock="Left"/>
</DockPanel>
<!-- Busy Indicator -->
<StackPanel Grid.Row="1"
Orientation="Horizontal"
Margin="0,0,0,10"
x:Name="BusyPanel">
<TextBlock Text="Searching..." Margin="0,0,10,0"/>
<ProgressBar Width="100" Height="20" IsIndeterminate="True"/>
</StackPanel>
<!-- Results List -->
<ListBox x:Name="ResultsListBox"
Grid.Row="2"
Margin="0,0,0,10"/>
<!-- Status Bar -->
<TextBlock x:Name="StatusTextBlock"
Grid.Row="3"/>
</Grid>
</rxui:ReactiveWindow>
MainWindow.xaml.cs:
using ReactiveUI;
using Splat;
using System.Reactive.Disposables;
namespace MyCoolApp.WPF;
public partial class MainWindow : ReactiveWindow<MainViewModel>
{
public MainWindow()
{
InitializeComponent();
// Resolve ViewModel from DI container or create directly
ViewModel = AppLocator.Current.GetService<MainViewModel>() ?? new MainViewModel();
this.WhenActivated(disposables =>
{
// Two-way binding for search text
this.Bind(ViewModel,
vm => vm.SearchText,
v => v.SearchTextBox.Text)
.DisposeWith(disposables);
// Command bindings
this.BindCommand(ViewModel,
vm => vm.ClearCommand,
v => v.ClearButton)
.DisposeWith(disposables);
// One-way bindings
this.OneWayBind(ViewModel,
vm => vm.IsBusy,
v => v.BusyPanel.Visibility,
isBusy => isBusy ? Visibility.Visible : Visibility.Collapsed)
.DisposeWith(disposables);
this.OneWayBind(ViewModel,
vm => vm.SearchResults,
v => v.ResultsListBox.ItemsSource)
.DisposeWith(disposables);
this.OneWayBind(ViewModel,
vm => vm.StatusMessage,
v => v.StatusTextBlock.Text)
.DisposeWith(disposables);
});
}
}
Alternative: Traditional Setup (Legacy)
If you prefer not to use RxAppBuilder, you can initialize ReactiveUI manually:
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// Register views manually
Locator.CurrentMutable.Register(() => new MainWindow(), typeof(IViewFor<MainViewModel>));
// Register view models
Locator.CurrentMutable.RegisterLazySingleton(() => new MainViewModel(), typeof(MainViewModel));
var mainWindow = new MainWindow();
mainWindow.Show();
}
}
Creating UserControls
For reusable components, use ReactiveUserControl<TViewModel>:
SearchControl.xaml:
<rxui:ReactiveUserControl
x:Class="MyCoolApp.WPF.Controls.SearchControl"
x:TypeArguments="vm:SearchControlViewModel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:rxui="http://reactiveui.net"
xmlns:vm="clr-namespace:MyCoolApp.ViewModels;assembly=MyCoolApp">
<StackPanel>
<TextBox x:Name="QueryTextBox" Margin="0,0,0,5"/>
<Button x:Name="SearchButton" Content="Search"/>
</StackPanel>
</rxui:ReactiveUserControl>
SearchControl.xaml.cs:
using ReactiveUI;
namespace MyCoolApp.WPF.Controls;
public partial class SearchControl : ReactiveUserControl<SearchControlViewModel>
{
public SearchControl()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
this.Bind(ViewModel,
vm => vm.Query,
v => v.QueryTextBox.Text)
.DisposeWith(disposables);
this.BindCommand(ViewModel,
vm => vm.SearchCommand,
v => v.SearchButton)
.DisposeWith(disposables);
});
}
}
Dependency Injection with RxAppBuilder
RxAppBuilder integrates with Splat for dependency injection:
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithWpf()
.WithViewsFromAssembly(Assembly.GetExecutingAssembly())
.WithRegistration(locator =>
{
// Register services as singletons
locator.RegisterLazySingleton<IApiService>(() => new ApiService());
locator.RegisterLazySingleton<IDataRepository>(() => new DataRepository());
// Register view models (transient)
locator.Register<MainViewModel>(() => new MainViewModel());
// Register view models as singletons
locator.RegisterLazySingleton<SettingsViewModel>(() => new SettingsViewModel());
})
.BuildApp();
Then resolve services in your view models:
public MainViewModel()
{
var apiService = AppLocator.Current.GetService<IApiService>();
var repository = AppLocator.Current.GetService<IDataRepository>();
}
Routing in WPF
For navigation, use ReactiveUI's routing system with RoutedViewHost:
public class MainViewModel : ReactiveObject, IScreen
{
public RoutingState Router { get; }
public MainViewModel()
{
Router = new RoutingState();
// Navigate to the first page
Router.Navigate.Execute(new FirstViewModel(this)).Subscribe();
}
}
In your XAML:
<rxui:RoutedViewHost
x:Name="RoutedViewHost"
Router="{Binding Router}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"/>
See the Routing Guide for more details.
Key Points
- Use ReactiveWindow
or ReactiveUserControl base classes - Use ReactiveUI.SourceGenerators for cleaner property and command declarations
- Use RxAppBuilder for modern dependency injection and platform setup
- Always call DisposeWith(disposables) inside WhenActivated to prevent memory leaks
- Use ReactiveMarbles.ObservableEvents.SourceGenerator for converting WPF events to observables
Common Patterns
Value Converters in Bindings
this.OneWayBind(ViewModel,
vm => vm.IsEnabled,
v => v.MyButton.Visibility,
isEnabled => isEnabled ? Visibility.Visible : Visibility.Collapsed)
.DisposeWith(disposables);
Reactive Validation
using ReactiveUI.Validation.Extensions;
using ReactiveUI.Validation.Helpers;
public partial class LoginViewModel : ReactiveValidationObject
{
[Reactive]
private string _username = string.Empty;
[Reactive]
private string _password = string.Empty;
public LoginViewModel()
{
this.ValidationRule(
vm => vm.Username,
username => !string.IsNullOrWhiteSpace(username),
"Username is required");
this.ValidationRule(
vm => vm.Password,
password => password?.Length >= 6,
"Password must be at least 6 characters");
LoginCommand = ReactiveCommand.CreateFromTask(
async () => await LoginAsync(),
this.IsValid());
}
[ReactiveCommand]
private async Task LoginAsync() { /* ... */ }
}
Loading Data on Activation
public MainViewModel()
{
this.WhenActivated(disposables =>
{
// Load data when the view model is activated
LoadDataCommand.Execute().Subscribe().DisposeWith(disposables);
});
}