Skip to content

WPF

Run the complete page example.

ReactiveUI.WPF connects ReactiveUI to Windows Presentation Foundation. It gives your windows, pages and user controls a ViewModel property WPF can bind to. It adds a host that shows the current page of a router, and a way to preview a view model without navigating to it. It also adds the main-thread sequencer that delivers results onto WPF's dispatcher. The example below is a small grade book: a window lists every student, and opening one navigates to a page where you can edit their grade. Add the package by following Installation. ReactiveUI.WPF also ships as ReactiveUI.WPF.Reactive, built from the same source, for apps that use System.Reactive.

The example project builds on any operating system, but a WPF app only runs on Windows. Headings below that show --smoke output ran the compiled app on a real Windows machine, driving the window the way a person would: opening a student, typing a grade, and going back.

Configure ReactiveUI for WPF

1. Call WithWpf on the app builder. RxAppBuilder creates the builder; WithWpf registers the WPF view hosts, the activation fetcher and the dispatcher-backed main-thread sequencer, then BuildApp applies everything. Do this once, in your Application's startup.

_ = RxAppBuilder.CreateReactiveUIBuilder().WithWpf().BuildApp();

2. Wire up automatic state saving. AutoSuspendHelper watches the WPF Application for you: it forwards Startup, Activated, Deactivated and Exit into ReactiveUI's suspension host. IdleTimeout is how long the app waits, after losing focus, before it asks you to save. See Data Persistence for the suspension host itself.

AutoSuspendHelper autoSuspendHelper = new(this) { IdleTimeout = IdleTimeout };
Console.WriteLine($"Auto-suspend idle timeout: {autoSuspendHelper.IdleTimeout}");
RxSuspension.SuspensionHost.CreateNewAppState = static () => new GradeBookState();
_persistSubscription = RxSuspension.SuspensionHost.ShouldPersistState.Subscribe(static token =>
{
    Console.WriteLine("Saving grade book state...");
    token.Dispose();
});
Auto-suspend idle timeout: 00:00:20

3. Show the main window. Everything below builds on this window and the pages it navigates to.

MainWindow = new MainWindow();
MainWindow.Show();

Show the current page in a window

A router needs a host that watches its stack and swaps in whatever view is current. RoutedViewHost is that host for WPF. Put one in your window and give it a Router and a ViewLocator. It shows the view for whichever page is on top of the stack, and shows DefaultContent instead when the stack is empty. RoutedViewHost derives from TransitioningContentControl, so a change of content plays an animation instead of popping in. Transition picks the animation, Direction and Duration shape it, and TransitionStarted/TransitionCompleted fire around it.

The window itself is a ReactiveWindow<TViewModel>, a Window that implements IViewFor<TViewModel>: it has a ViewModel property WPF can bind to, and BindingRoot exposes the same view model under the name every ReactiveUI view uses. The window's XAML names the generic argument with x:TypeArguments.

<reactiveui:ReactiveWindow
    x:Class="ReactiveUI.Documentation.PlatformWpf.MainWindow"
    x:TypeArguments="local:AppShell"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:reactiveui="http://reactiveui.net"
    xmlns:local="clr-namespace:ReactiveUI.Documentation.PlatformWpf"
    Title="Grade Book"
    Width="640"
    Height="360"
    WindowStartupLocation="CenterScreen">

    <DockPanel>
        <Button x:Name="AboutButton" DockPanel.Dock="Top" Content="About" HorizontalAlignment="Right" Margin="8" />
        <StackPanel DockPanel.Dock="Bottom" Margin="8">
            <reactiveui:RoutedViewHostUnsafe x:Name="NoticeBoardHost" />
            <reactiveui:ViewModelViewHostUnsafe x:Name="LatestNoticeHost" />
        </StackPanel>
        <reactiveui:RoutedViewHost x:Name="Host" />
    </DockPanel>

</reactiveui:ReactiveWindow>

ViewContractObservable is the stream a host reads its ViewContract from; setting ViewContract itself is really setting this to a stream of one value. An app whose layout never changes with device orientation can replace the default, orientation-built stream with a fixed one:

Host.ViewContractObservable = Signal.Emit<string?>("desktop");
Console.WriteLine($"Host view contract observable set: {Host.GetValue(RoutedViewHost.ViewContractObservableProperty) is not null}");

The two Unsafe hosts in the bottom panel show the school office's notices. The service locator section covers them.

The code-behind sets Transition, Direction, Duration and DefaultContent, then assigns Router and ViewLocator inside WhenActivated so the host attaches only while the window is on screen. See When Activated for the activation lifetime. Once Router is set, navigating to a page is one call to Router.Navigate.Execute; Routing covers the router itself.

