Resource Management in WinUI 3
Table of Contents
- What XAML Resources Are
- ResourceDictionary
- Resource Scoping and Lookup
- Merged Dictionaries
- StaticResource vs ThemeResource
- ThemeDictionaries
- Resource Organization Strategies
- Common Pitfalls
What XAML Resources Are
XAML resources are reusable objects defined once and referenced throughout the application. Rather than repeating the same brush color, font size, or control template on every element that needs it, you define the object in one place, give it a key, and reference it by that key wherever it is needed. When you need to change the value later, you change it in one location and every reference updates automatically.
The range of objects that work well as resources is broad. Color brushes and gradients are the most common, since color consistency across an application demands centralized definitions. Styles, which group multiple property setters into a named unit, are nearly as common. Data templates define how a specific data type renders inside a collection control, and those templates typically live as resources so that multiple lists sharing the same item type can share the same presentation logic. Converters, which are objects implementing IValueConverter and used in bindings, must be instantiated before they can be referenced in markup; resources are how that instantiation happens at the XAML level without requiring code-behind.
Other candidate resource types include geometry objects for vector paths, font families, numeric constants expressed as doubles or thicknesses, and string values for labels or format strings. Any object that would otherwise be duplicated across multiple XAML files is a resource candidate.
ResourceDictionary
A ResourceDictionary is the container that holds XAML resources. It behaves like a dictionary where each entry has a string key and an object value. The key is specified using the x:Key attribute on the resource element, and this key is what markup extensions like {StaticResource} and {ThemeResource} use to retrieve the value.
Every XAML element that derives from FrameworkElement exposes a Resources property of type ResourceDictionary. You add resources to this dictionary using property-element syntax directly in the markup file.
<Page.Resources>
<SolidColorBrush x:Key="PrimaryBrush" Color="#0078D4" />
<x:Double x:Key="CardCornerRadius">8</x:Double>
<Thickness x:Key="CardPadding">16,12,16,12</Thickness>
</Page.Resources>
Every resource must have an x:Key. Without it, the XAML parser will throw an error at load time because there is no way to address an unnamed resource in a dictionary. The only exception is implicit styles, which are styles without an x:Key but with a TargetType set; these apply automatically to all elements of that type within scope, so the type itself serves as the lookup key.
When the XAML parser encounters {StaticResource PrimaryBrush}, it needs to find an entry with that key in a ResourceDictionary. The parser searches the resource dictionaries attached to elements in the visual tree, starting from the element that contains the reference and walking up toward the root. This lookup process is described in more detail in the next section.
The ResourceDictionary class also supports loading resources lazily by wrapping them in a ResourceDictionary.ThemeDictionaries or by using the deferred loading capability, though most applications rely on standard eager loading where all resources in a dictionary are parsed and instantiated when the dictionary is loaded.
Resource Scoping and Lookup
Resource dictionaries can be attached at different levels of the element tree, and where you attach a dictionary determines which elements can use the resources it contains. The three primary attachment points are the application, individual pages or windows, and individual controls.
Resources defined in App.xaml are globally available. Every element in the application can reference them because App.xaml represents the top of the resource lookup hierarchy. This makes it the right home for resources shared across the entire application, such as the primary brand colors, the base button style, or a commonly used converter instance.
<!-- App.xaml -->
<Application.Resources>
<SolidColorBrush x:Key="BrandBrush" Color="#0078D4" />
<local:BoolToVisibilityConverter x:Key="BoolToVisibility" />
</Application.Resources>
Page-level resources are defined in the Resources section of a page or window. They are available to any element within that page but are not visible to other pages. This is appropriate for resources that are specific to a particular view, such as a data template for an item type that only one page renders, or a style variant that only one screen uses.
Control-level resources, defined in the Resources section of a specific control, are the most tightly scoped. Only elements within that control’s subtree can reference them. This is rarely the right choice for styles or brushes, but it can be useful for small converters or templates that are genuinely local to a self-contained control.
When the runtime resolves a resource reference, it starts at the element where the reference appears and walks up the element tree, checking each element’s Resources dictionary. If it reaches the root of the tree without finding the key, it falls back to Application.Resources. If the key is still not found, a runtime exception occurs for {StaticResource} references, while {ThemeResource} references may fail silently depending on context.
This lookup order means that a resource defined at the page level will shadow a resource with the same key defined at the application level. Pages can intentionally override global resources this way, though accidental shadowing through naming collisions is a common source of confusing behavior.
Merged Dictionaries
As an application grows, placing all resources directly in App.xaml or page files becomes unwieldy. Merged dictionaries solve this by letting you split resources across separate XAML files while still making them available as a unified lookup namespace.
A ResourceDictionary has a MergedDictionaries collection that can contain other ResourceDictionary instances, each loaded from a separate file via its Source attribute. When the runtime performs a resource lookup, it searches the merged dictionaries as if their contents were part of the parent dictionary.
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Assets/Styles/Colors.xaml" />
<ResourceDictionary Source="/Assets/Styles/Typography.xaml" />
<ResourceDictionary Source="/Assets/Styles/Controls.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
Notice that when you use MergedDictionaries, you must wrap Application.Resources in an explicit <ResourceDictionary> element. This is a common stumbling block: the merged dictionaries syntax requires the outer dictionary to be declared as an explicit element rather than relying on the implicit dictionary that the Resources property provides. Forgetting this wrapper results in a parse error.
Each file referenced via Source is a standalone XAML file whose root element is <ResourceDictionary>. These files contain only resources; they have no code-behind and no x:Class attribute. They are pure data files that the resource system loads and merges.
Merged dictionaries can themselves contain further MergedDictionaries, allowing for hierarchical organization. A top-level AllStyles.xaml might merge Colors.xaml, Brushes.xaml, and Typography.xaml, and then App.xaml merges only AllStyles.xaml. This keeps App.xaml readable while allowing fine-grained organization of the resource files themselves.
StaticResource vs ThemeResource
The difference between {StaticResource} and {ThemeResource} comes down to when the lookup happens and whether it can repeat.
{StaticResource} performs a one-time lookup when the element is loaded. The resource is found, the property value is set, and no further connection between the property and the dictionary entry exists. If the dictionary entry changes after load, or if the application theme changes, the property value is unaffected. This makes {StaticResource} slightly more efficient and appropriate for values that are genuinely constant, such as a fixed icon size, a converter instance, or a data template.
{ThemeResource} performs an initial lookup at load time like {StaticResource}, but it also registers a listener on the property. When the application theme changes between light, dark, and high-contrast modes, the runtime performs a new lookup and updates the property with the resolved value from the new theme’s resource dictionary. Any visual element whose appearance should respond to theme changes must use {ThemeResource}.
The WinUI 3 control library uses {ThemeResource} pervasively in its default control templates for exactly this reason. Brushes like ApplicationPageBackgroundThemeBrush, TextFillColorPrimaryBrush, and SystemFillColorSuccessBrush are all theme resources that automatically flip between light and dark variants when the user changes their system theme.
For your own resources, the rule of thumb is straightforward: if the value has light and dark variants (or any theme variants at all), use {ThemeResource}. If the value is the same regardless of theme, use {StaticResource}. Applying {ThemeResource} to a resource that has no theme variants wastes the overhead of registering a listener that will never update anything, but it will still work correctly. Applying {StaticResource} to a brush that should respond to theme changes will leave that brush stuck on whatever theme was active when the page loaded, which is a subtle bug that only manifests when users switch themes while the application is running.
ThemeDictionaries
ThemeDictionaries is how you define the per-theme variants of your resources within a single ResourceDictionary. Rather than creating entirely separate files for light and dark themes, you nest dictionaries within the ThemeDictionaries collection, each keyed to a theme name.
The recognized theme keys are Light, Dark, and HighContrast. The runtime inspects the application’s current theme and selects the matching dictionary when resolving {ThemeResource} references.
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="CardBackgroundBrush" Color="#FFFFFF" />
<SolidColorBrush x:Key="CardBorderBrush" Color="#E0E0E0" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="CardBackgroundBrush" Color="#2B2B2B" />
<SolidColorBrush x:Key="CardBorderBrush" Color="#404040" />
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="CardBackgroundBrush" Color="{ThemeResource SystemColorWindowColor}" />
<SolidColorBrush x:Key="CardBorderBrush" Color="{ThemeResource SystemColorWindowTextColor}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
Each theme dictionary must define the same set of keys. If CardBackgroundBrush exists in the Light dictionary but not in the Dark dictionary, a {ThemeResource CardBackgroundBrush} reference will fail to resolve in dark mode. The runtime does not fall back across theme dictionaries; if the key is absent from the active theme’s dictionary, the lookup fails entirely.
The HighContrast dictionary deserves particular attention. High-contrast mode uses a small, fixed set of system colors, and those colors are themselves theme resources provided by the system. Referencing them via {ThemeResource SystemColorWindowColor} inside your HighContrast dictionary ensures that the values remain correct regardless of which specific high-contrast theme the user has selected. Hardcoding hex colors in the HighContrast dictionary undermines the purpose of high-contrast support.
ThemeDictionaries can appear in any ResourceDictionary, including the per-file dictionaries referenced through MergedDictionaries. A common pattern is to have a dedicated BrandThemes.xaml file that contains only ThemeDictionaries, keeping all theme-aware color definitions in one place while keeping the file focused and readable.
Resource Organization Strategies
There is no single correct way to organize resources, but several approaches have proven effective in practice. The choice depends on the size of the application, the team structure, and how much the design system is expected to evolve.
Organizing by type is the simplest approach and works well for small-to-medium applications. You create separate files for colors and brushes, typography, spacing and geometry, and control styles. Each file has a clear, focused purpose and stays manageable in size. Developers looking for a specific brush know to check Colors.xaml without hunting through unrelated style definitions.
Organizing by feature area works better when a large application has sections with distinct visual treatments. A dashboard section might have its own DashboardStyles.xaml merged at the page or region level, while an onboarding flow has its own OnboardingStyles.xaml. Global shared resources still live in App.xaml, but feature-specific styles stay close to the feature code rather than contributing to an ever-growing global file.
Organizing by control type is a middle path where you create one file per major control type, such as ButtonStyles.xaml, CardStyles.xaml, and NavigationStyles.xaml. This mirrors how WinUI 3 itself structures its default styles in the generic theme resources, which can make it easier to compare your overrides against the originals.
Regardless of the organizational strategy, keeping the depth of MergedDictionaries chains shallow improves lookup performance. Every level of dictionary nesting adds a traversal step during resource resolution. Two or three levels of nesting is reasonable; more than that is a signal that the organization strategy may be creating unnecessary complexity.
Common Pitfalls
Resource key collisions in merged dictionaries are among the most common resource management bugs. When two merged dictionaries define the same key, the one that appears last in the MergedDictionaries collection wins. The parser does not warn you about this; it silently uses the last definition. This means the order in which you list dictionaries in MergedDictionaries is semantically meaningful, and changing that order can change which resource gets applied. Keeping a consistent naming convention that makes collisions obvious, such as prefixing keys with a file or feature abbreviation, helps prevent this.
Referencing a resource before it is defined causes a runtime exception with {StaticResource}. XAML is parsed in document order, and a resource dictionary entry must appear before the first reference to it in the same file. Resources in App.xaml are safe to reference from any page because the application resources are loaded before any page is instantiated. But within a single XAML file, if you place a resource reference above the Resources section that defines it, the lookup will fail. Merged dictionaries declared before any resource references in the same dictionary load first, so the ordering constraint applies across the merge chain as well.
Circular references, where dictionary A merges dictionary B and dictionary B merges dictionary A, will cause a stack overflow at load time. The parser follows merge references recursively without cycle detection, so the chain loops until the stack exhausts. This is rare in practice but can emerge through indirect dependencies when multiple feature-area files each merge a shared foundation file, and one of those feature files is then merged back into the foundation.
Deep merge chains have a real but often overlooked performance cost. When the runtime resolves a resource key, it searches the current dictionary, then each merged dictionary in reverse order, and then recursively searches within each merged dictionary’s own merge chain. An application with a very deep merge hierarchy can cause measurable lookup latency during initial load, especially for pages that reference many distinct resource keys during initialization. Flattening the hierarchy, where possible by consolidating related resources into fewer files, keeps lookup paths short and load times predictable.
Finally, instantiation behavior is worth understanding. Each ResourceDictionary instantiates its resources once when the dictionary is loaded. If multiple pages use the same merged dictionary, they share the same resource instances. For brushes and styles this is fine and expected, but for objects with mutable state, a shared instance can produce surprising cross-page side effects. Converters and geometry objects are generally safe to share; controls and view models should never be placed in a resource dictionary, as they will be shared across every consumer and only one instance will exist regardless of how many pages reference them.
Found this guide helpful? Share it with your team:
Share on LinkedIn