Themes & Fluent Design
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
A theme is the complete visual definition of the standard controls: the colours, corner radii, spacing, animations
and templates that decide what a Button, a TextBox or a ListBox looks like before the app adds a single style of
its own. For almost two decades a WPF app got whatever theme the framework shipped with. Since .NET 9 WPF also
ships a second theme, based on the Fluent design principles of Windows 11, with integrated light and dark modes
and support for the accent colour that the user picks in Windows Settings. This page explains how to turn it on, what
the experimental label means in practice, how the accent colour and dark mode behave, which controls are covered in
.NET 10, and how to combine the built-in theme with the app’s own resources.
The page was written against these versions, current in October 2026:
-
.NET 10 LTS, latest patch 10.0.13, with C# 14. .NET 11 is at release candidate 2 (11.0.0-rc.2) and general availability is expected at .NET Conf, 10 to 13 November 2026; the .NET 11 WPF release notes list bug fixes only at the time of writing, so nothing here is described as changing for it. .NET 8 and .NET 9 leave support on 2026-11-10; the Fluent theme exists on .NET 9 and later, not on .NET 8.
-
CommunityToolkit.Mvvm 8.4.2 for the view models of the example.
-
Windows App SDK 2.5.1 (stable channel) for the WinUI 3 comparison.
-
Visual Studio 2026 18.11.1.
-
Windows 10 and Windows 11. The Fluent theme is designed after the Windows 11 look; Microsoft Learn does not state a minimum Windows version for it.
The running scenario is Bookshelf Desktop. Its BooksViewModel, IBookRepository and IDialogService are the ones
used on the other pages of this section; here a small settings view model lets the user choose between the system,
light and dark appearance.
What Microsoft Learn Provides, and What It Does Not
The material on this topic is spread over three places:
-
the What’s new in WPF for .NET 9 page, which contains the only prose description of the Fluent theme, of the
ThemeModevalues, of theWPF0001experimental diagnostic and of the accent colour keys; -
the What’s new in WPF for .NET 10 page, which lists the controls that gained Fluent styles in that release;
-
the API reference page for the
ThemeModestruct.
There is no Fluent theming deep dive in the official WPF documentation. There is no page that documents the contents
of Fluent.xaml (the keys it defines or how to override them), no list of every control that is styled, and no
statement about Mica or other backdrops for WPF windows. Where this page goes beyond the three sources, it says so
and presents the code as a pattern, not as an official sample. The WPF Gallery app, which Microsoft publishes in the
Microsoft Store, is the official way to preview the theme on real controls.
Two Themes: Aero2 and Fluent
Without any setting, a WPF app on .NET uses the Aero2 theme: the look WPF has had since .NET Framework 4.5, which resembles Windows 8. It has no dark mode, it ignores the Windows accent colour, and it looks out of place next to the Windows 11 shell. The Fluent theme is an opt-in alternative. The two coexist in the framework and the choice is made per application or per window.
| Aspect | Aero2 (default) | Fluent (opt-in, .NET 9 and later) |
|---|---|---|
How to select it |
Nothing to do; |
Set |
Light and dark modes |
Light only |
Both, integrated; |
Windows accent colour |
Not used by the controls |
Used by the controls |
Where it lives |
Part of the framework; not described further in the documentation used here |
A separate assembly, |
Status |
Stable |
In progress: .NET 10 added more controls and fixes, and the code API for |
Control coverage |
All standard controls |
Growing; see the table later on this page |
Source: What’s new in WPF for .NET 9
The accent colour is not tied to the Fluent theme: the .NET 9 page states explicitly that the accent colour resources are available with or without it. The controls only use that colour automatically when the Fluent theme is on.
The Theme Resolution Flow
A control gets its visual definition by looking for its default style. With ThemeMode set, WPF selects the Fluent
dictionary (light or dark) instead of the Aero2 theme dictionary, and the app’s own resources then sit on top of it as
they always did.
Microsoft Learn states the two rules this diagram relies on: ThemeMode is set per application with
Application.ThemeMode and per window with Window.ThemeMode, and once the application value is anything other than
None, a window can no longer be set back to None. The precise internal lookup order is not documented, so the
diagram shows the documented behaviour, not the implementation.
ThemeMode
ThemeMode is a readonly struct in the System.Windows namespace, in PresentationFramework.dll, applicable to
.NET 9, 10 and 11. It is decorated with [Experimental("WPF0001")]. Because it is experimental, the API reference
asks you not to create instances yourself with the constructor and to use the four static properties:
| Value | Meaning |
|---|---|
|
Applies the light Fluent theme. |
|
Applies the dark Fluent theme. |
|
Applies the light or the dark Fluent theme according to the user’s current Windows setting. |
|
The default. Uses the Aero2 theme. |
In XAML the property is set as an attribute with these names. To style the entire application from the Windows
setting, set it on the Application element of App.xaml:
<Application x:Class="Bookshelf.Desktop.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
ThemeMode="System">
<Application.Resources>
<!-- App-wide brushes, converters and styles go here as usual. -->
</Application.Resources>
</Application>
A single window can force a mode regardless of the Windows setting. The Bookshelf "reading lamp" window is always dark, so that long reading sessions are easy on the eyes:
<Window x:Class="Bookshelf.Desktop.Views.ReaderWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Reader" Height="600" Width="900"
ThemeMode="Dark">
<Grid Margin="16">
<TextBlock Text="{Binding CurrentChapter}" TextWrapping="Wrap" FontSize="18" />
</Grid>
</Window>
ThemeMode is described as designed to respect the settings of a Fluent dictionary, so that the Fluent theme can be
customised: values that the app defines in a dictionary are not discarded when the mode is applied. The details of how
customisation and the mode interact are not documented beyond that sentence.
The Experimental Status and WPF0001
The attribute [Experimental("WPF0001")] is the standard .NET mechanism for marking an API that can change or be
removed in a later release. A compiler that sees code using such an API reports the diagnostic ID as an error, not a
warning, until the project opts in. The .NET 9 page applies this to the code side of ThemeMode: accessing the
property from code produces error WPF0001, which prevents access to the API until the error is suppressed. The page
itself marks the API as experimental and subject to change.
Two suppression options are documented. The project-wide one adds the ID to NoWarn in the project file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
<!-- Allow the experimental ThemeMode API in code. -->
<NoWarn>$(NoWarn);WPF0001</NoWarn>
</PropertyGroup>
</Project>
The narrower option is a #pragma around the lines that need the API, which keeps the rest of the project under the
diagnostic. The .NET 9 page names this directive as an alternative to the project-wide setting:
using System.Windows;
namespace Bookshelf.Desktop.Theming;
public static class ThemeInterop
{
// Confines the experimental API to one file, so a later change of ThemeMode
// only needs to be fixed here.
#pragma warning disable WPF0001
public static void ApplyToApplication(ThemeMode mode) => Application.Current.ThemeMode = mode;
public static void ApplyToWindow(Window window, ThemeMode mode) => window.ThemeMode = mode;
public static ThemeMode Light => ThemeMode.Light;
public static ThemeMode Dark => ThemeMode.Dark;
public static ThemeMode System => ThemeMode.System;
#pragma warning restore WPF0001
}
The static wrapper is a design choice, not part of the official sample: it gives the rest of the app a single place to adjust if the API changes shape before it is stabilised. The official examples assign the property directly:
// Set light mode at the application level
Application.Current.ThemeMode = ThemeMode.Light;
// Set dark mode on the current window (inside a Window's code-behind)
this.ThemeMode = ThemeMode.Dark;
The same page puts WPF0001 in the context of the code setter. It does not state whether the XAML attribute
ThemeMode="System" also needs the suppression when the project is built, and it does not state when the API is
expected to become stable. The pages of this section therefore treat ThemeMode as a feature to adopt deliberately:
it is usable in production by apps that accept a possible source-level change on a later .NET version, and the
resource dictionary approach below avoids the experimental API altogether.
The Fluent.xaml Resource Dictionary
The second way to apply the theme is to reference the dictionary directly. It is available at the pack URI
/PresentationFramework.Fluent;component/Themes/Fluent.xaml, and the documented application-level usage merges it
into the application resources with the absolute form of the pack URI:
<Application x:Class="Bookshelf.Desktop.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/PresentationFramework.Fluent;component/Themes/Fluent.xaml" />
<ResourceDictionary Source="Themes/Bookshelf.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
The same page says the dictionary can also be applied to a single Window, to theme just that window. The mechanism is
the one explained on the Resources & Resource Dictionaries page of this section: a ResourceDictionary in
Window.Resources is searched before the application resources, so the Fluent styles apply only to that window.
<Window x:Class="Bookshelf.Desktop.Views.AboutWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="About" Height="240" Width="420">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/PresentationFramework.Fluent;component/Themes/Fluent.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<StackPanel Margin="16">
<TextBlock Text="Bookshelf Desktop" FontSize="20" />
<Button Content="Close" Margin="0,16,0,0" IsCancel="True" />
</StackPanel>
</Window>
The two approaches compare as follows:
| Question | ThemeMode |
Merged Fluent.xaml |
|---|---|---|
Experimental diagnostic |
Setting it in code needs |
Not involved; it is an ordinary resource dictionary |
Light and dark choice |
Built in ( |
The documentation does not describe a variant selection for the dictionary reference |
Runtime change |
Assign a new value |
Replace or remove the merged dictionary |
Scope |
|
Application resources, window resources, or any element that has resources |
Documented customisation |
"Respects the settings set by a Fluent Dictionary" |
Add your own dictionaries after it so that your keys win |
The Fluent.xaml file itself is not documented on Microsoft Learn: the list of keys is not published, so overriding
a brush or a style key of the Fluent theme means reading the file in the dotnet/wpf repository for the version in use.
That is a maintenance cost to weigh before overriding internals; prefer overriding at the level of the app’s own
styles (see the custom themes section).
Accent Colours
Windows lets the user pick an accent colour. Since .NET 9, WPF exposes it through the SystemColors class, as a
Color, as a SolidColorBrush and as the matching ResourceKey values. Along with the accent itself, three lighter
and three darker shades are available:
| Shade | Color | Color key | Brush | Brush key |
|---|---|---|---|---|
Accent |
|
|
|
|
Light 1 |
|
|
|
|
Light 2 |
|
|
|
|
Light 3 |
|
|
|
|
Dark 1 |
|
|
|
|
Dark 2 |
|
|
|
|
Dark 3 |
|
|
|
|
The documented guidance is to wrap the resource key in a dynamic resource, so that the colour in the open app updates
if the user changes the accent while the app is running. The markup extensions are written with passthrough in
inline code: {DynamicResource {x:Static SystemColors.AccentColorBrushKey}}. A Bookshelf "unread" badge and a
section title use it like this:
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
<Border Width="10" Height="10" CornerRadius="5" VerticalAlignment="Center" Margin="0,0,8,0"
Background="{DynamicResource {x:Static SystemColors.AccentColorBrushKey}}" />
<TextBlock Text="Unread" FontWeight="SemiBold"
Foreground="{DynamicResource {x:Static SystemColors.AccentColorBrushKey}}" />
</StackPanel>
<!-- A darker shade for text on a light background: more contrast than the plain accent. -->
<TextBlock Text="Currently reading"
Foreground="{DynamicResource {x:Static SystemColors.AccentColorDark1BrushKey}}" />
The AccentColorBrushKey resource key is looked up through the resource system, so the same DynamicResource rules as
on the Resources & Resource Dictionaries page apply: a StaticResource captures the colour once and never updates.
Reading the colour from code uses the properties, which return the current value:
using System.Windows;
using System.Windows.Media;
namespace Bookshelf.Desktop.Theming;
public static class Accent
{
// The current Windows accent colour and a darker shade of it.
public static Color Primary => SystemColors.AccentColor;
public static Color Strong => SystemColors.AccentColorDark1;
// Brushes are what most visual properties need.
public static SolidColorBrush PrimaryBrush => SystemColors.AccentColorBrush;
}
A value read once in code is a snapshot. The documentation does not describe an event that tells code the accent
changed; the dynamic resource is the supported way to follow it. This page does not claim that the property values
change in a running app; use the DynamicResource form for anything that must follow the user’s choice.
Dark Mode
In the Fluent theme, dark mode is not a separate set of styles that the app writes: the controls have a light
variant and a dark variant, and ThemeMode selects between them. There are three ways to arrive at dark:
-
ThemeMode="Dark", which is dark regardless of Windows; -
ThemeMode="System"with Windows set to the dark app mode; -
a hand-written dark dictionary, as on the Resources & Resource Dictionaries page, for apps that stay on Aero2.
For a Bookshelf settings screen, the user picks among system, light and dark, and the view model remembers the choice.
The view model uses [ObservableProperty] on a partial property (CommunityToolkit.Mvvm 8.4.2 and C# 14):
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Bookshelf.Desktop.ViewModels;
public enum ThemePreference
{
System,
Light,
Dark,
}
public interface IFluentThemeService
{
ThemePreference Current { get; }
void Apply(ThemePreference preference);
}
public sealed partial class SettingsViewModel : ObservableObject
{
private readonly IFluentThemeService _themes;
public SettingsViewModel(IFluentThemeService themes)
{
_themes = themes;
Preference = themes.Current;
}
[ObservableProperty]
public partial ThemePreference Preference { get; set; }
public IReadOnlyList<ThemePreference> Choices { get; } = Enum.GetValues<ThemePreference>();
partial void OnPreferenceChanged(ThemePreference value) => _themes.Apply(value);
[RelayCommand]
private void Reset() => Preference = ThemePreference.System;
}
Source: ObservableProperty attribute
The service that maps the preference to ThemeMode is the only class that touches the experimental API. Registering it
in the Generic Host is covered on the Dependency Injection & the Generic Host page of this section (not linked here
because that page is not written yet); the registration is services.AddSingleton<IFluentThemeService, FluentThemeService>().
using System.Windows;
using Bookshelf.Desktop.ViewModels;
namespace Bookshelf.Desktop.Theming;
public sealed class FluentThemeService : IFluentThemeService
{
public ThemePreference Current { get; private set; } = ThemePreference.System;
public void Apply(ThemePreference preference)
{
Current = preference;
#pragma warning disable WPF0001 // ThemeMode is experimental in .NET 9 and 10
Application.Current.ThemeMode = preference switch
{
ThemePreference.Light => ThemeMode.Light,
ThemePreference.Dark => ThemeMode.Dark,
_ => ThemeMode.System,
};
#pragma warning restore WPF0001
}
}
The view is an ordinary bound ComboBox:
<StackPanel Margin="24" MaxWidth="360" HorizontalAlignment="Left">
<TextBlock Text="Appearance" FontSize="20" Margin="0,0,0,12" />
<ComboBox ItemsSource="{Binding Choices}"
SelectedItem="{Binding Preference, Mode=TwoWay}"
AutomationProperties.Name="Theme" />
<Button Content="Follow Windows" Margin="0,12,0,0" Command="{Binding ResetCommand}" />
</StackPanel>
The persistence of the choice (the next launch should start with it) is covered by a later page of the section on local settings; the view model above deliberately does not store it.
Windows chrome and the title bar
The ThemeMode documentation does not say whether the non-client area of the window (the title bar and
its caption buttons) follows the mode. This page therefore makes no claim about it. Test the target Windows versions
on a real machine, and if the title bar must match the content, check the WPF Gallery and the dotnet/wpf repository
for the current behaviour. The same caution applies to window backdrops: the WPF documentation does not
mention Mica for WPF windows, while WinUI 3 supports Mica and Acrylic as part of its design system (see the comparison
table below).
High Contrast
Windows has a high contrast mode for accessibility, which replaces the app palette with a small set of system colours chosen by the user. Two statements from the documentation matter:
-
The .NET 10 notes list a fix for crashes related to
HighContrastamong the Fluent style fixes, so older .NET 9 builds are the more fragile place to rely on the Fluent theme in high contrast. The page does not describe the conditions of the crash. -
The mechanism for apps remains the one from the Resources & Resource Dictionaries page:
SystemParameters.HighContrastreports the mode, andSystemColorsbrushes give the colours the user chose.
A custom control that draws with its own brushes must stay readable in high contrast. The safe approach is to use the
SystemColors keys for text and borders when the mode is on:
using System.Windows;
namespace Bookshelf.Desktop.Theming;
public sealed class HighContrastMonitor
{
public HighContrastMonitor()
{
// SystemParameters.StaticPropertyChanged is raised when a system setting changes.
SystemParameters.StaticPropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(SystemParameters.HighContrast))
{
HighContrastChanged?.Invoke(this, EventArgs.Empty);
}
};
}
public event EventHandler? HighContrastChanged;
public bool IsHighContrast => SystemParameters.HighContrast;
}
Source: SystemParameters.HighContrast
The accessibility checklist for the whole app is on the globalization and accessibility page later in the section.
Which Controls the Fluent Theme Covers
The documentation does not publish a complete coverage list. What it states, from the .NET 10 notes, is a delta: "Fluent UI style support is still in progress", and .NET 10 added Fluent styles for these controls:
| Control | Release that added the Fluent style |
|---|---|
|
.NET 10 |
|
.NET 10 |
|
.NET 10 |
|
.NET 10 |
|
.NET 10 |
|
.NET 10 |
|
.NET 10 |
|
.NET 10 |
|
.NET 10 |
The same section lists these non-control changes: the Expander animation was fixed, the missing
RecognizesAccessKey was added to several controls, and right-to-left layout problems were fixed for MenuItem,
Expander and TreeViewItem. A control not in the table may still be styled: it may have been styled in .NET 9
(which the .NET 9 page does not enumerate), or it may not be styled at all. Treat the table as the list of controls
whose style is known to be new in .NET 10, and verify others visually. Third-party and custom controls are not
themed unless they use the dictionary keys or default styles that the theme provides.
Two rules of thumb follow from the incomplete list:
-
Test every window of the app on both modes (light and dark) with the controls that are really used. The WPF Gallery app shows how each standard control looks.
-
When a control looks wrong, check for an app-level implicit
Stylethat replaces the template. A style that setsTemplatereplaces the Fluent template for that control, which is what the Dependency Properties & Attached Properties page means by "the app-level style overrides the framework look".
Custom Themes on Top of Fluent
An app brand colour or a custom control must coexist with the built-in theme. The pattern that works with both
ThemeMode and the merged dictionary is to keep the app’s own tokens in a dictionary whose entries are not the same
keys as the framework’s, reference them with DynamicResource, and let ThemeMode handle the standard controls.
Brand brushes are defined in light and dark variants, as in the Resources & Resource Dictionaries page, and
selected by a small theme service; the accent colour comes from SystemColors.
<!-- Themes/Bookshelf.xaml : tokens that do not collide with framework keys -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Spacing tokens are independent of the theme. -->
<Thickness x:Key="Bookshelf.Spacing.Page">24</Thickness>
<CornerRadius x:Key="Bookshelf.Radius.Card">8</CornerRadius>
<!-- A card style that follows the accent colour and the theme-driven foreground. -->
<Style x:Key="Bookshelf.Style.Card" TargetType="Border">
<Setter Property="Padding" Value="16" />
<Setter Property="CornerRadius" Value="{StaticResource Bookshelf.Radius.Card}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="{DynamicResource {x:Static SystemColors.AccentColorBrushKey}}" />
</Style>
<!-- A named style for the page title; it does not replace the control template. -->
<Style x:Key="Bookshelf.Style.PageTitle" TargetType="TextBlock">
<Setter Property="FontSize" Value="24" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Margin" Value="0,0,0,12" />
</Style>
</ResourceDictionary>
Source: Merged resource dictionaries
Used in the Bookshelf list screen:
<Border Style="{StaticResource Bookshelf.Style.Card}" Margin="{StaticResource Bookshelf.Spacing.Page}">
<StackPanel>
<TextBlock Style="{StaticResource Bookshelf.Style.PageTitle}" Text="My shelf" />
<ListBox ItemsSource="{Binding Books}" SelectedItem="{Binding SelectedBook}" DisplayMemberPath="Title" />
</StackPanel>
</Border>
Source: Styles and templates
Three guidelines keep a custom theme maintainable:
-
Style by type with
BasedOn, not by replacing templates. AStylewithTargetType="Button"andBasedOn="{StaticResource {x:Type Button}}"adds to the active theme instead of replacing it, so the control still gets the Fluent template. A style with a newControlTemplatestops the control from following the theme. -
Order of the merged dictionaries. Resources are searched in the order of the merged dictionaries from the last to the first, so the dictionary placed after
Fluent.xamlwins. Put the app dictionary after the framework one. -
Keep theme-dependent values behind
DynamicResource. Anything that has to change when the user switches mode or accent colour is a dynamic reference. Spacing, radii and font sizes can stayStaticResourcebecause they do not change.
A BasedOn example, so that a Bookshelf primary button inherits the Fluent look and only adds its own spacing:
<Style x:Key="Bookshelf.Style.PrimaryButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Padding" Value="16,6" />
<Setter Property="MinWidth" Value="96" />
</Style>
Source: Styles and templates
Whether {StaticResource {x:Type Button}} resolves to the Fluent default style when ThemeMode is used, rather than
to the Aero2 one, is not stated in the sources used here. Verify with the WPF Gallery pattern on the target .NET
version (look at a button next to one without BasedOn); if the BasedOn reference fails to pick up the theme in a
given release, drop BasedOn and style only the properties you set.
Comparison With Other Stacks
| Topic | WPF (.NET 9 and later) | WinUI 3 | Windows Forms |
|---|---|---|---|
Default look |
Aero2; Fluent opt-in |
Fluent (WinUI controls) |
Classic Windows look |
Selecting light or dark |
|
|
|
Follow the Windows setting |
|
Follows the Windows app mode by default when |
|
Accent colour |
|
|
Not provided by the controls |
Backdrop materials (Mica, Acrylic) |
Not documented for WPF |
Supported |
Not provided |
Status of the feature |
Experimental in code; control coverage growing |
Stable |
Stable in .NET 10 |
For the choice among the stacks, see Windows App Platforms: WPF, WinUI 3, WinForms, MAUI & More; for how a WinUI XAML file differs from a WPF one, see XAML Dialects: WPF, WinUI & MAUI.
Testing a Themed App
Because the same window can be shown in several modes, the cheap way to catch regressions is to open it in each:
-
Run the app with
ThemeMode="Light", then"Dark", then switch Windows to the other app mode whileSystemis active and see whether the window follows. -
Change the Windows accent colour while the app is open and confirm the accent-coloured elements update.
-
Turn high contrast on and confirm the text is readable and focus visuals remain visible.
-
Review any window that has a custom
Stylethat setsTemplateor colours directly, because those do not follow the theme. -
Use the tools described in Development Environment & Tooling to inspect the running visual tree.
A view-model unit test cannot see the theme, but it can verify that the settings view model asks the service for what the
user chose. That test uses a fake IFluentThemeService, which keeps the experimental API out of the test project:
using Bookshelf.Desktop.ViewModels;
using Xunit;
namespace Bookshelf.Desktop.Tests;
public sealed class SettingsViewModelTests
{
private sealed class FakeThemes : IFluentThemeService
{
public ThemePreference Current { get; private set; } = ThemePreference.System;
public ThemePreference? LastApplied { get; private set; }
public void Apply(ThemePreference preference)
{
Current = preference;
LastApplied = preference;
}
}
[Fact]
public void Choosing_dark_applies_dark()
{
var themes = new FakeThemes();
var vm = new SettingsViewModel(themes);
vm.Preference = ThemePreference.Dark;
Assert.Equal(ThemePreference.Dark, themes.LastApplied);
}
[Fact]
public void Reset_returns_to_system()
{
var themes = new FakeThemes();
var vm = new SettingsViewModel(themes) { Preference = ThemePreference.Light };
vm.ResetCommand.Execute(null);
Assert.Equal(ThemePreference.System, vm.Preference);
}
}
Source: RelayCommand attribute
Pitfalls
-
Experimental means experimental. The code API is subject to change; keep it in one class such as the
FluentThemeServiceabove, and re-read the What’s new in WPF page of each .NET version before upgrading. -
WPF0001is an error, not a warning. WithoutNoWarnor the#pragma, a project that setsThemeModein code does not compile. Do not add a blanketNoWarnthat hides unrelated diagnostics; addWPF0001only. -
No way back to
Noneper window. IfApplication.ThemeModeis notNone, a window cannot be set toNone. A window that must stay on Aero2 requires the application to be onNoneand the other windows to opt in. -
Static references to theme brushes.
StaticResourceon an accent or a theme brush keeps the old colour after the user changes mode or accent. UseDynamicResource. -
Replacing templates. An app-wide implicit style that sets
Templatefor a control removes the Fluent template from it; check the control in both modes. -
Assuming complete coverage. .NET 10 still describes Fluent support as in progress. A control that is not styled looks like Aero2 inside a Fluent window. Test every control the app uses.
-
Overriding framework keys. The keys inside
Fluent.xamlare not documented as a stable contract; an override can stop working on a later .NET version. -
Third-party and custom controls. Controls that ship their own templates or colours do not follow
ThemeMode. Check their documentation for Fluent or dark-mode support. -
High contrast on .NET 9. The .NET 10 notes mention fixed crashes related to
HighContrastin the Fluent styles; test high contrast on the exact .NET version you ship. -
Third-party claims beyond Microsoft Learn. Articles that describe Mica, title bar and accent-colour behaviour for WPF go beyond what Microsoft Learn documents. Treat them as hints, and test.