Host.Transition = TransitioningContentControl.TransitionType.Slide;
Host.Direction = TransitioningContentControl.TransitionDirection.Left;
Host.Duration = TimeSpan.FromMilliseconds(250);
Host.DefaultContent = "Pick a student to begin.";
Host.TransitionStarted += static (_, _) => Console.WriteLine("Transition started.");
Host.TransitionCompleted += static (_, _) => Console.WriteLine("Transition completed.");
CourseListViewModel courseList = new(ViewModel, courses);

// The "d(...)" style registers one disposable at a time, rather than collecting them into a
// MultipleDisposable first; RoutedViewHost's own constructor uses the same style internally.
_ = this.WhenActivated(d =>
{
    Host.Router = ViewModel!.Router;
    Host.ViewLocator = ViewLocator.GetCurrent();
    d(ViewModel.Router.Navigate.Execute(courseList).Subscribe());
    Console.WriteLine($"View contract: {Host.ViewContract ?? "(none)"}");

    // BindingRoot is the same view model as ViewModel, exposed under the name every ReactiveUI view uses.
    Console.WriteLine($"BindingRoot's router has {BindingRoot?.Router.NavigationStack.Count} page(s) on screen.");
});
View contract: (none)
BindingRoot's router has 1 page(s) on screen.

On real Windows, navigating to the course list and then to a student's grade page plays a transition each time:

Transition started.
...
Transition completed.

GetIsDesignMode guards constructors like this one: WPF's designer surface constructs every view in a project to lay it out, and a router with nothing to navigate to would throw in that environment. The constructor returns before touching the router when the view is running under the designer.

if (this.GetIsDesignMode())
{
    return;
}
%%{init: {"theme": "base", "themeVariables": {"fontFamily": "Roboto, Helvetica, Arial, sans-serif", "fontSize": "15px", "primaryColor": "#DCE9FF", "primaryBorderColor": "#6C8EC4", "primaryTextColor": "#0B2447", "secondaryColor": "#E3F2E8", "secondaryBorderColor": "#7FA88C", "secondaryTextColor": "#12301C", "tertiaryColor": "#F3E5F5", "tertiaryBorderColor": "#A98BB0", "tertiaryTextColor": "#2E1437", "lineColor": "#7B8699", "textColor": "#1B1F27", "noteBkgColor": "#FFF4D6", "noteBorderColor": "#C9A94F", "noteTextColor": "#3A2A00", "actorBkg": "#DCE9FF", "actorBorder": "#6C8EC4", "actorTextColor": "#0B2447", "signalColor": "#7B8699", "signalTextColor": "#1B1F27", "labelBoxBkgColor": "#F1F3F8", "labelBoxBorderColor": "#A7AEBB", "edgeLabelBackground": "#F7F9FC", "clusterBkg": "#F7F9FC", "clusterBorder": "#C9D1DE"}}}%%
flowchart LR
    classDef view fill:#DCE9FF,stroke:#6C8EC4,color:#0B2447
    classDef vm fill:#E3F2E8,stroke:#7FA88C,color:#12301C
    MainWindow(["MainWindow"]):::view -- "hosts" --> Host(["RoutedViewHost"]):::view
    Host -- "watches" --> Router(["Router"]):::vm
    Router -- "current page" --> PageView(["Course list / student detail view"]):::view
    PageView -- "SelectedStudent" --> SummaryHost(["ViewModelViewHost"]):::view
    SummaryHost -- "shows" --> SummaryView(["Student summary view"]):::view

RoutedViewHost swaps the page view as the router's stack changes; the course list also hosts a ViewModelViewHost of its own, shown next.

Bind a view to its view model

A page like CourseListView is a ReactiveUserControl<TViewModel>: a UserControl that is also an IViewFor<TViewModel>, with the same ViewModel and BindingRoot shape as ReactiveWindow<TViewModel>.

<reactiveui:ReactiveUserControl
    x:Class="ReactiveUI.Documentation.PlatformWpf.CourseListView"
    x:TypeArguments="local:CourseListViewModel"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:reactiveui="http://reactiveui.net"
    xmlns:local="clr-namespace:ReactiveUI.Documentation.PlatformWpf">

    <DockPanel Margin="16">
        <TextBlock DockPanel.Dock="Top" Text="Students" FontSize="18" Margin="0,0,0,8" />

        <StackPanel DockPanel.Dock="Right" Width="180" Margin="16,0,0,0">
            <TextBlock Text="Selected student" FontWeight="Bold" Margin="0,0,0,8" />
            <local:LoggingViewModelViewHost x:Name="SummaryHost" />
            <Button x:Name="OpenButton" Content="Open grade" Margin="0,16,0,0" />
        </StackPanel>

        <ListBox x:Name="StudentList" DisplayMemberPath="Name" />
    </DockPanel>

</reactiveui:ReactiveUserControl>

