Table of Contents

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

Getting Started

Features

Samples

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

  1. Use Virtualization: For large lists, use VirtualizingStackPanel
  2. Freeze Resources: Freeze brushes and other freezable resources
  3. Throttle Events: Use Throttle for high-frequency events
  4. Async Commands: Keep UI responsive with async operations
  5. 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

  1. Use ReactiveWindow/ReactiveUserControl: For proper lifetime management
  2. WhenActivated: Always wrap subscriptions
  3. ObserveOn MainThread: Before updating UI
  4. DisposeWith: Automatic cleanup
  5. 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
});

Community Resources