WPF performance and testing MVVM applications

Binding and layout cost, virtualization and frozen resources, profiling the UI thread, testing view models without a window, and when UI automation earns its cost.

Where the time goes

CostTypical causeFix
Layout thrashChanging Width, Margin or Visibility in a loopBatch changes, use RenderTransform
Binding overheadHundreds of bindings on dynamic dataBind to a stable snapshot; avoid deep paths
Container creationVirtualization disabledTurn on item-based scrolling and recycling
Freezable churnA new brush or geometry per itemFreeze one instance and share it
Dispatcher saturationToo many BeginInvoke callsCoalesce and use Background priority
Template explosionA heavy DataTemplate per rowSimplify the template; show detail on demand
// a frozen, shared brush has no change tracking and can be used everywhere
public static class Palette
{
    public static readonly SolidColorBrush Accent = Create("#2F6FED");
    public static readonly SolidColorBrush Muted  = Create("#6B7280");

    private static SolidColorBrush Create(string hex)
    {
        var brush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(hex));
        brush.Freeze();     // immutable and shareable
        return brush;
    }
}
  • Turn on binding trace output in the debugger. A binding that fails on every render is invisible in behaviour and very visible in a profile.
  • PresentationTraceSources.TraceLevel=High on a single binding answers "why is this value wrong" without guesswork.
  • Profile the UI thread and the render thread separately; a smooth UI thread with a saturated render thread means too much visual complexity, not too much code.

Testing view models

[Fact]
public async Task LoadAsync_populates_orders_and_clears_busy_flag()
{
    var api = Substitute.For<IOrderApi>();
    api.GetOrdersAsync().Returns(Task.FromResult<IReadOnlyList<Order>>(
        new[] { new Order("A-1", "Acme", 120m) }));

    var vm = new OrdersViewModel(api, Substitute.For<IDialogService>());

    await vm.LoadAsync();

    Assert.Single(vm.Orders);
    Assert.False(vm.IsBusy);
    Assert.Null(vm.Error);
}

[Fact]
public async Task LoadAsync_surfaces_an_api_failure()
{
    var api = Substitute.For<IOrderApi>();
    api.GetOrdersAsync().Returns(Task.FromException<IReadOnlyList<Order>>(new HttpRequestException("503")));

    var vm = new OrdersViewModel(api, Substitute.For<IDialogService>());
    await vm.LoadAsync();

    Assert.Contains("503", vm.Error);
    Assert.False(vm.IsBusy);
}

A view model built on interfaces can be tested in milliseconds with no window, no dispatcher and no application instance. That is the practical payoff of MVVM: the interesting logic is the part that does not need a screen.

// anything that touches the dispatcher needs it while testing
[Fact]
public void Save_command_raises_property_changed()
{
    var raised = new List<string>();
    var vm = new EditorViewModel();
    vm.PropertyChanged += (s, e) => raised.Add(e.PropertyName);

    vm.SaveCommand.Execute(null);

    Assert.Contains(nameof(EditorViewModel.IsDirty), raised);
}

When to add UI automation

  • Automate the flows that genuinely break: start-up, navigation to a key screen, and one full save round-trip per feature area.
  • Select elements with AutomationProperties.AutomationId. Locating by visible text makes the test fail every time the copy changes.
  • Run UI tests on a machine with a real desktop session; headless agents without a session cannot drive WPF reliably.
  • Treat a flaky UI test as a bug in the test. Retrying a flaky test hides the real problem and erodes trust in the suite.
  • Keep the UI suite small and fast. Twenty reliable tests that run in a minute beat two hundred that run for an hour.
<Button Content="Save"
        AutomationProperties.AutomationId="SaveButton"
        AutomationProperties.Name="Save the order"
        Command="{Binding SaveCommand}" />
⚠️
Do not measure performance from a debug build, and do not measure it on a warm run only. First-start cost includes XAML parsing, JIT and resource loading; a second launch can be twice as fast and hide a genuine regression.

FAQ

How many bindings are too many?
There is no fixed number - a grid with a few thousand live bindings can be perfectly smooth once virtualization is on. Measure with the visual tree tool and a UI-thread profile rather than counting.
Should I test the XAML?
Test the view models thoroughly and automate only the handful of end-to-end flows you would otherwise release broken. Asserting on visual details tests the theme, not the behaviour.

Items controls, collections and virtualization Custom controls and user controls

Last refreshed 2026-09-18.