Its code-behind binds inside WhenActivated, using the Action<Action<IDisposable>> overload: d registers one subscription at a time. OneWayBind, Bind and BindCommand are the same binding methods every ReactiveUI platform uses; Bindings covers them, and Bind both ways covers the two-way case Bind handles here.

_ = this.WhenActivated(d =>
{
    SummaryHost.ViewLocator = ViewLocator.GetCurrent();
    _ = this.OneWayBind(ViewModel, vm => vm.Students, v => v.StudentList.ItemsSource)
        .DisposeWith(d);
    _ = this.Bind(ViewModel, vm => vm.SelectedStudent, v => v.StudentList.SelectedItem)
        .DisposeWith(d);
    _ = this.OneWayBind(ViewModel, vm => vm.Summary, v => v.SummaryHost.ViewModel)
        .DisposeWith(d);
    _ = this.BindCommand(
            ViewModel,
            vm => vm.OpenStudent,
            v => v.OpenButton,
            this.WhenAnyValue(v => v.ViewModel!.SelectedStudent).WhereNotNull())
        .DisposeWith(d);
});

SummaryHost is a second host, of a different kind: a ViewModelViewHost needs no router, only a ViewModel to show. Unlike Host, it shows nothing until a student is selected, so it gets its own DefaultContent. It also gets its own Transition and Direction: a preview panel changing next to the list reads better as a small upward move than the full-width slide Host uses.

SummaryHost.DefaultContent = "Select a student to preview their grade.";
SummaryHost.Transition = TransitioningContentControl.TransitionType.Move;
SummaryHost.Direction = TransitioningContentControl.TransitionDirection.Up;

ContractFallbackByPass stops the host from falling back to an uncontracted view when a view under the current contract is missing. ViewContractObservable is the stream ViewContract itself is built from. Host uses the same pair above, on RoutedViewHost instead of ViewModelViewHost. Reading each one back through its dependency property field, rather than the wrapper property, confirms both are backed by the same storage:

SummaryHost.ContractFallbackByPass = true;
Console.WriteLine($"Contract fallback bypass (via field): {(bool)SummaryHost.GetValue(ViewModelViewHost.ContractFallbackByPassProperty)}");

SummaryHost.ViewContractObservable = Signal.Emit<string?>(null);
Console.WriteLine($"Summary host contract observable set: {SummaryHost.GetValue(ViewModelViewHost.ViewContractObservableProperty) is not null}");

SummaryHost is a LoggingViewModelViewHost, not a plain ViewModelViewHost: it overrides the protected ResolveViewForViewModel(object?, string?), the method a host calls every time its ViewModel or view contract changes, to log the view it resolved after calling the base implementation.

public sealed class LoggingViewModelViewHost : ViewModelViewHost
{
    /// <inheritdoc/>
    protected override void ResolveViewForViewModel(object? viewModel, string? contract)
    {
        base.ResolveViewForViewModel(viewModel, contract);
        Console.WriteLine($"Summary panel resolved: {(viewModel is null ? "(none)" : Content?.GetType().Name)}");
    }
}

A view can build its bindings and return them instead of registering each one through d. StudentSummaryView uses the Func<IEnumerable<IDisposable>> overload: WhenActivated disposes every item in the list when the view deactivates.

_ = this.WhenActivated(CreateBindings);
private IEnumerable<IDisposable> CreateBindings() =>
[
    this.OneWayBind(ViewModel, vm => vm.Student.Name, v => v.NameText.Text),
    this.OneWayBind(ViewModel, vm => vm.Student.Grade, v => v.GradeText.Text, static grade => $"Grade: {grade}")
];

Preview a view model in place

ViewModelViewHost shows whichever view model its own ViewModel property holds, resolving the matching view the same way RoutedViewHost does. Unlike RoutedViewHost, it needs no router: bind its ViewModel to any property and it swaps views as that property changes. The course list above binds SummaryHost.ViewModel to vm.Summary, a StudentSummaryViewModel. That view model never joins the router's stack; the list shows it as a preview next to itself, not as a separate page.

Both hosts ask the view locator for a view in two steps, and neither step uses reflection. The first step is the view lookup the ReactiveUI.Binding source generator writes for every view class in your project that implements IViewFor<T>. The second is the views you add to the view locator with Map. So both hosts are safe to trim and to compile ahead of time, and StudentSummaryView needs no registration at all.

Show a view only the service locator knows

A view registered only in Splat's service locator, whose class the source generator never sees, sits outside both steps. One example is a view from a library built without the generator. Each default type has an Unsafe twin for that view. A twin asks both steps first, then the service locator for IViewFor<T> closed over the view model's run-time type. Building that type while the app runs needs code the compiler never generated, so each twin is marked [RequiresDynamicCode].

