ReactiveUI for WPF
Windows Presentation Foundation (WPF) is Microsoft's rich desktop application framework for Windows. ReactiveUI provides comprehensive support for WPF with reactive MVVM patterns that make desktop development more productive and maintainable.
Why ReactiveUI with WPF?
- Reactive Data Binding: Powerful binding with WhenAnyValue
- Dispatcher Integration: Automatic thread marshaling
- Command Support: ReactiveCommand with async/await
- View Activation: Lifecycle management for subscriptions
- Design-Time Support: ViewModel design data support
- Testable: Full unit testing without UI dependencies
Quick Links
Getting Started
Features
Samples
- WPF Samples Repository
- GitHub for Visual Studio (Production App)
Platform-Specific Features
ReactiveWindow
public partial class MainWindow : ReactiveWindow<MainViewModel>
{
public MainWindow()
{
InitializeComponent();
ViewModel = new MainViewModel();
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.Name, v => v.NameTextBox.Text)
.DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCommand, v => v.SaveButton)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.IsLoading, v => v.LoadingIndicator.Visibility)
.DisposeWith(disposables);
});
}
}
Dispatcher Scheduler
ReactiveUI automatically uses the Dispatcher:
// Automatically marshals to UI thread
Observable.Timer(TimeSpan.FromSeconds(1))
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(_ => UpdateUI());
Dependency Properties
Bind to WPF dependency properties:
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.Value, v => v.Slider.Value)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.Text, v => v.TextBlock.Text)
.DisposeWith(disposables);
});
Common Patterns
ReactiveUserControl
public partial class SearchControl : ReactiveUserControl<SearchViewModel>
{
public SearchControl()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.SearchText, v => v.SearchBox.Text)
.DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SearchCommand, v => v.SearchButton)
.DisposeWith(disposables);
});
}
}
ObservableEvents
Convert WPF events to observables:
using ReactiveMarbles.ObservableEvents;
this.WhenActivated(disposables =>
{
// Mouse events
this.Events().MouseMove
.Throttle(TimeSpan.FromMilliseconds(100))
.Subscribe(e => UpdateMousePosition(e.GetPosition(this)))
.DisposeWith(disposables);
// Window events
this.Events().Closing
.Subscribe(e =>
{
if (HasUnsavedChanges)
{
e.Cancel = !ConfirmClose();
}
})
.DisposeWith(disposables);
});
Validation
public partial class EditViewModel : ReactiveValidationObject
{
[Reactive]
private string _email = string.Empty;
[Reactive]
private string _password = string.Empty;
public EditViewModel()
{
this.ValidationRule(
vm => vm.Email,
email => email?.Contains("@") == true,
"Please enter a valid email");
this.ValidationRule(
vm => vm.Password,
password => password?.Length >= 6,
"Password must be at least 6 characters");
SaveCommand = ReactiveCommand.CreateFromTask(
SaveAsync,
this.IsValid());
}
[ReactiveCommand]
private async Task Save() => await SaveAsync();
}
MVVM Patterns
View-ViewModel Connection
XAML:
<Window x:Class="MyApp.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:MyApp.ViewModels"
Title="My App" Height="450" Width="800">
<Grid>
<TextBox x:Name="NameTextBox" />
<Button x:Name="SaveButton" Content="Save" />
</Grid>
</Window>
Code-Behind:
public partial class MainWindow : ReactiveWindow<MainViewModel>
{
public MainWindow()
{
InitializeComponent();
ViewModel = new MainViewModel();
}
}
Design-Time Data
public class DesignTimeMainViewModel : MainViewModel
{
public DesignTimeMainViewModel()
{
Name = "Design Time Name";
Items = new ObservableCollection<string> { "Item 1", "Item 2" };
}
}
In XAML:
<Window xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:MyApp.ViewModels"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=vm:DesignTimeMainViewModel, IsDesignTimeCreatable=True}">
Advanced Features
Custom Controls
public class ReactiveButton : Button, IViewFor<ButtonViewModel>
{
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register(
nameof(ViewModel),
typeof(ButtonViewModel),
typeof(ReactiveButton),
new PropertyMetadata(null));
public ButtonViewModel ViewModel
{
get => (ButtonViewModel)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
object IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = value as ButtonViewModel;
}
}
Attached Behaviors
public static class TextBoxBehavior
{
public static readonly DependencyProperty SelectAllOnFocusProperty =
DependencyProperty.RegisterAttached(
"SelectAllOnFocus",
typeof(bool),
typeof(TextBoxBehavior),
new PropertyMetadata(false, OnSelectAllOnFocusChanged));
public static bool GetSelectAllOnFocus(DependencyObject obj) =>
(bool)obj.GetValue(SelectAllOnFocusProperty);
public static void SetSelectAllOnFocus(DependencyObject obj, bool value) =>
obj.SetValue(SelectAllOnFocusProperty, value);
private static void OnSelectAllOnFocusChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
if (d is TextBox textBox && (bool)e.NewValue)
{
textBox.Events().GotFocus
.Subscribe(_ => textBox.SelectAll());
}
}
}
Collections with DynamicData
public partial class ItemListViewModel : ReactiveObject
{
private readonly SourceCache<Item, int> _itemsCache;
private readonly ReadOnlyObservableCollection<ItemViewModel> _items;
public ReadOnlyObservableCollection<ItemViewModel> Items => _items;
public ItemListViewModel()
{
_itemsCache = new SourceCache<Item, int>(x => x.Id);
_itemsCache.Connect()
.Transform(item => new ItemViewModel(item))
.Filter(vm => vm.IsVisible)
.Sort(SortExpressionComparer<ItemViewModel>.Ascending(x => x.Name))
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Bind(out _items)
.Subscribe();
}
}
Performance Tips
- Use Virtualization: For large lists, use VirtualizingStackPanel
- Freeze Resources: Freeze brushes and other freezable resources
- Throttle Events: Use Throttle for high-frequency events
- Async Commands: Keep UI responsive with async operations
- Proper Disposal: Always use WhenActivated
Testing
[WpfFact]
public async Task MainWindow_LoadsData()
{
// Arrange
var window = new MainWindow();
window.ViewModel = new MainViewModel();
// Act
await window.ViewModel.LoadCommand.Execute();
// Assert
window.ViewModel.Data.Should().NotBeNull();
}
Styling
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="{StaticResource PrimaryBrush}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="10,5" />
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource PrimaryDarkBrush}" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
Best Practices
- Use ReactiveWindow/ReactiveUserControl: For proper lifetime management
- WhenActivated: Always wrap subscriptions
- ObserveOn MainThread: Before updating UI
- DisposeWith: Automatic cleanup
- Design-Time Data: Better design experience
Common Issues
Issue: UI Not Updating
Solution: Ensure ObserveOn(RxSchedulers.MainThreadScheduler)
backgroundOperation
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(result => UpdateUI(result));
Issue: Memory Leaks
Solution: Always use WhenActivated
this.WhenActivated(disposables =>
{
// All subscriptions here are automatically disposed
});