AOT-safe typeUnsafe twinWhen the view is only in the service locator
RoutedViewHostRoutedViewHostUnsafeThe safe host throws an InvalidOperationException that names the twin.
ViewModelViewHostViewModelViewHostUnsafeThe safe host logs a warning that names the twin and shows DefaultContent.
AutoDataTemplateBindingHookAutoDataTemplateBindingHookUnsafeEach item's ViewModelViewHost shows nothing.

Prefer to bridge the registration instead: locator.CreateMappingBuilder().MapFromServiceLocator<TViewModel, IViewFor<TViewModel>>() adds a Map entry whose view comes from the service locator, so the default hosts find it and the app stays safe to compile ahead of time. WinUI shows it end to end. When the service locator holds more than one registration of the same IViewFor<TViewModel> type, the contracted overload tells them apart.

1. Register the view with the service locator. The school office's OfficeNoticeView stands in for a view from the office's shared library. [ExcludeFromViewRegistration] keeps it out of the generated lookup, and startup registers it with Splat only:

// The school office's library registers its view with the service locator only, so the window's notice
// board shows it through the Unsafe twins.
AppLocator.CurrentMutable.Register<IViewFor<OfficeNoticeViewModel>>(static () => new OfficeNoticeView());

2. Host it with the Unsafe twins. RoutedViewHostUnsafe and ViewModelViewHostUnsafe derive from the default hosts, so they take the same Router and ViewModel properties. The window's XAML above declares both. The code-behind gives the routed twin a router of its own and shows the latest notice in the other:

// OfficeNoticeView is registered only with the service locator, so the notice board uses the Unsafe twins.
NoticeBoard noticeBoard = new();
NoticeBoardHost.Router = noticeBoard.Router;
LatestNoticeHost.ViewModel = new OfficeNoticeViewModel(noticeBoard, "Reports are due on Friday.");

Inside WhenActivated, the window pins a notice to the board:

d(noticeBoard.Router.Navigate.Execute(new OfficeNoticeViewModel(noticeBoard, "Parent evening is on Tuesday."))
    .Subscribe());

The --smoke run prints what each twin shows:

RoutedViewHostUnsafe shows: OfficeNoticeView (Parent evening is on Tuesday.)
ViewModelViewHostUnsafe shows: OfficeNoticeView (Reports are due on Friday.)

3. Register AutoDataTemplateBindingHookUnsafe for lists of those views. Add it next to the safe hook that WithWpf registers. It replaces only the template the safe hook assigned, so the registration order does not matter. The example registers it on a resolver of its own, so the grade book keeps the safe template:

using ModernDependencyResolver resolver = new();
_ = resolver.CreateReactiveUIBuilder()
    .WithWpf()
    .WithRegistration(static registrar => registrar.RegisterConstant<IPropertyBindingHook>(new AutoDataTemplateBindingHookUnsafe()));

foreach (IPropertyBindingHook hook in resolver.GetServices<IPropertyBindingHook>())
{
    Console.WriteLine($"Binding hook: {hook.GetType().Name}");
}
Binding hook: AutoDataTemplateBindingHook
Binding hook: AutoDataTemplateBindingHookUnsafe

In your own app, add the same WithRegistration call to the RxAppBuilder.CreateReactiveUIBuilder() chain, before BuildApp().

Each hook's DefaultItemTemplate is the DataTemplate it assigns; an app can inspect or reuse it directly instead of registering the hook.

DataTemplate template = AutoDataTemplateBindingHook.DefaultItemTemplate.Value;
DependencyObject root = template.LoadContent();
Console.WriteLine($"Default item template hosts each item in a: {root.GetType().Name}");

DataTemplate unsafeTemplate = AutoDataTemplateBindingHookUnsafe.DefaultItemTemplate.Value;
DependencyObject unsafeRoot = unsafeTemplate.LoadContent();
Console.WriteLine($"Unsafe default item template hosts each item in a: {unsafeRoot.GetType().Name}");
Default item template hosts each item in a: ViewModelViewHost
Unsafe default item template hosts each item in a: ViewModelViewHostUnsafe

Every transition, and a view that isn't an IViewFor

TransitioningContentControl supports five transitions (Fade, Move, Slide, Drop, Bounce) and four directions (Up, Down, Left, Right). Host and SummaryHost above use Slide/Left and Move/Up. NoticeBoardHost and LatestNoticeHost pick two more. A page-name banner built entirely in code picks the last one. It sets its direction and duration through the raw dependency properties, instead of the Direction and Duration wrapper properties the other hosts use:

NoticeBoardHost.Transition = TransitioningContentControl.TransitionType.Drop;
NoticeBoardHost.Direction = TransitioningContentControl.TransitionDirection.Right;
LatestNoticeHost.Transition = TransitioningContentControl.TransitionType.Fade;
PageNameBanner.Transition = TransitioningContentControl.TransitionType.Bounce;
PageNameBanner.SetValue(TransitioningContentControl.TransitionDirectionProperty, TransitioningContentControl.TransitionDirection.Down);
_ = PageNameBanner.SetBinding(
    TransitioningContentControl.TransitionDurationProperty,
    new System.Windows.Data.Binding(nameof(AppShell.PageBannerDuration)) { Source = ViewModel });

NoticeStatusText, CommandStatusText and PageSummaryText are plain TextBlock labels, not IViewFor instances of their own. WpfViewForMixins.WhenActivated has a twin for each block style — Action<Action<IDisposable>>, Action<MultipleDisposable> and Func<IEnumerable<IDisposable>> — that also takes an explicit IViewFor. A plain element like these three labels uses it to tie its activation to a window or view that is one:

_ = NoticeStatusText.WhenActivated(
    d => d(noticeBoard.Router.CurrentViewModel.Subscribe(page =>
        NoticeStatusText.Text = $"Notice board page: {page?.UrlPathSegment ?? "(none)"}")),
    this);

_ = CommandStatusText.WhenActivated(
    d => d.Add(courseList.OpenStudent.IsExecuting.Subscribe(executing =>
        CommandStatusText.Text = executing ? "Opening student..." : "Idle")),
    this);

_ = PageSummaryText.WhenActivated(
    () =>
    [
        ViewModel!.Router.CurrentViewModel.CombineLatest(
            noticeBoard.Router.CurrentViewModel,
            static (course, notice) => $"{course?.UrlPathSegment ?? "start"} / {notice?.UrlPathSegment ?? "none"}")
            .Subscribe(text => PageSummaryText.Text = text)
    ],
    this);

The parameterless WhenActivated() overload activates a view with no disposables of its own, purely to trigger an IActivatableViewModel's activation. OfficeHoursBannerView has nothing to bind — its text is fixed — so it uses this overload just to log when its view model activates:

public sealed class OfficeHoursBannerView : ReactiveUserControl<OfficeHoursBannerViewModel>
{
    private readonly TextBlock _text = new() { Text = "Office hours: 9am-4pm, Monday to Friday." };

    public OfficeHoursBannerView()
    {
        Content = _text;
        ViewModel = new OfficeHoursBannerViewModel();
        ViewModel.Activator.Activated.Subscribe(static _ => Console.WriteLine("Office hours banner activated."));

        _ = this.WhenActivated();
    }
}

Bind with validation

BindWithValidation binds a control two-way to a view model property without a generated field for the control. It walks the view's visual tree to find the control by the name in your view expression, then sets a WPF Binding in TwoWay mode with UpdateSourceTrigger.PropertyChanged. Use it on a view that resolves its bound control at run time instead of through source-generated Bind.

_ = this.BindWithValidation(ViewModel!, vm => vm.Grade, v => v.GradeBox.Text)
    .DisposeWith(d);
<TextBox x:Name="GradeBox" Margin="0,0,0,16" />

Show a page outside the router

Not every WPF app navigates only through a router; Frame and NavigationWindow navigate their own Page objects. ReactivePage<TViewModel> gives a WPF Page the same ViewModel/BindingRoot pair as ReactiveUserControl<TViewModel> and ReactiveWindow<TViewModel>, so a page shown that way binds the same way. The example's "About" button opens one in its own NavigationWindow, outside the router entirely.

<reactiveui:ReactivePage
    x:Class="ReactiveUI.Documentation.PlatformWpf.CourseInfoPage"
    x:TypeArguments="local:CourseInfoPageViewModel"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:reactiveui="http://reactiveui.net"
    xmlns:local="clr-namespace:ReactiveUI.Documentation.PlatformWpf"
    Title="About the Grade Book">

    <StackPanel Margin="24">
        <TextBlock Text="Grade Book" FontSize="20" Margin="0,0,0,12" />
        <TextBlock x:Name="CourseCountText" Margin="0,0,0,4" />
        <TextBlock x:Name="StudentCountText" />
    </StackPanel>

</reactiveui:ReactivePage>
_ = this.WhenActivated(d =>
{
    _ = this.OneWayBind(ViewModel, vm => vm.CourseCount, v => v.CourseCountText.Text, static count => $"Courses: {count}")
        .DisposeWith(d);
    _ = this.OneWayBind(ViewModel, vm => vm.StudentCount, v => v.StudentCountText.Text, static count => $"Students: {count}")
        .DisposeWith(d);

    // BindingRoot is the same view model as ViewModel, exposed under the name every ReactiveUI view uses so
    // XAML resource lookups and view-agnostic code can read it without knowing the concrete view model type.
    Console.WriteLine($"About page shows {BindingRoot?.CourseCount} courses.");
});

Use WithWpf's pieces on their own

WithWpf calls three smaller extensions for you: WithWpfConverters registers the WPF-specific value converters, WithWpfScheduler sets the dispatcher-backed main-thread sequencer, and WpfMainThreadScheduler is the sequencer itself. An app that wants the converters or the sequencer without the rest of WithWpf can call these directly.

using ModernDependencyResolver resolver = new();
ReactiveUIBuilder builder = new(resolver, resolver);
IReactiveUIBuilder coreServices = (IReactiveUIBuilder)builder.WithCoreServices();
IReactiveUIBuilder configured = coreServices.WithWpfConverters().WithWpfScheduler();

Console.WriteLine($"WithWpfConverters/WithWpfScheduler configured: {configured is not null}");
Console.WriteLine($"WPF main-thread scheduler: {WpfMainThreadScheduler.GetType().Name}");
WithWpfConverters/WithWpfScheduler configured: True
WPF main-thread scheduler: DispatcherSequencer

WithWpf also has an overload on IAppBuilder, the interface every ReactiveUI builder and most Splat app builders implement. It forwards to the same configuration as the IReactiveUIBuilder overload the app's own startup calls above.

using ModernDependencyResolver resolver = new();
ReactiveUIBuilder builder = new(resolver, resolver);
IReactiveUIBuilder configured = ((IAppBuilder)builder).WithWpf();
Console.WriteLine($"IAppBuilder.WithWpf() configured: {configured is not null}");
IAppBuilder.WithWpf() configured: True

What ran on Windows

The example's --smoke argument drives the grade book without a person at the keyboard: it opens the window, selects a student, opens their grade page, edits the grade, and goes back. This is what it printed on a real Windows machine:

IAppBuilder.WithWpf() configured: True
WithWpfConverters/WithWpfScheduler configured: True
WPF main-thread scheduler: DispatcherSequencer
Binding hook: AutoDataTemplateBindingHook
Binding hook: AutoDataTemplateBindingHookUnsafe
Default item template hosts each item in a: ViewModelViewHost
Unsafe default item template hosts each item in a: ViewModelViewHostUnsafe
Auto-suspend idle timeout: 00:00:20
Host view contract observable set: True
View contract: (none)
BindingRoot's router has 1 page(s) on screen.
Office hours banner activated.
Contract fallback bypass (via field): True
Summary host contract observable set: True
Transition started.
Contract fallback bypass (via field): True
Summary host contract observable set: True
Summary panel resolved: (none)
Transition completed.
Courses loaded: 2
Students listed: 4
RoutedViewHostUnsafe shows: OfficeNoticeView (Parent evening is on Tuesday.)
ViewModelViewHostUnsafe shows: OfficeNoticeView (Reports are due on Friday.)
Page name banner: courses
Notice status: Notice board page: notice
Command status: Idle
Page summary: courses / notice
Summary panel resolved: StudentSummaryView
Selected in the summary panel: Ada Lovelace
Transition started.
Editing Ada Lovelace's grade.
Navigated to: students/Ada Lovelace
Typed grade: 97
Contract fallback bypass (via field): True
Summary host contract observable set: True
Summary panel resolved: (none)
Summary panel resolved: StudentSummaryView
Saved grade for Ada Lovelace: 97
Back on: courses
Closing window.

The smoke run waits for the first page to finish sliding in, so the first Transition started. has its Transition completed.. It closes the window before the second animation ends, so that transition has no completed line. Where Summary panel resolved:, Office hours banner activated. and the status labels' first values land relative to each other depends on when each element loads into the visual tree, so it can differ from run to run.

Selecting a student updates the ViewModelViewHost preview immediately, without navigating. Opening a student navigates the router, plays a transition, and shows students/Ada Lovelace as the current page's UrlPathSegment. Going back saves the typed grade to the student and returns to courses.

ReactiveUI.Blend

ReactiveUI.Blend adds a Microsoft.Xaml.Behaviors trigger and a behavior that read from a stream instead of an event. They suit a WPF app that already builds its view logic out of Blend behaviors. Both deliver on the WPF main-thread sequencer, so a behavior stays free to touch controls directly. The weather-station dashboard example uses each one.

FollowObservableStateBehavior drives a VisualStateManager state from a stream of state names. Attach it to any FrameworkElement that defines a VisualStateGroup, point StateObservable at a stream of state names, and it calls VisualStateManager.GoToState each time the stream delivers.

<Border x:Name="StatusPanel" Padding="12" BorderThickness="1" BorderBrush="Gray">
    <Border.Background>
        <SolidColorBrush x:Name="StatusBrush" Color="LightGreen" />
    </Border.Background>

    <i:Interaction.Behaviors>
        <blend:FollowObservableStateBehavior
            StateObservable="{Binding StateChanges}"
            TargetObject="{Binding ElementName=StatusPanel}"
            AutoResubscribeOnError="True" />
    </i:Interaction.Behaviors>

    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="WeatherStates">
            <VisualState Name="Calm">
                <Storyboard>
                    <ColorAnimation
                        Storyboard.TargetName="StatusBrush"
                        Storyboard.TargetProperty="Color"
                        To="LightGreen"
                        Duration="0" />
                </Storyboard>
            </VisualState>
            <VisualState Name="Windy">
                <Storyboard>
                    <ColorAnimation
                        Storyboard.TargetName="StatusBrush"
                        Storyboard.TargetProperty="Color"
                        To="Khaki"
                        Duration="0" />
                </Storyboard>
            </VisualState>
            <VisualState Name="Stormy">
                <Storyboard>
                    <ColorAnimation
                        Storyboard.TargetName="StatusBrush"
                        Storyboard.TargetProperty="Color"
                        To="IndianRed"
                        Duration="0" />
                </Storyboard>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>

    <TextBlock Text="{Binding State}" FontWeight="Bold" HorizontalAlignment="Center" />
</Border>

The view model's StateChanges is nothing more than a WhenAnyValue on the current condition:

StateChanges = this.WhenAnyValue(viewModel => viewModel.State);

ObservableTrigger runs its actions each time an observable delivers, instead of each time a routed event fires. Point Observable at a command's result stream, where each execution of the command is one delivery. Give it an action, such as this example's ShowAlertAction, that writes the delivered value into the view.

<TextBlock x:Name="AlertText" Margin="0,12,0,0" Text="(no alert)" />
<i:Interaction.Triggers>
    <blend:ObservableTrigger Observable="{Binding RaiseAlert}" AutoResubscribeOnError="True">
        <local:ShowAlertAction TargetText="{Binding ElementName=AlertText}" />
    </blend:ObservableTrigger>
</i:Interaction.Triggers>
RaiseAlert = ReactiveCommand.Create<string, object>(static message => message);

On real Windows, moving through every weather state and raising an alert printed:

State: Windy, panel background: #FFF0E68C
State: Stormy, panel background: #FFCD5C5C
State: Calm, panel background: #FF90EE90
Alert label: Storm warning issued

Both AutoResubscribeOnError="True" above tell the behavior and the trigger to re-subscribe to StateObservable and Observable if either ever fails, instead of leaving the panel or the alert label stuck. TargetObject is the element the behavior calls VisualStateManager.GoToState on. The XAML above binds it to StatusPanel itself; code that builds the behavior without XAML sets the same dependency property with SetValue:

FollowObservableStateBehavior stateBehavior = new() { SchedulerOverride = Sequencer.Immediate };

// A test that builds the behavior in code, rather than XAML, sets TargetObject through its dependency
// property field with SetValue; the window's XAML sets the same property with a binding instead.
stateBehavior.SetValue(FollowObservableStateBehavior.TargetObjectProperty, targetElement);
Console.WriteLine($"TargetObject via SetValue: {stateBehavior.GetValue(FollowObservableStateBehavior.TargetObjectProperty) == targetElement}");
TargetObject via SetValue: True

Both types also have a SchedulerOverride, for a test that wants to assert their effect right after raising it instead of pumping a dispatcher: setting it to Sequencer.Immediate before assigning StateObservable/Observable delivers on the calling thread, with no dispatcher involved.

stateBehavior.Attach(probeElement);
stateBehavior.StateObservable = Signal.Emit("Stormy");

SchedulerOverride only changes delivery the next time StateObservable/Observable is assigned; setting it on a behavior that has already been given one does nothing until that property is set again.

Run the complete Blend example.

ReactiveUI.Drawing

ReactiveUI.Drawing registers an IBitmapLoader for platforms that need one — WPF among them. Call WithDrawing on the app builder next to WithWpf, and Locator.Current.GetService<IBitmapLoader>() resolves a PlatformBitmapLoader afterward.

_ = RxAppBuilder.CreateReactiveUIBuilder().WithWpf().WithDrawing().BuildApp();

// ReactiveUI.Drawing.Registrations only registers an IBitmapLoader on Windows (or .NET Framework); this
// process is a real WPF app on Windows, so WithDrawing's registration is now resolvable.
IBitmapLoader? bitmapLoader = Locator.Current.GetService<IBitmapLoader>();
Console.WriteLine($"IBitmapLoader after WithDrawing(): {bitmapLoader?.GetType().Name ?? "(none)"}");

On real Windows this printed:

IBitmapLoader after WithDrawing(): PlatformBitmapLoader

Run the complete Drawing example.

ReactiveUI.WPF members

A handful of members work behind the scenes rather than through a call the example makes directly. Wpf.Registrations.Register is what WithWpf calls to add the platform services below to the dependency resolver. It does not register the Unsafe twins; you choose those yourself. ActivationForViewFetcher is the IActivationForViewFetcher that registration installs. It watches a FrameworkElement's Loaded, Unloaded and IsHitTestVisibleChanged events, and a Window's Closed event, so WhenActivated knows when a WPF view is on screen. AutoDataTemplateBindingHook gives an ItemsControl a default DataTemplate when it is bound straight to a list of view models. That template shows each item through a ViewModelViewHost, so such a list needs no template of its own. PlatformOperations.GetOrientation reads whether the screen is in portrait or landscape; RoutedViewHost and ViewModelViewHost use it to build the view contract that changes with orientation.

MemberWhat it does
ActivationForViewFetcherTells WhenActivated when a WPF FrameworkElement (or its containing Window) is on screen. Installed by Wpf.Registrations.
AutoDataTemplateBindingHookSupplies a default DataTemplate for an ItemsControl bound to view models, showing each one through a ViewModelViewHost. DefaultItemTemplate is the template itself.
AutoDataTemplateBindingHookUnsafeThe same default template, with each item shown through a ViewModelViewHostUnsafe. Marked [RequiresDynamicCode]. DefaultItemTemplate is the template itself.
AutoSuspendHelperForwards a WPF Application's Startup, Activated, Deactivated and Exit events into the suspension host, with IdleTimeout controlling how long it waits before asking you to save.
WpfReactiveUIBuilderExtensions.WithWpf(IReactiveUIBuilder) / WithWpf(IAppBuilder)Registers the WPF view hosts, activation fetcher, converters and main-thread sequencer.
WpfReactiveUIBuilderExtensions.WithWpfConvertersRegisters the WPF-specific value converters on their own.
WpfReactiveUIBuilderExtensions.WithWpfSchedulerSets the dispatcher-backed main-thread sequencer on its own.
WpfReactiveUIBuilderExtensions.WpfMainThreadSchedulerThe dispatcher-backed sequencer WithWpfScheduler installs.
PlatformOperations.GetOrientationReads the current screen orientation, used to build a view contract that changes with it.
ReactivePage<TViewModel>A WPF Page that implements IViewFor<TViewModel>, for apps that navigate with Frame/NavigationWindow instead of, or alongside, a router.
ReactiveUserControl<TViewModel>A UserControl that implements IViewFor<TViewModel>.
ReactiveWindow<TViewModel>A Window that implements IViewFor<TViewModel>.
RoutedViewHostShows the view for the router's current page, resolved through ViewLocator from generated and Map views without reflection; shows DefaultContent when the stack is empty. ViewContractObservable is the stream ViewContract is built from.
RoutedViewHostUnsafeA RoutedViewHost that also asks the service locator, for a view registered only there. Marked [RequiresDynamicCode].
TransitioningContentControlThe ContentControl RoutedViewHost and ViewModelViewHost build on; animates between old and new content with Transition (Fade, Move, Slide, Drop, Bounce), Direction (Up, Down, Left, Right) and Duration, and raises TransitionStarted/TransitionCompleted.
ValidationBindingMixins.BindWithValidationBinds a control two-way to a view model property, finding the control by name in the view's visual tree instead of through a generated field.
ViewModelViewHostShows the view for whatever view model its own ViewModel property holds, with no router involved. Finds generated and Map views without reflection. ViewContractObservable is the stream ViewContract is built from; ContractFallbackByPass stops it falling back to an uncontracted view; ResolveViewForViewModel is the protected method it calls to resolve and show the view, and a subclass can override it.
ViewModelViewHostUnsafeA ViewModelViewHost that also asks the service locator, for a view registered only there. Marked [RequiresDynamicCode].
Wpf.RegistrationsThe IWantsToRegisterStuff that WithWpf runs to register the platform services above; not the Unsafe twins.
WpfViewForMixins.GetIsDesignModeReports whether the WPF designer is loading the view, so a constructor can skip work the designer cannot run.
WpfViewForMixins.WhenActivatedThe Action<Action<IDisposable>>, Func<IEnumerable<IDisposable>>, Action<MultipleDisposable> and no-argument overloads that run a block while a WPF view is active, each with a twin that also takes an explicit IViewFor for a plain element that isn't one itself.
ReactiveUI.Blend.FollowObservableStateBehaviorA Blend behavior that drives a VisualStateManager state from a stream of state names. TargetObject is the element it calls GoToState on; AutoResubscribeOnError re-subscribes StateObservable after a failure.
ReactiveUI.Blend.ObservableTriggerA Blend trigger that runs its actions each time an observable delivers a value. AutoResubscribeOnError re-subscribes Observable after a failure.
ReactiveUIBuilderDrawingExtensions.WithDrawingRegisters an IBitmapLoader for platforms, including WPF, that need one.
ReactiveUI.Drawing.RegistrationsThe IWantsToRegisterStuff that WithDrawing runs.