<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: s-iguchi09</title>
    <description>The latest articles on DEV Community by s-iguchi09 (@s-iguchi09).</description>
    <link>https://dev.to/s-iguchi09</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4116074%2Fced7d4a4-c3cc-41a4-bf13-70305f1b7475.png</url>
      <title>DEV Community: s-iguchi09</title>
      <link>https://dev.to/s-iguchi09</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/s-iguchi09"/>
    <language>en</language>
    <item>
      <title>Binding to the Parent DataContext from Inside a WPF DataTemplate</title>
      <dc:creator>s-iguchi09</dc:creator>
      <pubDate>Sat, 26 Sep 2026 08:21:22 +0000</pubDate>
      <link>https://dev.to/s-iguchi09/binding-to-the-parent-datacontext-from-inside-a-wpf-datatemplate-5d69</link>
      <guid>https://dev.to/s-iguchi09/binding-to-the-parent-datacontext-from-inside-a-wpf-datatemplate-5d69</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://s-iguchi09.github.io/articles/wpf-datatemplate-parent-datacontext-binding/" rel="noopener noreferrer"&gt;s-iguchi09.github.io&lt;/a&gt;.&lt;br&gt;
A &lt;a href="https://s-iguchi09.github.io/ja/articles/wpf-datatemplate-parent-datacontext-binding/" rel="noopener noreferrer"&gt;Japanese version&lt;/a&gt; is also available.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;A button placed inside the &lt;code&gt;DataTemplate&lt;/code&gt; of an &lt;code&gt;ItemsControl&lt;/code&gt; or &lt;code&gt;ListBox&lt;/code&gt; often fails to invoke a command that lives on the parent view model.&lt;br&gt;
The binding expression itself is correct: it is evaluated exactly as written, but the &lt;code&gt;DataContext&lt;/code&gt; it starts from has been switched to the individual item, so the target member is never reached.&lt;br&gt;
This article explains the cause in terms of &lt;code&gt;DataContext&lt;/code&gt; inheritance and compares four ways to reach the parent — &lt;code&gt;RelativeSource&lt;/code&gt;, &lt;code&gt;ElementName&lt;/code&gt;, &lt;code&gt;x:Reference&lt;/code&gt;, and &lt;code&gt;PlacementTarget&lt;/code&gt; — based on measured behavior inside a &lt;code&gt;DataTemplate&lt;/code&gt; and inside a &lt;code&gt;ContextMenu&lt;/code&gt; or &lt;code&gt;ToolTip&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Prerequisites / Environment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Framework: .NET 6 or later / WPF&lt;/li&gt;
&lt;li&gt;Language: C# 9 or later / XAML (the code samples assume target-typed new and enabled nullable reference types; on C# 8 and earlier, state the type explicitly and drop &lt;code&gt;!&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Target features: &lt;code&gt;ItemTemplate&lt;/code&gt; / &lt;code&gt;DataTemplate&lt;/code&gt; of &lt;code&gt;ItemsControl&lt;/code&gt;-derived controls, plus &lt;code&gt;ContextMenu&lt;/code&gt; and &lt;code&gt;ToolTip&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Architecture: MVVM, where commands and shared display values belong to the parent view model rather than to each item&lt;/li&gt;
&lt;li&gt;Behavior verified on: .NET 10 / Windows 11&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;Consider a list where each row carries a delete button, with the command bound inside the &lt;code&gt;DataTemplate&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ItemsControl&lt;/span&gt; &lt;span class="na"&gt;ItemsSource=&lt;/span&gt;&lt;span class="s"&gt;"{Binding Items}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ItemsControl.ItemTemplate&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;DataTemplate&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;StackPanel&lt;/span&gt; &lt;span class="na"&gt;Orientation=&lt;/span&gt;&lt;span class="s"&gt;"Horizontal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;TextBlock&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"{Binding Name}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Content=&lt;/span&gt;&lt;span class="s"&gt;"Delete"&lt;/span&gt; &lt;span class="na"&gt;Command=&lt;/span&gt;&lt;span class="s"&gt;"{Binding DeleteCommand}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/DataTemplate&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ItemsControl.ItemTemplate&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ItemsControl&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DeleteCommand&lt;/code&gt; belongs to the parent view model assigned to the &lt;code&gt;DataContext&lt;/code&gt; of the &lt;code&gt;ItemsControl&lt;/code&gt;, not to the elements of &lt;code&gt;Items&lt;/code&gt;.&lt;br&gt;
Clicking the button does nothing.&lt;br&gt;
The button is not rendered as disabled either, so it remains fully clickable and the failure is invisible on screen.&lt;/p&gt;

&lt;p&gt;The Output window records the following binding error, emitted as a single line and wrapped here for readability.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System.Windows.Data Error: 40 : BindingExpression path error: 'DeleteCommand' property not found on
'object' ''Measurement' (HashCode=58682725)'. BindingExpression:Path=DeleteCommand;
DataItem='Measurement' (HashCode=58682725); target element is 'Button' (Name='');
target property is 'Command' (type 'ICommand')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The decisive detail is that &lt;code&gt;DataItem&lt;/code&gt; names the item type (&lt;code&gt;Measurement&lt;/code&gt; here) rather than the parent view model.&lt;br&gt;
Reading these messages in general is covered in &lt;a href="https://s-iguchi09.github.io/articles/wpf-binding-error-debugging-output-window/" rel="noopener noreferrer"&gt;Reading WPF Binding Errors and Diagnosing Them with the Output Window&lt;/a&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Cause / Background
&lt;/h2&gt;

&lt;p&gt;When a &lt;code&gt;Binding&lt;/code&gt; specifies none of &lt;code&gt;Source&lt;/code&gt;, &lt;code&gt;RelativeSource&lt;/code&gt;, or &lt;code&gt;ElementName&lt;/code&gt;, it resolves &lt;code&gt;Path&lt;/code&gt; against the &lt;code&gt;DataContext&lt;/code&gt; of the target element.&lt;br&gt;
&lt;code&gt;DataContext&lt;/code&gt; is inherited down the element tree, so a view model assigned to the &lt;code&gt;Window&lt;/code&gt; normally reaches every descendant.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ItemsControl&lt;/code&gt; interrupts that inheritance.&lt;br&gt;
It generates a container for each element of &lt;code&gt;ItemsSource&lt;/code&gt;, such as a &lt;code&gt;ContentPresenter&lt;/code&gt; or a &lt;code&gt;ListBoxItem&lt;/code&gt;, and assigns the corresponding data item to that container's &lt;code&gt;DataContext&lt;/code&gt;.&lt;br&gt;
Because the container's own &lt;code&gt;DataContext&lt;/code&gt; is the item, a binding written in an &lt;code&gt;ItemContainerStyle&lt;/code&gt; setter resolves against the item as well (see &lt;a href="https://s-iguchi09.github.io/articles/wpf-treeview-select-item-programmatically/" rel="noopener noreferrer"&gt;Selecting and Expanding a WPF TreeView Node from Code, and Why SelectedItem Is Read-Only&lt;/a&gt;).&lt;br&gt;
Because &lt;code&gt;DataContext&lt;/code&gt; is an inherited property, elements expanded from the &lt;code&gt;DataTemplate&lt;/code&gt; receive the container's value unchanged.&lt;br&gt;
As a result, &lt;code&gt;{Binding DeleteCommand}&lt;/code&gt; inside the template looks for &lt;code&gt;DeleteCommand&lt;/code&gt; on the item, does not find it, and leaves the binding unresolved.&lt;/p&gt;

&lt;p&gt;The easily missed consequence is that &lt;strong&gt;a failed &lt;code&gt;Command&lt;/code&gt; binding does not disable the button&lt;/strong&gt;.&lt;br&gt;
Through &lt;code&gt;Command&lt;/code&gt;, a button renders as disabled when the &lt;code&gt;ICommand&lt;/code&gt; assigned to it returns &lt;code&gt;false&lt;/code&gt; from &lt;code&gt;CanExecute&lt;/code&gt;.&lt;br&gt;
With &lt;code&gt;Command&lt;/code&gt; left at &lt;code&gt;null&lt;/code&gt; there is nothing to evaluate, and &lt;code&gt;IsEnabled&lt;/code&gt; stays &lt;code&gt;true&lt;/code&gt;.&lt;br&gt;
This applies to any control implementing &lt;code&gt;ICommandSource&lt;/code&gt;, &lt;code&gt;MenuItem&lt;/code&gt; included, not only to &lt;code&gt;Button&lt;/code&gt;.&lt;br&gt;
The visible state therefore looks correct, and the only symptom is that clicking has no effect.&lt;br&gt;
A command that is assigned correctly but never toggles between enabled and disabled has a different cause, covered in &lt;a href="https://s-iguchi09.github.io/articles/wpf-relaycommand-canexecute-not-updating/" rel="noopener noreferrer"&gt;Fixing a RelayCommand Whose CanExecute Does Not Update the Button State in WPF&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A plain &lt;code&gt;{Binding}&lt;/code&gt; only walks the &lt;code&gt;DataContext&lt;/code&gt;, so it has no way to cross this switch.&lt;br&gt;
Crossing it requires either walking up the element tree or referring to the target element by name.&lt;/p&gt;

&lt;p&gt;The following diagram shows where the &lt;code&gt;DataContext&lt;/code&gt; switches and where the element tree itself is severed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Figure:&lt;/strong&gt; Diagram showing the DataContext switching to the item from ContentPresenter downward in the window element tree, and the ContextMenu inside a Popup detached from that tree, where RelativeSource and ElementName stop at the boundary while PlacementTarget still points at the owning element.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;DataContext&lt;/code&gt; switch (blue is the parent view model, red is the item) and the element tree severed by the &lt;code&gt;Popup&lt;/code&gt;. On the left, the walk up to the &lt;code&gt;ItemsControl&lt;/code&gt; succeeds; inside the &lt;code&gt;Popup&lt;/code&gt; on the right no such path exists, and only &lt;code&gt;PlacementTarget&lt;/code&gt; reaches the owning element.&lt;/p&gt;

&lt;p&gt;The diagram is rendered in the &lt;a href="https://s-iguchi09.github.io/articles/wpf-datatemplate-parent-datacontext-binding/" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;



&lt;p&gt;The figure below records whether the &lt;code&gt;Command&lt;/code&gt; inside the template resolves, per way of writing the binding.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Figure:&lt;/strong&gt; A table of whether Button.Command inside a DataTemplate resolves, per binding style. A plain Binding leaves Command null; RelativeSource and ElementName both resolve it. IsEnabled stays True in every case.&lt;/p&gt;

&lt;p&gt;Measured on .NET 10 / Windows 11 by reading &lt;code&gt;Command&lt;/code&gt; off the &lt;code&gt;Button&lt;/code&gt; placed in an &lt;code&gt;ItemsControl.ItemTemplate&lt;/code&gt;. Nothing differs between the rows except how the binding is written.&lt;/p&gt;

&lt;p&gt;The diagram is rendered in the &lt;a href="https://s-iguchi09.github.io/articles/wpf-datatemplate-parent-datacontext-binding/" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Note the &lt;code&gt;IsEnabled&lt;/code&gt; column.&lt;/strong&gt; It stays &lt;code&gt;True&lt;/code&gt; even on the row where &lt;code&gt;Command&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt;.&lt;br&gt;
A failed resolution changes nothing visible, so it cannot be spotted by looking at the screen.&lt;br&gt;
All that remains is the symptom: clicking does nothing.&lt;/p&gt;


&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;Use the &lt;code&gt;FindAncestor&lt;/code&gt; mode of &lt;code&gt;RelativeSource&lt;/code&gt; to walk up to an ancestor that still holds the original &lt;code&gt;DataContext&lt;/code&gt;, then reach the member through that &lt;code&gt;DataContext&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Prefer &lt;code&gt;ItemsControl&lt;/code&gt; as the ancestor type.&lt;br&gt;
&lt;code&gt;Window&lt;/code&gt; or &lt;code&gt;UserControl&lt;/code&gt; also works, but moving the template into another view or &lt;code&gt;UserControl&lt;/code&gt; changes the surrounding structure, so &lt;code&gt;ItemsControl&lt;/code&gt; survives reuse better.&lt;/p&gt;

&lt;p&gt;Prefix the path with &lt;code&gt;DataContext.&lt;/code&gt;.&lt;br&gt;
&lt;code&gt;RelativeSource&lt;/code&gt; returns the ancestor &lt;strong&gt;element&lt;/strong&gt;, and its &lt;code&gt;DataContext&lt;/code&gt; is not traversed automatically.&lt;br&gt;
Conversely, omit &lt;code&gt;DataContext.&lt;/code&gt; when the target is a dependency property of the ancestor element itself.&lt;br&gt;
Referencing a dependency property defined on a &lt;code&gt;UserControl&lt;/code&gt; from inside that control is one such case (see &lt;a href="https://s-iguchi09.github.io/articles/wpf-usercontrol-dependencyproperty-binding-not-working/" rel="noopener noreferrer"&gt;Binding to a WPF UserControl's Own Dependency Property from Inside the Control&lt;/a&gt;).&lt;/p&gt;


&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;The following XAML places a plain binding and a &lt;code&gt;RelativeSource&lt;/code&gt; binding side by side inside the same &lt;code&gt;DataTemplate&lt;/code&gt;.&lt;br&gt;
Both attempt to display &lt;code&gt;Unit&lt;/code&gt;, which only the parent view model owns, so the sole difference is how the parent is reached.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ItemsControl&lt;/span&gt; &lt;span class="na"&gt;ItemsSource=&lt;/span&gt;&lt;span class="s"&gt;"{Binding Items}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ItemsControl.ItemTemplate&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;DataTemplate&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;StackPanel&lt;/span&gt; &lt;span class="na"&gt;Orientation=&lt;/span&gt;&lt;span class="s"&gt;"Horizontal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;TextBlock&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"{Binding Name}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;TextBlock&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"{Binding Value}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

                &lt;span class="c"&gt;&amp;lt;!-- The item has no Unit, so this stays empty --&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;TextBlock&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"{Binding Unit}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

                &lt;span class="c"&gt;&amp;lt;!-- Walks up to the ItemsControl and reads Unit from its DataContext --&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;TextBlock&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"{Binding DataContext.Unit,
                                  RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

                &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Content=&lt;/span&gt;&lt;span class="s"&gt;"Delete"&lt;/span&gt;
                        &lt;span class="na"&gt;Command=&lt;/span&gt;&lt;span class="s"&gt;"{Binding DataContext.DeleteCommand,
                                  RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"&lt;/span&gt;
                        &lt;span class="na"&gt;CommandParameter=&lt;/span&gt;&lt;span class="s"&gt;"{Binding}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/DataTemplate&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ItemsControl.ItemTemplate&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ItemsControl&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CommandParameter="{Binding}"&lt;/code&gt; deliberately uses a plain binding and passes the &lt;code&gt;DataContext&lt;/code&gt; of that element, which is the item itself.&lt;br&gt;
The division is that the parent view model supplies which member to invoke, while the item supplies what to invoke it on.&lt;/p&gt;

&lt;p&gt;The matching view model is shown below.&lt;br&gt;
&lt;code&gt;Unit&lt;/code&gt; and &lt;code&gt;DeleteCommand&lt;/code&gt; are declared here rather than on the item type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Measurement&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Measurement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MeasurementListViewModel&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;MeasurementListViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;DeleteCommand&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RelayCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;Measurement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;!));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Unit&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"kg"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ObservableCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Measurement&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Measurement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Measurement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Measurement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"C"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;240&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ICommand&lt;/span&gt; &lt;span class="n"&gt;DeleteCommand&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Measurement&lt;/code&gt; is deliberately not a &lt;code&gt;record&lt;/code&gt;, because &lt;code&gt;ObservableCollection&amp;lt;T&amp;gt;.Remove&lt;/code&gt; deletes the first element that compares equal.&lt;br&gt;
Records compare by value, so with two identical rows the deletion removes the first one rather than the row that was clicked.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RelayCommand&lt;/code&gt; stands in for any &lt;code&gt;ICommand&lt;/code&gt; implementation with a constructor that takes an &lt;code&gt;Action&amp;lt;object?&amp;gt;&lt;/code&gt;; an implementation with a fixed parameter type, such as &lt;code&gt;RelayCommand&amp;lt;Measurement&amp;gt;&lt;/code&gt; in CommunityToolkit.Mvvm, is the equivalent.&lt;br&gt;
Assigning this instance to the &lt;code&gt;DataContext&lt;/code&gt; of the &lt;code&gt;Window&lt;/code&gt; lets the &lt;code&gt;ItemsControl&lt;/code&gt; inherit it, which is where the &lt;code&gt;RelativeSource&lt;/code&gt; bindings land.&lt;/p&gt;

&lt;p&gt;Displaying the XAML above, with an added header identifying each expression and a frame around each value, makes the difference between the two bindings visible.&lt;/p&gt;


  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs-iguchi09.github.io%2Fimages%2Farticles%2Fwpf-datatemplate-parent-datacontext-binding%2Fdatatemplate-parent-binding.png" alt="Three rows of an ItemsControl where the left box, using a plain Binding, stays empty and the right box, using RelativeSource, shows kg." width="598" height="209"&gt;Result of the two bindings placed in the same &lt;code&gt;DataTemplate&lt;/code&gt;. The left box uses &lt;code&gt;{Binding Unit}&lt;/code&gt; and resolves against the item, leaving it empty; the right box walks up to the &lt;code&gt;ItemsControl&lt;/code&gt; and shows the value from the parent view model. The header text and the frames around each value were added to the figure to identify which expression produced each result and to make the empty box visible. The delete button is omitted from the figure because it plays no part in the contrast between the two bindings (produced on .NET 10 / Windows 11).
  



&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ElementName&lt;/code&gt; also resolves from inside a &lt;code&gt;DataTemplate&lt;/code&gt;.&lt;/strong&gt;
A &lt;code&gt;DataTemplate&lt;/code&gt; establishes its own XAML namescope, but WPF resolves &lt;code&gt;ElementName&lt;/code&gt; by searching outward into enclosing namescopes, so &lt;code&gt;{Binding DataContext.DeleteCommand, ElementName=RootWindow}&lt;/code&gt; works inside a template (verified on .NET 10 / Windows 11).
The common claim that &lt;code&gt;ElementName&lt;/code&gt; cannot be used inside templates does not apply to WPF.
The namescope restriction on markup-based &lt;code&gt;ElementName&lt;/code&gt; resolution is documented for the WinUI &lt;code&gt;Binding.ElementName&lt;/code&gt;; the WPF documentation carries no equivalent restriction.
It does depend on the referenced name, so the binding breaks once the template moves to another view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neither &lt;code&gt;RelativeSource&lt;/code&gt; nor &lt;code&gt;ElementName&lt;/code&gt; reaches out of a &lt;code&gt;ContextMenu&lt;/code&gt; or &lt;code&gt;ToolTip&lt;/code&gt;.&lt;/strong&gt;
Both are hosted inside a &lt;code&gt;Popup&lt;/code&gt;, and a &lt;code&gt;Popup&lt;/code&gt; renders its content in a separate window on screen.
Elements inside the popup are therefore detached from the element tree of the application window, and since both ancestor lookup and name resolution walk that tree, neither crosses the boundary.
Unlike the &lt;code&gt;DataTemplate&lt;/code&gt; case, no outward path exists at all.
The Output window records &lt;code&gt;Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Window', AncestorLevel='1''&lt;/code&gt; or &lt;code&gt;Cannot find source for binding with reference 'ElementName=RootWindow'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;ContextMenu&lt;/code&gt; inherits its &lt;code&gt;DataContext&lt;/code&gt; from the element that owns it.&lt;/strong&gt;
When the &lt;code&gt;ContextMenu&lt;/code&gt; is attached to an element inside a &lt;code&gt;DataTemplate&lt;/code&gt;, the inherited value is that element's &lt;code&gt;DataContext&lt;/code&gt;, which is the item.
A plain &lt;code&gt;{Binding}&lt;/code&gt; inside the menu therefore resolves against the item exactly as it does inside the template, and still never reaches the parent view model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ContextMenu&lt;/code&gt; derives from &lt;code&gt;ItemsControl&lt;/code&gt;.&lt;/strong&gt;
Specifying &lt;code&gt;AncestorType={x:Type ItemsControl}&lt;/code&gt; inside a &lt;code&gt;ContextMenu&lt;/code&gt; matches the &lt;code&gt;ContextMenu&lt;/code&gt; itself rather than the outer list.
Ancestor type lookup therefore does not behave as intended inside a &lt;code&gt;ContextMenu&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both the inherited &lt;code&gt;DataContext&lt;/code&gt; and &lt;code&gt;PlacementTarget&lt;/code&gt; are established as the menu opens.&lt;/strong&gt;
For a &lt;code&gt;ContextMenu&lt;/code&gt; assigned to &lt;code&gt;FrameworkElement.ContextMenu&lt;/code&gt;, &lt;code&gt;ContextMenuService&lt;/code&gt; sets &lt;code&gt;PlacementTarget&lt;/code&gt; to the owning element as the menu opens.
Before that, &lt;code&gt;PlacementTarget&lt;/code&gt; and &lt;code&gt;DataContext&lt;/code&gt; are both &lt;code&gt;null&lt;/code&gt;, and neither is set yet at the &lt;code&gt;ContextMenuOpening&lt;/code&gt; stage.
A binding that goes through &lt;code&gt;PlacementTarget&lt;/code&gt; therefore does not resolve until the menu opens.
Because &lt;code&gt;PlacementTarget&lt;/code&gt; is a dependency property, the binding is re-evaluated once it is assigned and resolves correctly from then on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;x:Reference&lt;/code&gt; carries a documented restriction.&lt;/strong&gt;
&lt;code&gt;x:Reference&lt;/code&gt; is XAML 2009 syntax, and the documentation states that XAML 2009 features are usable in WPF only for XAML that is not markup-compiled.
In practice &lt;code&gt;{x:Reference}&lt;/code&gt; written in a &lt;code&gt;.xaml&lt;/code&gt; page resolves both inside a &lt;code&gt;DataTemplate&lt;/code&gt; and inside a &lt;code&gt;ContextMenu&lt;/code&gt; (verified on .NET 10 / Windows 11), but it falls outside what the documentation guarantees, so it should not be the first choice.
The same documentation also states that &lt;code&gt;ElementName&lt;/code&gt; binding should still be used for most WPF applications.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Alternatives / Comparison
&lt;/h2&gt;

&lt;p&gt;The table lists whether each mechanism reaches the parent view model, measured inside a &lt;code&gt;DataTemplate&lt;/code&gt; and inside a &lt;code&gt;ContextMenu&lt;/code&gt; or &lt;code&gt;ToolTip&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Inside DataTemplate&lt;/th&gt;
&lt;th&gt;Inside ContextMenu / ToolTip&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plain &lt;code&gt;{Binding X}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Not reachable&lt;/td&gt;
&lt;td&gt;Not reachable&lt;/td&gt;
&lt;td&gt;Shortest to write&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DataContext&lt;/code&gt; switches to the item, so the parent is out of range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;RelativeSource&lt;/code&gt; &lt;code&gt;AncestorType&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Reachable&lt;/td&gt;
&lt;td&gt;Not reachable&lt;/td&gt;
&lt;td&gt;No name dependency, survives template reuse&lt;/td&gt;
&lt;td&gt;Depends on tree structure. Cannot cross the popup boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ElementName&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reachable&lt;/td&gt;
&lt;td&gt;Not reachable&lt;/td&gt;
&lt;td&gt;Short, and no ancestor type to choose&lt;/td&gt;
&lt;td&gt;Depends on the referenced name. Cannot cross the popup boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x:Reference&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reachable&lt;/td&gt;
&lt;td&gt;Reachable&lt;/td&gt;
&lt;td&gt;Crosses the popup boundary&lt;/td&gt;
&lt;td&gt;XAML 2009 feature with a documented restriction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;PlacementTarget&lt;/code&gt; + &lt;code&gt;Tag&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;td&gt;Reachable&lt;/td&gt;
&lt;td&gt;Works reliably in popups&lt;/td&gt;
&lt;td&gt;Occupies &lt;code&gt;Tag&lt;/code&gt;. &lt;code&gt;PlacementTarget&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt; until the menu opens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table narrows the options by reachability; which one to pick under which condition follows the selection criteria in the summary.&lt;/p&gt;

&lt;p&gt;To reach the parent view model from a &lt;code&gt;ContextMenu&lt;/code&gt;, stash the parent &lt;code&gt;DataContext&lt;/code&gt; in the owner's &lt;code&gt;Tag&lt;/code&gt; and read it back through &lt;code&gt;PlacementTarget&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Border&lt;/span&gt; &lt;span class="na"&gt;Tag=&lt;/span&gt;&lt;span class="s"&gt;"{Binding DataContext,
              RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Border.ContextMenu&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ContextMenu&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;MenuItem&lt;/span&gt; &lt;span class="na"&gt;Header=&lt;/span&gt;&lt;span class="s"&gt;"Delete"&lt;/span&gt;
                      &lt;span class="na"&gt;Command=&lt;/span&gt;&lt;span class="s"&gt;"{Binding PlacementTarget.Tag.DeleteCommand,
                                RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"&lt;/span&gt;
                      &lt;span class="na"&gt;CommandParameter=&lt;/span&gt;&lt;span class="s"&gt;"{Binding}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/ContextMenu&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Border.ContextMenu&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Border&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Border&lt;/code&gt; that sets &lt;code&gt;Tag&lt;/code&gt; sits inside the &lt;code&gt;DataTemplate&lt;/code&gt;, so &lt;code&gt;RelativeSource&lt;/code&gt; can still reach the &lt;code&gt;ItemsControl&lt;/code&gt; from there.&lt;br&gt;
The &lt;code&gt;MenuItem&lt;/code&gt; walks up to the &lt;code&gt;ContextMenu&lt;/code&gt;, then reads the &lt;code&gt;Tag&lt;/code&gt; of its &lt;code&gt;PlacementTarget&lt;/code&gt;, which is that &lt;code&gt;Border&lt;/code&gt;, and arrives at the parent view model.&lt;br&gt;
&lt;code&gt;CommandParameter="{Binding}"&lt;/code&gt; resolves against the item the &lt;code&gt;ContextMenu&lt;/code&gt; inherited from its owner, so the target item still travels with the command.&lt;/p&gt;

&lt;p&gt;The same structure applies to a &lt;code&gt;ToolTip&lt;/code&gt;, with the ancestor type changed accordingly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;TextBlock&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"{Binding PlacementTarget.Tag.Unit,
                  RelativeSource={RelativeSource AncestorType={x:Type ToolTip}}}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To cross the popup boundary without occupying &lt;code&gt;Tag&lt;/code&gt;, use &lt;code&gt;x:Reference&lt;/code&gt;.&lt;br&gt;
It supplies the element itself as &lt;code&gt;Source&lt;/code&gt;, so neither the element tree nor a name resolution path is walked.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;TextBlock&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"{Binding Source={x:Reference RootWindow}, Path=DataContext.Unit}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;A binding that cannot reach the parent view model from inside a &lt;code&gt;DataTemplate&lt;/code&gt; is not a syntax problem: the starting &lt;code&gt;DataContext&lt;/code&gt; has been switched to the item.&lt;br&gt;
For a &lt;code&gt;Command&lt;/code&gt;, the button stays enabled and silently does nothing, so the diagnosis comes from the &lt;code&gt;DataItem&lt;/code&gt; value in the Output window rather than from the visible state.&lt;/p&gt;

&lt;p&gt;Choose as follows.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ordinary &lt;code&gt;DataTemplate&lt;/code&gt; content:&lt;/strong&gt;
Use &lt;code&gt;RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}&lt;/code&gt;.
It avoids name dependencies and survives reuse of the template in another list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A template that never leaves its current view:&lt;/strong&gt;
&lt;code&gt;ElementName&lt;/code&gt; reaches the parent as well.
It removes the need to pick an ancestor type, at the cost of tracking the referenced name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inside a &lt;code&gt;ContextMenu&lt;/code&gt; or &lt;code&gt;ToolTip&lt;/code&gt;:&lt;/strong&gt;
Stash the parent &lt;code&gt;DataContext&lt;/code&gt; in the owner's &lt;code&gt;Tag&lt;/code&gt; and read &lt;code&gt;PlacementTarget.Tag&lt;/code&gt;.
&lt;code&gt;RelativeSource&lt;/code&gt; and &lt;code&gt;ElementName&lt;/code&gt; cannot cross the popup boundary and are not options here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When &lt;code&gt;Tag&lt;/code&gt; is already in use:&lt;/strong&gt;
&lt;code&gt;x:Reference&lt;/code&gt; crosses the popup boundary as well.
It is a XAML 2009 feature and falls outside what the documentation guarantees, so prefer &lt;code&gt;PlacementTarget&lt;/code&gt; wherever it is available.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most stable structure is to give each item its own view model that owns the operations performed on it.&lt;br&gt;
When the markup accumulates bindings that reach up to parent commands, the first thing to evaluate is whether those commands belong on the item view model instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-datagrid-cell-editing-template/" rel="noopener noreferrer"&gt;Switching Controls Between Display and Edit Modes in WPF DataGrid Cells&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-combobox-itemssource-patterns/" rel="noopener noreferrer"&gt;WPF ComboBox ItemsSource Binding Patterns and Selected Value Retrieval&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/apps/wpf-standard-control-demo/treeview.html" rel="noopener noreferrer"&gt;TreeView | WPF Standard Control Demo App (.NET 10)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/apps/wpf-standard-control-demo/listbox.html" rel="noopener noreferrer"&gt;ListBox | WPF Standard Control Demo App (.NET 10)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>wpf</category>
      <category>dotnet</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Expressing SQL Outer Joins in LINQ — Implementing LeftJoin, RightJoin and Shuffle</title>
      <dc:creator>s-iguchi09</dc:creator>
      <pubDate>Thu, 17 Sep 2026 14:02:53 +0000</pubDate>
      <link>https://dev.to/s-iguchi09/expressing-sql-outer-joins-in-linq-implementing-leftjoin-rightjoin-and-shuffle-476n</link>
      <guid>https://dev.to/s-iguchi09/expressing-sql-outer-joins-in-linq-implementing-leftjoin-rightjoin-and-shuffle-476n</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://s-iguchi09.github.io/articles/linq-backport-netframework-to-net10/" rel="noopener noreferrer"&gt;s-iguchi09.github.io&lt;/a&gt;.&lt;br&gt;
A &lt;a href="https://s-iguchi09.github.io/ja/articles/linq-backport-netframework-to-net10/" rel="noopener noreferrer"&gt;Japanese version&lt;/a&gt; is also available.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;An outer join that SQL writes with a single &lt;code&gt;LEFT JOIN&lt;/code&gt; clause required composing three methods in LINQ — &lt;code&gt;GroupJoin&lt;/code&gt;, &lt;code&gt;SelectMany&lt;/code&gt; and &lt;code&gt;DefaultIfEmpty&lt;/code&gt; — for over a decade.&lt;br&gt;
.NET 10 finally closes that gap by adding &lt;code&gt;LeftJoin&lt;/code&gt; and &lt;code&gt;RightJoin&lt;/code&gt; as first-class operators.&lt;br&gt;
Alongside them, random reordering — long imitated with the &lt;code&gt;OrderBy(_ =&amp;gt; Guid.NewGuid())&lt;/code&gt; pseudo-idiom — was standardized as &lt;code&gt;Shuffle&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Starting from the correspondence between SQL join clauses and LINQ idioms, this article implements polyfills that make the three operators available on .NET Framework.&lt;br&gt;
It then digs into a concern unique to this backport, born from the fact that outer joins live next door to database queries: applying the polyfill to &lt;code&gt;IQueryable&amp;lt;T&amp;gt;&lt;/code&gt; silently breaks query translation.&lt;/p&gt;


&lt;h2&gt;
  
  
  Prerequisites / Environment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Frameworks: .NET Framework 4.8 (backport target) / .NET 10+ (future migration target)&lt;/li&gt;
&lt;li&gt;APIs: LINQ &lt;code&gt;LeftJoin&lt;/code&gt; (2 signatures), &lt;code&gt;RightJoin&lt;/code&gt; (2 signatures), &lt;code&gt;Shuffle&lt;/code&gt; (1 signature)&lt;/li&gt;
&lt;li&gt;Approach: apply &lt;code&gt;#nullable enable&lt;/code&gt;; disable automatically on migration via &lt;code&gt;#if !NET10_0_OR_GREATER&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Language version: nullable annotations on unconstrained type parameters (&lt;code&gt;TInner?&lt;/code&gt; / &lt;code&gt;TOuter?&lt;/code&gt;) require &lt;code&gt;LangVersion&lt;/code&gt; 9.0 or later (recommended: &lt;code&gt;latest&lt;/code&gt;). No other project configuration changes&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  How SQL Join Clauses Map to LINQ Idioms
&lt;/h2&gt;

&lt;p&gt;The three operators added in .NET 10 are the following.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Added in&lt;/th&gt;
&lt;th&gt;Corresponding operation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LeftJoin&amp;lt;TOuter, TInner, TKey, TResult&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;.NET 10.0&lt;/td&gt;
&lt;td&gt;SQL &lt;code&gt;LEFT OUTER JOIN&lt;/code&gt; (keeps every outer element)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RightJoin&amp;lt;TOuter, TInner, TKey, TResult&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;.NET 10.0&lt;/td&gt;
&lt;td&gt;SQL &lt;code&gt;RIGHT OUTER JOIN&lt;/code&gt; (keeps every inner element)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Shuffle&amp;lt;TSource&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;.NET 10.0&lt;/td&gt;
&lt;td&gt;Random reordering&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;LINQ before .NET 10 has no dedicated outer-join operator — only &lt;code&gt;Join&lt;/code&gt; (inner join).&lt;br&gt;
An operation that SQL expresses in one clause mapped to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- SQL: keep employees with no matching department&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeptName&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Employee&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Department&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeptId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeptId&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// LINQ (.NET 9 and earlier): composing GroupJoin + SelectMany + DefaultIfEmpty&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GroupJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;departments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeptId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeptId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SelectMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DefaultIfEmpty&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;DeptName&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The composed idiom buries the intent — "outer join" — in structure, and misplacing &lt;code&gt;SelectMany&lt;/code&gt; or &lt;code&gt;DefaultIfEmpty&lt;/code&gt; quietly turns it into an inner or cross join.&lt;br&gt;
Random ordering has the same shape of problem: &lt;code&gt;OrderBy(_ =&amp;gt; Guid.NewGuid())&lt;/code&gt; generates a key per element, pays for a full sort, and offers no uniformity guarantee as a shuffle.&lt;/p&gt;


  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs-iguchi09.github.io%2Fimages%2Farticles%2Flinq-backport-netframework-to-net10%2Flinq-leftjoin-rightjoin-shuffle.png" alt="Results of LeftJoin and RightJoin over two sequences. LeftJoin yields null for the missing right side, RightJoin yields null for the missing left side, and Shuffle reorders the elements." width="448" height="218"&gt;Evaluation results when the two sequences contain non-matching keys. &lt;code&gt;LeftJoin&lt;/code&gt; keeps the left side and fills the counterpart with &lt;code&gt;null&lt;/code&gt;; &lt;code&gt;RightJoin&lt;/code&gt; does the reverse. &lt;code&gt;Shuffle&lt;/code&gt; randomizes the order, so that row shows the result of a single run.
  



&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;The following is the complete polyfill for &lt;code&gt;LeftJoin&lt;/code&gt; (2 signatures), &lt;code&gt;RightJoin&lt;/code&gt; (2 signatures) and &lt;code&gt;Shuffle&lt;/code&gt; (1 signature).&lt;br&gt;
&lt;code&gt;LeftJoin&lt;/code&gt; / &lt;code&gt;RightJoin&lt;/code&gt; embed the composition idiom above in the same shape as the built-ins, and &lt;code&gt;Shuffle&lt;/code&gt; buffers the source into an array before applying a Fisher–Yates shuffle.&lt;br&gt;
The reasons for placing the polyfill in &lt;code&gt;System.Linq&lt;/code&gt; and for the migration guard are covered in the &lt;a href="https://s-iguchi09.github.io/articles/linq-backport-netframework-to-net5/" rel="noopener noreferrer"&gt;series foundation article&lt;/a&gt;.&lt;br&gt;
Add it to the project as, for example, &lt;code&gt;LinqExtensions.Net10.cs&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#nullable enable
&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Collections.Generic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cp"&gt;#if !NET10_0_OR_GREATER // Active only below .NET 10.0 (e.g. .NET Framework)
&lt;/span&gt;
&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;System.Linq&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Provides extension methods that backfill LINQ methods introduced in .NET 10.0 for older target frameworks.&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LinqExtensions&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ==========================================&lt;/span&gt;
        &lt;span class="c1"&gt;// 1. LeftJoin (left outer join)&lt;/span&gt;
        &lt;span class="c1"&gt;// ==========================================&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LeftJoin&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOuter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TInner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOuter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInner&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOuter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TKey&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outerKeySelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TKey&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;innerKeySelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOuter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TInner&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;resultSelector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LeftJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outerKeySelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;innerKeySelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resultSelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comparer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LeftJoin&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOuter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TInner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOuter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInner&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOuter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TKey&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outerKeySelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TKey&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;innerKeySelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOuter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TInner&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;resultSelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;IEqualityComparer&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TKey&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;comparer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outer&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outerKeySelector&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outerKeySelector&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;innerKeySelector&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;innerKeySelector&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resultSelector&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resultSelector&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

            &lt;span class="c1"&gt;// Group matching inner elements per outer element; supply default(TInner) when none match.&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GroupJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outerKeySelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;innerKeySelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inners&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inners&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;comparer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SelectMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DefaultIfEmpty&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resultSelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// ==========================================&lt;/span&gt;
        &lt;span class="c1"&gt;// 2. RightJoin (right outer join)&lt;/span&gt;
        &lt;span class="c1"&gt;// ==========================================&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RightJoin&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOuter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TInner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOuter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInner&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOuter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TKey&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outerKeySelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TKey&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;innerKeySelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOuter&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="n"&gt;TInner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;resultSelector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RightJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outerKeySelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;innerKeySelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resultSelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comparer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RightJoin&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOuter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TInner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOuter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInner&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOuter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TKey&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outerKeySelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TKey&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;innerKeySelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOuter&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="n"&gt;TInner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;resultSelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;IEqualityComparer&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TKey&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;comparer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outer&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outerKeySelector&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outerKeySelector&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;innerKeySelector&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;innerKeySelector&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resultSelector&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resultSelector&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

            &lt;span class="c1"&gt;// GroupJoin pivoted on the inner sequence; supply default(TOuter) when none match.&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GroupJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;innerKeySelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outerKeySelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outers&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;comparer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SelectMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DefaultIfEmpty&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resultSelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// ==========================================&lt;/span&gt;
        &lt;span class="c1"&gt;// 3. Shuffle (random reordering)&lt;/span&gt;
        &lt;span class="c1"&gt;// ==========================================&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TSource&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Shuffle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TSource&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TSource&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;ShuffleIterator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TSource&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ShuffleIterator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TSource&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TSource&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="c1"&gt;// Fisher–Yates: swap each position from the tail with an undecided element.&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;--)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SharedRandom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cp"&gt;#if NET6_0_OR_GREATER
&lt;/span&gt;        &lt;span class="c1"&gt;// .NET 6+ provides a thread-safe shared instance.&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Random&lt;/span&gt; &lt;span class="n"&gt;SharedRandom&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shared&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#else
&lt;/span&gt;        &lt;span class="c1"&gt;// .NET Framework has no Random.Shared, so keep one instance per thread.&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ThreadStatic&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_threadRandom&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Random&lt;/span&gt; &lt;span class="n"&gt;SharedRandom&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_threadRandom&lt;/span&gt; &lt;span class="p"&gt;??=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cp"&gt;#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether this implementation returns what the standard LINQ returns can be checked by building the same calling code for &lt;code&gt;net48&lt;/code&gt; (polyfill active) and for &lt;code&gt;net10.0&lt;/code&gt; (built-in active), running both, and comparing the output.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Figure:&lt;/strong&gt; A table comparing the output of the same calling code run against the net48 polyfill and the net10.0 built-in. LeftJoin, RightJoin, and Shuffle all produce identical results, boundary cases included.&lt;/p&gt;

&lt;p&gt;The implementation above, built as-is for &lt;code&gt;net48&lt;/code&gt; and built for &lt;code&gt;net10.0&lt;/code&gt; where &lt;code&gt;#if&lt;/code&gt; switches it to the built-in, run through one and the same driver. Measured with .NET SDK 10.0.302.&lt;/p&gt;

&lt;p&gt;The diagram is rendered in the &lt;a href="https://s-iguchi09.github.io/articles/linq-backport-netframework-to-net10/" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;Shuffle&lt;/code&gt; draws on randomness, so its output is re-sorted before the comparison. The two sides also agree on the default value handed to a row that has no counterpart.&lt;/p&gt;

&lt;p&gt;The result selectors of &lt;code&gt;LeftJoin&lt;/code&gt; / &lt;code&gt;RightJoin&lt;/code&gt; carry the same nullable annotations as the built-ins — &lt;code&gt;TInner?&lt;/code&gt; for &lt;code&gt;LeftJoin&lt;/code&gt;, &lt;code&gt;TOuter?&lt;/code&gt; for &lt;code&gt;RightJoin&lt;/code&gt;.&lt;br&gt;
The signature itself thus documents which side can be missing, and nullable analysis agrees before and after migration.&lt;br&gt;
&lt;code&gt;Shuffle&lt;/code&gt;'s random source branches further on a nested &lt;code&gt;#if NET6_0_OR_GREATER&lt;/code&gt;: where &lt;code&gt;Random.Shared&lt;/code&gt; is unavailable, a &lt;code&gt;[ThreadStatic]&lt;/code&gt; instance provides thread safety.&lt;/p&gt;


&lt;h2&gt;
  
  
  Using &lt;code&gt;LeftJoin&lt;/code&gt; / &lt;code&gt;RightJoin&lt;/code&gt;
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;LeftJoin&lt;/code&gt;: Keep Every Outer (Left) Element
&lt;/h3&gt;

&lt;p&gt;For outer elements with no matching inner element, the result selector receives &lt;code&gt;default(TInner)&lt;/code&gt; (&lt;code&gt;null&lt;/code&gt; for reference types) as its second argument.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Sato"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="n"&gt;DeptId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Suzuki"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="n"&gt;DeptId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Takahashi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DeptId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;99&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// No matching department&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;departments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;DeptId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DeptName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Sales"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;DeptId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DeptName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Engineering"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LeftJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;departments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeptId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeptId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;DeptName&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"(unassigned)"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Sato: Sales&lt;/span&gt;
&lt;span class="c1"&gt;// Suzuki: Engineering&lt;/span&gt;
&lt;span class="c1"&gt;// Takahashi: (unassigned)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Matching SQL's &lt;code&gt;LEFT JOIN ... ON e.DeptId = d.DeptId&lt;/code&gt;, the unmatched "Takahashi" stays in the output.&lt;br&gt;
The second selector argument &lt;code&gt;d&lt;/code&gt; is nullable and must be null-checked before use.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;RightJoin&lt;/code&gt;: Keep Every Inner (Right) Element
&lt;/h3&gt;

&lt;p&gt;For inner elements with no matching outer element, the result selector receives &lt;code&gt;default(TOuter)&lt;/code&gt; as its first argument.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Sato"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;DeptId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Suzuki"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DeptId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;departments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;DeptId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DeptName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Sales"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;DeptId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DeptName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Engineering"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;DeptId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DeptName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"General Affairs"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// No employees assigned&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RightJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;departments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeptId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeptId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeptName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"(vacant)"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Sales: Sato&lt;/span&gt;
&lt;span class="c1"&gt;// Engineering: Suzuki&lt;/span&gt;
&lt;span class="c1"&gt;// General Affairs: (vacant)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RightJoin(outer, inner, ...)&lt;/code&gt; preserves every element of &lt;code&gt;inner&lt;/code&gt;, symmetric to &lt;code&gt;LeftJoin&lt;/code&gt;; the implementation simply pivots the &lt;code&gt;GroupJoin&lt;/code&gt; onto the inner sequence.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;Shuffle&lt;/code&gt; versus Pseudo-Shuffles
&lt;/h2&gt;

&lt;p&gt;Random ordering via &lt;code&gt;OrderBy(_ =&amp;gt; Guid.NewGuid())&lt;/code&gt; is widespread but carries two problems.&lt;br&gt;
It generates a GUID per element and pays for an $O(n \log n)$ sort, and the distribution of generated GUIDs as sort keys guarantees no uniformity of the resulting permutation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Shuffle&lt;/code&gt; uses Fisher–Yates, producing each permutation with equal probability in a single $O(n)$ pass.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;deck&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Enumerable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;52&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shuffled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deck&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Shuffle&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// e.g. [17, 3, 50, 28, ...] (differs per call)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Shuffle&lt;/code&gt; is deferred, but like &lt;code&gt;OrderBy&lt;/code&gt; it buffers the entire source before yielding the first element.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Enumerable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Shuffle&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// Source is enumerated and shuffled here&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;second&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Re-enumerating yields a different order&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because deferred queries re-shuffle on every enumeration, materialize once with &lt;code&gt;ToArray&lt;/code&gt; / &lt;code&gt;ToList&lt;/code&gt; when a fixed order is needed.&lt;br&gt;
The randomness is non-cryptographic; for lotteries or anything requiring unpredictability, use &lt;code&gt;System.Security.Cryptography&lt;/code&gt; randomness instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  The &lt;code&gt;IQueryable&amp;lt;T&amp;gt;&lt;/code&gt; Pitfall
&lt;/h2&gt;

&lt;p&gt;Outer joins live next door to database queries, which gives this polyfill a risk the other backports do not have.&lt;br&gt;
It extends &lt;code&gt;Enumerable&lt;/code&gt; (&lt;code&gt;IEnumerable&amp;lt;T&amp;gt;&lt;/code&gt;), but &lt;code&gt;IQueryable&amp;lt;T&amp;gt;&lt;/code&gt; inherits &lt;code&gt;IEnumerable&amp;lt;T&amp;gt;&lt;/code&gt;, so the compiler happily applies it to Entity Framework queries too.&lt;/p&gt;

&lt;p&gt;What happens then is not a runtime error but &lt;strong&gt;silent performance degradation&lt;/strong&gt;.&lt;br&gt;
With no &lt;code&gt;Queryable&lt;/code&gt; counterpart available, the &lt;code&gt;Enumerable&lt;/code&gt; polyfill binds, and the join is never translated to SQL — it executes client-side.&lt;br&gt;
Entire tables are transferred and joined in memory, and nothing looks wrong until data volume grows.&lt;/p&gt;

&lt;p&gt;For server-side outer joins in database queries below .NET 10, keep writing the provider-translatable &lt;code&gt;GroupJoin(...).SelectMany(..., DefaultIfEmpty())&lt;/code&gt; form.&lt;br&gt;
&lt;code&gt;AsEnumerable&lt;/code&gt; only draws the client-evaluation boundary; it does not keep the join on the server.&lt;br&gt;
.NET 10 does add &lt;code&gt;LeftJoin&lt;/code&gt; / &lt;code&gt;RightJoin&lt;/code&gt; to &lt;code&gt;Queryable&lt;/code&gt; as well, but their translatability depends on the provider, and this article's scope is &lt;code&gt;Enumerable&lt;/code&gt; only.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migration Guard
&lt;/h2&gt;

&lt;p&gt;The polyfill is wrapped in &lt;code&gt;#if !NET10_0_OR_GREATER&lt;/code&gt;.&lt;br&gt;
&lt;code&gt;LeftJoin&lt;/code&gt;, &lt;code&gt;RightJoin&lt;/code&gt; and &lt;code&gt;Shuffle&lt;/code&gt; do not exist before .NET 10, so the wrong guard breaks compilation. &lt;code&gt;!NETCOREAPP&lt;/code&gt; disables the polyfill on every target where &lt;code&gt;NETCOREAPP&lt;/code&gt; is defined (.NET Core and .NET 5–9), whereas &lt;code&gt;!NET9_0_OR_GREATER&lt;/code&gt; disables it only on &lt;code&gt;net9.0&lt;/code&gt; and later (on .NET 8 the &lt;code&gt;NET9_0_OR_GREATER&lt;/code&gt; symbol is undefined, so the polyfill stays active; on .NET 9 it is disabled and fails to compile). Both switch the polyfill off where the operators are missing, so the correct guard is &lt;code&gt;#if !NET10_0_OR_GREATER&lt;/code&gt;.&lt;br&gt;
The general rule — disable at and above the version that introduced the methods — is laid out in the &lt;a href="https://s-iguchi09.github.io/articles/linq-backport-netframework-to-net6/" rel="noopener noreferrer"&gt;.NET 6 backport article&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Join direction&lt;/strong&gt;: &lt;code&gt;LeftJoin&lt;/code&gt; preserves every element of the first argument (&lt;code&gt;outer&lt;/code&gt;); &lt;code&gt;RightJoin&lt;/code&gt; preserves every element of the second (&lt;code&gt;inner&lt;/code&gt;). The nullable selector argument is the inner element for &lt;code&gt;LeftJoin&lt;/code&gt; and the outer element for &lt;code&gt;RightJoin&lt;/code&gt; — null-check before use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key equality&lt;/strong&gt;: the comparer-free overloads use &lt;code&gt;EqualityComparer&amp;lt;TKey&amp;gt;.Default&lt;/code&gt;. Pass an &lt;code&gt;IEqualityComparer&amp;lt;TKey&amp;gt;&lt;/code&gt; for case-insensitive joins and the like. Delegation from the smaller overloads pins resolution with the named argument &lt;code&gt;comparer:&lt;/code&gt; (the same technique used in the &lt;a href="https://s-iguchi09.github.io/articles/linq-backport-netframework-to-net8/" rel="noopener noreferrer"&gt;ToDictionary backport&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Shuffle&lt;/code&gt; cannot handle infinite sequences&lt;/strong&gt;: the entire source is buffered at enumeration start, so an unbounded sequence never completes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compile with C# 9 or later&lt;/strong&gt;: the nullable annotations on unconstrained type parameters (&lt;code&gt;TInner?&lt;/code&gt; / &lt;code&gt;TOuter?&lt;/code&gt;) fail with errors such as &lt;code&gt;CS8627&lt;/code&gt; under the .NET Framework 4.8 default &lt;code&gt;LangVersion&lt;/code&gt; (7.3). Set &lt;code&gt;&amp;lt;LangVersion&amp;gt;9.0&amp;lt;/LangVersion&amp;gt;&lt;/code&gt; (or &lt;code&gt;latest&lt;/code&gt;) in the &lt;code&gt;.csproj&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No name collisions&lt;/strong&gt;: these signatures do not exist in .NET Framework, and they differ from &lt;code&gt;Join&lt;/code&gt; and &lt;code&gt;OrderBy&lt;/code&gt; in name and parameters, so overload resolution is unaffected.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Alternatives / Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hand-rolled polyfill (this article)&lt;/td&gt;
&lt;td&gt;No dependency; the name states the SQL-equivalent intent&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;IQueryable&lt;/code&gt; misapplication risk must be managed&lt;/td&gt;
&lt;td&gt;Mostly in-memory joins and shuffles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write &lt;code&gt;GroupJoin&lt;/code&gt; + &lt;code&gt;DefaultIfEmpty&lt;/code&gt; inline&lt;/td&gt;
&lt;td&gt;No extra code; translates under &lt;code&gt;IQueryable&lt;/code&gt; too&lt;/td&gt;
&lt;td&gt;Verbose; easy to get subtly wrong&lt;/td&gt;
&lt;td&gt;Outer joins in database queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Substitute &lt;code&gt;OrderBy(_ =&amp;gt; Guid.NewGuid())&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No extra code&lt;/td&gt;
&lt;td&gt;Inefficient; no uniformity guarantee&lt;/td&gt;
&lt;td&gt;Small collections where rigor is irrelevant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adopt MoreLINQ or similar&lt;/td&gt;
&lt;td&gt;Implemented and tested&lt;/td&gt;
&lt;td&gt;External dependency; API differs from the built-ins&lt;/td&gt;
&lt;td&gt;Projects already accepting the dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upgrade to .NET 10&lt;/td&gt;
&lt;td&gt;Root fix; &lt;code&gt;Queryable&lt;/code&gt; versions available&lt;/td&gt;
&lt;td&gt;Migration cost&lt;/td&gt;
&lt;td&gt;When migration is feasible&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Until a .NET 10 migration, the pragmatic split is: polyfill for in-memory collections, classic &lt;code&gt;GroupJoin&lt;/code&gt; idiom for database queries.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;.NET 10's &lt;code&gt;LeftJoin&lt;/code&gt;, &lt;code&gt;RightJoin&lt;/code&gt; and &lt;code&gt;Shuffle&lt;/code&gt; promote operations that were standard in SQL — or imitated through pseudo-idioms — to first-class LINQ operators.&lt;br&gt;
The backport rests on three points.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;LeftJoin&lt;/code&gt; / &lt;code&gt;RightJoin&lt;/code&gt; embed the classic &lt;code&gt;GroupJoin&lt;/code&gt; + &lt;code&gt;SelectMany&lt;/code&gt; + &lt;code&gt;DefaultIfEmpty&lt;/code&gt; idiom and use nullable annotations to state which side can be missing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Shuffle&lt;/code&gt; performs a uniform Fisher–Yates shuffle, fixing both the inefficiency and the bias of &lt;code&gt;Guid.NewGuid()&lt;/code&gt; sorting&lt;/li&gt;
&lt;li&gt;The polyfill is &lt;code&gt;Enumerable&lt;/code&gt;-only; applied to an &lt;code&gt;IQueryable&amp;lt;T&amp;gt;&lt;/code&gt; database query it falls back to client evaluation — keep the classic idiom for database queries&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Side preserved&lt;/th&gt;
&lt;th&gt;Nullable selector argument&lt;/th&gt;
&lt;th&gt;Evaluation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LeftJoin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Outer (left)&lt;/td&gt;
&lt;td&gt;Inner element &lt;code&gt;TInner?&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Deferred&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RightJoin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inner (right)&lt;/td&gt;
&lt;td&gt;Outer element &lt;code&gt;TOuter?&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Deferred&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Shuffle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Deferred (full buffering at enumeration)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/linq-backport-netframework-to-net5/" rel="noopener noreferrer"&gt;Designing LINQ Polyfills That Preserve Lazy Evaluation — Implementing Append, Prepend, TakeLast and SkipLast&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/linq-backport-netframework-to-net6/" rel="noopener noreferrer"&gt;Replacing GroupBy and Full-Sort Workarounds — Implementing Chunk, MaxBy, MinBy and DistinctBy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/linq-backport-netframework-to-net7/" rel="noopener noreferrer"&gt;Order and OrderDescending by Pure Delegation — A Minimal Polyfill with IOrderedEnumerable Compatibility&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/linq-backport-netframework-to-net8/" rel="noopener noreferrer"&gt;Selector-Free ToDictionary — Designing for Overload Resolution and the notnull Constraint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/linq-backport-netframework-to-net9/" rel="noopener noreferrer"&gt;Key-Based Aggregation Without GroupBy — Dictionary-Backed CountBy, AggregateBy and Index&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-radiobutton-enum-binding/" rel="noopener noreferrer"&gt;Why a WPF RadioButton Bound to an Enum Shows No Initial Selection — The Role of GroupName&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/csharp-operators-initialization-syntax-by-version/" rel="noopener noreferrer"&gt;C# Operators and Initialization Syntax by Version&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/csharp14-extension-members-static-class-limitation/" rel="noopener noreferrer"&gt;C# 14 Extension Members on Static Classes: Static Members Only&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>linq</category>
    </item>
    <item>
      <title>Why WPF Validation Errors Are Not Displayed, and Choosing Between IDataErrorInfo and INotifyDataErrorInfo</title>
      <dc:creator>s-iguchi09</dc:creator>
      <pubDate>Mon, 14 Sep 2026 16:54:09 +0000</pubDate>
      <link>https://dev.to/s-iguchi09/why-wpf-validation-errors-are-not-displayed-and-choosing-between-idataerrorinfo-and-475c</link>
      <guid>https://dev.to/s-iguchi09/why-wpf-validation-errors-are-not-displayed-and-choosing-between-idataerrorinfo-and-475c</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://s-iguchi09.github.io/articles/wpf-validation-error-not-displayed/" rel="noopener noreferrer"&gt;s-iguchi09.github.io&lt;/a&gt;.&lt;br&gt;
A &lt;a href="https://s-iguchi09.github.io/ja/articles/wpf-validation-error-not-displayed/" rel="noopener noreferrer"&gt;Japanese version&lt;/a&gt; is also available.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;A required-field check is in place, yet the &lt;code&gt;TextBox&lt;/code&gt; never turns red.&lt;br&gt;
The symptom splits into two variants.&lt;br&gt;
In one, a breakpoint in the validation code is never hit; in the other, validation clearly runs and &lt;code&gt;Validation.GetHasError&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt;, but the screen does not change.&lt;/p&gt;

&lt;p&gt;Neither is a defect in the validation logic.&lt;br&gt;
WPF validation is split into three independent parts: the path that produces an error, the place that stores it, and the place that draws it.&lt;br&gt;
When one of them is missing, the other two keep working correctly while the UI stays silent.&lt;/p&gt;

&lt;p&gt;This article breaks the "no error shown" symptom into those three stages, isolates each cause, and compares &lt;code&gt;ValidationRule&lt;/code&gt;, &lt;code&gt;IDataErrorInfo&lt;/code&gt;, &lt;code&gt;INotifyDataErrorInfo&lt;/code&gt;, and exception-based validation.&lt;br&gt;
One more symptom is covered alongside them: all three stages hold, yet the message alone is missing, which is the documented behavior of the default &lt;code&gt;ErrorTemplate&lt;/code&gt;.&lt;br&gt;
Every behavior and default value described here was measured on .NET 10 / Windows 11.&lt;/p&gt;


&lt;h2&gt;
  
  
  Prerequisites / Environment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Framework: .NET 6 or later / WPF (behavior verified on .NET 10 / Windows 11)&lt;/li&gt;
&lt;li&gt;Language: C# 12 or later / XAML (samples use collection expressions; on targets whose default language version is C# 11 or earlier, such as &lt;code&gt;net6.0&lt;/code&gt;, read &lt;code&gt;[]&lt;/code&gt; as &lt;code&gt;new()&lt;/code&gt; and the spread element &lt;code&gt;[.. messages]&lt;/code&gt; as &lt;code&gt;new List&amp;lt;string&amp;gt;(messages)&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Target features: &lt;code&gt;Binding&lt;/code&gt; validation (&lt;code&gt;Validation&lt;/code&gt; attached properties, &lt;code&gt;ValidationRule&lt;/code&gt; subclasses, &lt;code&gt;IDataErrorInfo&lt;/code&gt;, &lt;code&gt;INotifyDataErrorInfo&lt;/code&gt;, &lt;code&gt;ValidatesOnExceptions&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Architecture: MVVM, with the view model holding validation results&lt;/li&gt;
&lt;li&gt;Other constraints: the default &lt;code&gt;ErrorTemplate&lt;/code&gt; is the baseline; custom templates are covered in the implementation section&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;Three &lt;code&gt;TextBox&lt;/code&gt; controls are stacked in one panel against the same state, an empty required name.&lt;br&gt;
Only the binding syntax and the interface implemented by the view model differ.&lt;br&gt;
The validation logic is identical in all three cases: an empty string yields exactly one "required" error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;StackPanel&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Bound to a view model that implements IDataErrorInfo --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TextBox&lt;/span&gt; &lt;span class="na"&gt;x:Name=&lt;/span&gt;&lt;span class="s"&gt;"Plain"&lt;/span&gt;
             &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"{Binding Name, UpdateSourceTrigger=PropertyChanged}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Same view model, with ValidatesOnDataErrors added --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TextBox&lt;/span&gt; &lt;span class="na"&gt;x:Name=&lt;/span&gt;&lt;span class="s"&gt;"WithFlag"&lt;/span&gt;
             &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"{Binding Name, UpdateSourceTrigger=PropertyChanged,
                    ValidatesOnDataErrors=True}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Bound to a view model that implements INotifyDataErrorInfo --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TextBox&lt;/span&gt; &lt;span class="na"&gt;x:Name=&lt;/span&gt;&lt;span class="s"&gt;"Notify"&lt;/span&gt;
             &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"{Binding Name, UpdateSourceTrigger=PropertyChanged}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the comparison, each &lt;code&gt;TextBox&lt;/code&gt; receives its own &lt;code&gt;DataContext&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Plain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataContext&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DataErrorAccount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// implements IDataErrorInfo&lt;/span&gt;
&lt;span class="n"&gt;WithFlag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataContext&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DataErrorAccount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// same type&lt;/span&gt;
&lt;span class="n"&gt;Notify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataContext&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NotifyErrorAccount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// implements INotifyDataErrorInfo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DataErrorAccount&lt;/code&gt; and &lt;code&gt;NotifyErrorAccount&lt;/code&gt; are view models that return the same required-field check through &lt;code&gt;IDataErrorInfo&lt;/code&gt; and &lt;code&gt;INotifyDataErrorInfo&lt;/code&gt; respectively; their definitions are omitted here.&lt;br&gt;
When all three are displayed at once, the default error indication, a red border, appears on the lower two only.&lt;/p&gt;


  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs-iguchi09.github.io%2Fimages%2Farticles%2Fwpf-validation-error-not-displayed%2Fvalidation-error-display.png" alt="Three TextBox controls stacked vertically. The top one, bound with IDataErrorInfo but without ValidatesOnDataErrors, keeps its normal border, while the one with ValidatesOnDataErrors=True and the one backed by INotifyDataErrorInfo are surrounded by a red border." width="474" height="224"&gt;Default error indication for the same "name is empty" state. Only the binding syntax and the interface implemented by the view model differ. The label above each &lt;code&gt;TextBox&lt;/code&gt; was added to the figure to identify the corresponding binding (produced on .NET 10 / Windows 11).
  


&lt;p&gt;For the top &lt;code&gt;TextBox&lt;/code&gt;, &lt;code&gt;Validation.GetHasError&lt;/code&gt; stays &lt;code&gt;false&lt;/code&gt; and &lt;code&gt;Validation.Errors&lt;/code&gt; is empty.&lt;br&gt;
A breakpoint in the &lt;code&gt;IDataErrorInfo&lt;/code&gt; indexer is never hit.&lt;/p&gt;

&lt;p&gt;The two controls that do show a red border have a problem of their own.&lt;br&gt;
&lt;code&gt;Validation.Errors&lt;/code&gt; holds the message, but no message reaches the screen.&lt;/p&gt;

&lt;p&gt;There is also a configuration in which not even the border appears.&lt;br&gt;
In an application that replaces the &lt;code&gt;ControlTemplate&lt;/code&gt; of its &lt;code&gt;Window&lt;/code&gt;, &lt;code&gt;Validation.GetHasError&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt; and &lt;code&gt;Validation.Errors&lt;/code&gt; contains an entry, yet the UI is completely unresponsive to the error.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Three Stages, and Where It Stops
&lt;/h2&gt;

&lt;p&gt;WPF validation consists of three independent stages, all of which must hold before anything reaches the screen.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Production&lt;/strong&gt; — a validation rule associated with the binding runs and creates a &lt;code&gt;ValidationError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt; — the &lt;code&gt;ValidationError&lt;/code&gt; is added to &lt;code&gt;Validation.Errors&lt;/code&gt; on the binding target element, and &lt;code&gt;Validation.HasError&lt;/code&gt; becomes &lt;code&gt;true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rendering&lt;/strong&gt; — &lt;code&gt;Validation.ErrorTemplate&lt;/code&gt; is drawn on the adorner layer of that element.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Stage 2 is handled by the binding engine, so the stages that application code can leave incomplete are 1 and 3.&lt;br&gt;
The sections below separate the symptoms by which of the two is missing.&lt;/p&gt;
&lt;h3&gt;
  
  
  Stage 1: no rule is associated with the binding
&lt;/h3&gt;

&lt;p&gt;This stops at stage 1.&lt;br&gt;
Implementing &lt;code&gt;IDataErrorInfo&lt;/code&gt; does not by itself take part in validation.&lt;br&gt;
The indexer &lt;code&gt;this[string columnName]&lt;/code&gt; is called only once a &lt;code&gt;DataErrorValidationRule&lt;/code&gt; has been added to the binding.&lt;br&gt;
&lt;code&gt;ValidatesOnDataErrors&lt;/code&gt; is the shorthand that adds that rule, and its default value is &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The activation requirements and the measured default behavior of each approach are as follows.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Validation approach&lt;/th&gt;
&lt;th&gt;Rule recorded on the error&lt;/th&gt;
&lt;th&gt;Required setting&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Error present on initial display&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Custom &lt;code&gt;ValidationRule&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The custom class&lt;/td&gt;
&lt;td&gt;Add to &lt;code&gt;Binding.ValidationRules&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Inactive unless added&lt;/td&gt;
&lt;td&gt;No by default; yes with &lt;code&gt;ValidatesOnTargetUpdated="True"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IDataErrorInfo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DataErrorValidationRule&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ValidatesOnDataErrors="True"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;False&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INotifyDataErrorInfo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NotifyDataErrorValidationRule&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ValidatesOnNotifyDataErrors&lt;/code&gt; (on by default)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;True&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type conversion failure&lt;/td&gt;
&lt;td&gt;Internal conversion rule&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Always active&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exception thrown by a setter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ExceptionValidationRule&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ValidatesOnExceptions="True"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;False&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;ValidatesOnNotifyDataErrors&lt;/code&gt; is the only one that defaults to &lt;code&gt;true&lt;/code&gt;.&lt;br&gt;
That is why a view model implementing &lt;code&gt;INotifyDataErrorInfo&lt;/code&gt; produces a red border even when the binding itself declares nothing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;INotifyDataErrorInfo&lt;/code&gt; also differs in how the error comes into being.&lt;br&gt;
&lt;code&gt;NotifyDataErrorValidationRule.Validate&lt;/code&gt; reports success regardless of the value passed to it; in the measured run &lt;code&gt;IsValid&lt;/code&gt; was &lt;code&gt;true&lt;/code&gt; even for &lt;code&gt;null&lt;/code&gt; and an empty string.&lt;br&gt;
The error itself is whatever the view model returns from &lt;code&gt;GetErrors&lt;/code&gt;, which the binding engine reads and then keeps in step with through &lt;code&gt;ErrorsChanged&lt;/code&gt; notifications to update &lt;code&gt;Validation.Errors&lt;/code&gt;.&lt;br&gt;
The rule serves as the marker that appears in &lt;code&gt;RuleInError&lt;/code&gt; on the resulting &lt;code&gt;ValidationError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That read follows a fixed order.&lt;br&gt;
The binding engine consults &lt;code&gt;HasErrors&lt;/code&gt; first and calls &lt;code&gt;GetErrors&lt;/code&gt; only when it returns &lt;code&gt;true&lt;/code&gt;.&lt;br&gt;
In the measured run, an implementation whose &lt;code&gt;GetErrors&lt;/code&gt; returned a message displayed nothing and never had &lt;code&gt;GetErrors&lt;/code&gt; called at all while &lt;code&gt;HasErrors&lt;/code&gt; returned &lt;code&gt;false&lt;/code&gt;.&lt;br&gt;
A &lt;code&gt;false&lt;/code&gt; result clears any notify errors already recorded, and the check repeats on every &lt;code&gt;ErrorsChanged&lt;/code&gt;.&lt;br&gt;
Implementing &lt;code&gt;HasErrors&lt;/code&gt; independently of the per-property validation results is therefore enough on its own to produce a view model that holds errors the UI never shows.&lt;/p&gt;

&lt;p&gt;Behavior on initial display differs by approach.&lt;br&gt;
&lt;code&gt;DataErrorValidationRule&lt;/code&gt; and &lt;code&gt;NotifyDataErrorValidationRule&lt;/code&gt; both reported an error before a single character was typed.&lt;br&gt;
A custom &lt;code&gt;ValidationRule&lt;/code&gt;, by contrast, was not called when the binding was attached; &lt;code&gt;Validate&lt;/code&gt; ran only at the first source update.&lt;br&gt;
This difference explains why a custom rule stays silent when a required field is empty at startup.&lt;/p&gt;

&lt;p&gt;The property behind the difference is &lt;code&gt;ValidationRule.ValidatesOnTargetUpdated&lt;/code&gt;, which decides whether the rule also runs when the target is updated, that is, when the binding is established and when the source value changes.&lt;br&gt;
In the measured run, the built-in &lt;code&gt;DataErrorValidationRule&lt;/code&gt; and &lt;code&gt;NotifyDataErrorValidationRule&lt;/code&gt; both returned &lt;code&gt;true&lt;/code&gt;, while the default for a custom &lt;code&gt;ValidationRule&lt;/code&gt; was &lt;code&gt;false&lt;/code&gt;.&lt;br&gt;
Specifying &lt;code&gt;ValidatesOnTargetUpdated="True"&lt;/code&gt; makes a custom rule validate from startup as well: the rule was called immediately after launch and then re-evaluated on every change of the source value.&lt;/p&gt;

&lt;p&gt;To establish this stage, activate the approach in use.&lt;br&gt;
A design built on &lt;code&gt;IDataErrorInfo&lt;/code&gt; specifies &lt;code&gt;ValidatesOnDataErrors="True"&lt;/code&gt; on the binding.&lt;br&gt;
&lt;code&gt;INotifyDataErrorInfo&lt;/code&gt; is active by default, allows several messages per property, and can report results that arrive asynchronously, such as a server lookup. Choose it when several messages or asynchronous validation are needed.&lt;/p&gt;
&lt;h3&gt;
  
  
  Stage 3: there is no adorner layer
&lt;/h3&gt;

&lt;p&gt;This stops at stage 3.&lt;br&gt;
&lt;code&gt;Validation.ErrorTemplate&lt;/code&gt; does not modify the target control itself; it is drawn on top of it, on the adorner layer.&lt;br&gt;
The most common provider of that layer is &lt;code&gt;AdornerDecorator&lt;/code&gt;, which the default &lt;code&gt;ControlTemplate&lt;/code&gt; of &lt;code&gt;Window&lt;/code&gt; contains.&lt;/p&gt;

&lt;p&gt;Replacing the window template with a custom one and omitting &lt;code&gt;AdornerDecorator&lt;/code&gt; removes the rendering surface.&lt;br&gt;
In the measured run, a template containing only a &lt;code&gt;ContentPresenter&lt;/code&gt; made &lt;code&gt;AdornerLayer.GetAdornerLayer&lt;/code&gt; return &lt;code&gt;null&lt;/code&gt;, and nothing appeared even though &lt;code&gt;Validation.HasError&lt;/code&gt; was &lt;code&gt;true&lt;/code&gt;.&lt;br&gt;
Adding a single &lt;code&gt;AdornerDecorator&lt;/code&gt; to the same template made the layer resolvable and attached one adorner.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;Window.ControlTemplate&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Validation.HasError&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;AdornerLayer.GetAdornerLayer&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Red border on screen&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Resolved&lt;/td&gt;
&lt;td&gt;Shown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replaced, without &lt;code&gt;AdornerDecorator&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not shown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replaced, with &lt;code&gt;AdornerDecorator&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Resolved&lt;/td&gt;
&lt;td&gt;Shown&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The error is produced and stored correctly, so nothing looks wrong from logs or from &lt;code&gt;HasError&lt;/code&gt;.&lt;br&gt;
That asymmetry is what makes this cause hard to locate.&lt;/p&gt;

&lt;p&gt;The window template is not the only source of a layer.&lt;br&gt;
An &lt;code&gt;AdornerDecorator&lt;/code&gt; placed anywhere in the visual tree creates a new layer for the subtree beneath it.&lt;br&gt;
The following content sits inside a &lt;code&gt;Window&lt;/code&gt; whose template omits &lt;code&gt;AdornerDecorator&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;StackPanel&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TextBox&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"{Binding Name, UpdateSourceTrigger=PropertyChanged}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;AdornerDecorator&lt;/span&gt; &lt;span class="na"&gt;HorizontalAlignment=&lt;/span&gt;&lt;span class="s"&gt;"Left"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;TextBox&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"{Binding Name, UpdateSourceTrigger=PropertyChanged}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/AdornerDecorator&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;TextBox&lt;/code&gt; controls bind to the same property of the same view model and hold the same validation error.&lt;br&gt;
The only difference is whether an &lt;code&gt;AdornerDecorator&lt;/code&gt; wraps them.&lt;/p&gt;


  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs-iguchi09.github.io%2Fimages%2Farticles%2Fwpf-validation-error-not-displayed%2Fadorner-layer-required.png" alt="Two TextBox controls stacked vertically. The upper one, which is not wrapped in an AdornerDecorator, keeps its normal border, while the lower one wrapped in an AdornerDecorator is surrounded by a red border." width="355" height="167"&gt;Only the lower control, wrapped in an &lt;code&gt;AdornerDecorator&lt;/code&gt;, draws a red border. Before the capture, &lt;code&gt;Validation.HasError&lt;/code&gt; was confirmed to be &lt;code&gt;true&lt;/code&gt; on both and &lt;code&gt;AdornerLayer.GetAdornerLayer&lt;/code&gt; to return &lt;code&gt;null&lt;/code&gt; for the upper one, so the difference comes from the rendering surface rather than from the error itself (produced on .NET 10 / Windows 11).
  


&lt;p&gt;&lt;code&gt;AdornerDecorator&lt;/code&gt; is not the only provider.&lt;br&gt;
The &lt;code&gt;ScrollContentPresenter&lt;/code&gt; inside a &lt;code&gt;ScrollViewer&lt;/code&gt; carries a layer as well.&lt;br&gt;
In the measured run, a &lt;code&gt;TextBox&lt;/code&gt; placed inside a &lt;code&gt;ScrollViewer&lt;/code&gt; drew its red border even under a &lt;code&gt;Window&lt;/code&gt; template that omits &lt;code&gt;AdornerDecorator&lt;/code&gt;.&lt;br&gt;
Results therefore diverge between the inside and the outside of a &lt;code&gt;ScrollViewer&lt;/code&gt; within one screen, and the symptom can surface as "only some fields lack the border".&lt;br&gt;
When the template has been replaced but the symptom does not reproduce, check whether the control sits inside a &lt;code&gt;ScrollViewer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To establish this stage, something above the target element must supply an &lt;code&gt;AdornerLayer&lt;/code&gt;.&lt;br&gt;
Including &lt;code&gt;AdornerDecorator&lt;/code&gt; when the &lt;code&gt;Window&lt;/code&gt; template has been replaced is the usual remedy, but as noted above, inside a &lt;code&gt;ScrollViewer&lt;/code&gt; the &lt;code&gt;ScrollContentPresenter&lt;/code&gt; supplies the layer and that alone is enough.&lt;/p&gt;
&lt;h3&gt;
  
  
  Before stage 1: the source has not been updated yet
&lt;/h3&gt;

&lt;p&gt;This concerns when stage 1 is reached.&lt;br&gt;
As described earlier, the two built-in rules have &lt;code&gt;ValidatesOnTargetUpdated&lt;/code&gt; set to &lt;code&gt;true&lt;/code&gt;, so they are also evaluated when the binding is established and when the source value changes.&lt;br&gt;
Input typed by the user, however, is validated only when the value is transferred from the target to the source.&lt;br&gt;
The default &lt;code&gt;UpdateSourceTrigger&lt;/code&gt; for &lt;code&gt;TextBox.Text&lt;/code&gt; is &lt;code&gt;LostFocus&lt;/code&gt;, so typing alone updates neither the source nor the validation state of that input.&lt;br&gt;
In the measured run, clearing a &lt;code&gt;TextBox&lt;/code&gt; that started with a valid value left &lt;code&gt;Validation.HasError&lt;/code&gt; at &lt;code&gt;false&lt;/code&gt; until focus moved away.&lt;/p&gt;

&lt;p&gt;This applies to &lt;code&gt;IDataErrorInfo&lt;/code&gt; and, as in the implementation below, to an &lt;code&gt;INotifyDataErrorInfo&lt;/code&gt; that validates inside its setters.&lt;br&gt;
The latter follows &lt;code&gt;ErrorsChanged&lt;/code&gt; on the view model, but in that arrangement the event is raised by the property setter, and the setter is invoked by the source update.&lt;br&gt;
With &lt;code&gt;UpdateSourceTrigger=Explicit&lt;/code&gt; the effect is stronger: in the same arrangement the validation state did not change until &lt;code&gt;UpdateSource&lt;/code&gt; was called.&lt;br&gt;
An implementation that raises &lt;code&gt;ErrorsChanged&lt;/code&gt; independently of the source update, such as one that reports when an asynchronous lookup completes, is not bound by this.&lt;/p&gt;



&lt;p&gt;Which stage a case stops at can be told apart by reading &lt;code&gt;Validation.HasError&lt;/code&gt;, the &lt;code&gt;Validation.Errors&lt;/code&gt; count, and the number of adorners separately.&lt;br&gt;
The figure below records those for a &lt;code&gt;TextBox&lt;/code&gt; bound to a source that always reports an error.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Figure:&lt;/strong&gt; A table of HasError, the Errors count, and the adorner count per validation setup. IDataErrorInfo alone gives False and 0 errors. Turning on ValidatesOnDataErrors gives True, 1, and 1. INotifyDataErrorInfo gives True, 1, and 1 by default. ValidationRules gives True, 1, and 1. Setting ErrorTemplate to null keeps True and 1 but drops the adorner count to 0.&lt;/p&gt;

&lt;p&gt;Measured on .NET 10 / Windows 11 with a &lt;code&gt;TextBox&lt;/code&gt; bound to a source that always reports an error. &lt;code&gt;adorners&lt;/code&gt; is the count returned by &lt;code&gt;AdornerLayer.GetAdorners&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The diagram is rendered in the &lt;a href="https://s-iguchi09.github.io/articles/wpf-validation-error-not-displayed/" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The first row shows 0 errors.&lt;/strong&gt; Implementing &lt;code&gt;IDataErrorInfo&lt;/code&gt; is not enough to reach stage 1 without &lt;code&gt;ValidatesOnDataErrors&lt;/code&gt;.&lt;br&gt;
The second row turns it on: one error appears, and one adorner is drawn.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;INotifyDataErrorInfo&lt;/code&gt; behaves differently, as the third row shows. Its counterpart &lt;code&gt;ValidatesOnNotifyDataErrors&lt;/code&gt; defaults to enabled, so implementing the interface is enough to take part in validation.&lt;br&gt;
The two interfaces differing in their default is part of what makes this hard to diagnose.&lt;/p&gt;

&lt;p&gt;The last row is a case that reaches stage 3 and stops there. &lt;code&gt;HasError&lt;/code&gt; is &lt;code&gt;True&lt;/code&gt; and &lt;code&gt;Errors&lt;/code&gt; still holds one entry, but the adorner count is 0.&lt;br&gt;
&lt;strong&gt;The error is held and simply not drawn.&lt;/strong&gt; A value that is invalid without any red outline appearing corresponds to this row.&lt;/p&gt;

&lt;p&gt;To get past this point, specify &lt;code&gt;UpdateSourceTrigger=PropertyChanged&lt;/code&gt; on the &lt;code&gt;TextBox.Text&lt;/code&gt; binding to report results while the user is still typing.&lt;/p&gt;


&lt;h2&gt;
  
  
  Implementing All Three Stages
&lt;/h2&gt;

&lt;p&gt;With the three in place, supply an &lt;code&gt;ErrorTemplate&lt;/code&gt; that renders the message.&lt;br&gt;
The default template draws only a red border and never shows the contents of &lt;code&gt;Validation.Errors&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first piece is a base class that stores validation results.&lt;br&gt;
&lt;code&gt;INotifyDataErrorInfo&lt;/code&gt; is designed to return a set of messages per property name, so a dictionary keeps the implementation small.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// With ImplicitUsings disabled, System and System.Collections.Generic are also required.&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Collections&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.ComponentModel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.CompilerServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ValidatableBase&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;INotifyPropertyChanged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;INotifyDataErrorInfo&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;HasErrors&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt; &lt;span class="nf"&gt;GetErrors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;propertyName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;propertyName&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;propertyName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="n"&gt;PropertyChangedEventHandler&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;PropertyChanged&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="n"&gt;EventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DataErrorsChangedEventArgs&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;ErrorsChanged&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="n"&gt;SetProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CallerMemberName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;propertyName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EqualityComparer&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;PropertyChanged&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PropertyChangedEventArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;propertyName&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;propertyName&lt;/span&gt;&lt;span class="p"&gt;!);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetErrors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;propertyName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;propertyName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;propertyName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[..&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;ErrorsChanged&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DataErrorsChangedEventArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;propertyName&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;PropertyChanged&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PropertyChangedEventArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HasErrors&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;propertyName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ErrorsChanged&lt;/code&gt; must be raised when an error is cleared, not only when one is added.&lt;br&gt;
Omitting that leaves the red border in place.&lt;br&gt;
The change notification for &lt;code&gt;HasErrors&lt;/code&gt; supports binding the enabled state of a save button directly to that property.&lt;br&gt;
Driving &lt;code&gt;CanExecute&lt;/code&gt; of an &lt;code&gt;ICommand&lt;/code&gt; instead requires a separate trigger for re-evaluation, such as &lt;code&gt;CommandManager.InvalidateRequerySuggested&lt;/code&gt;, because the notification alone does not re-query the command (&lt;a href="https://s-iguchi09.github.io/articles/wpf-relaycommand-canexecute-not-updating/" rel="noopener noreferrer"&gt;Fixing a RelayCommand Whose CanExecute Does Not Update the Button State in WPF&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The property name passed to &lt;code&gt;SetErrors&lt;/code&gt; must match the binding path exactly; the behavior on mismatch is covered in the notes.&lt;/p&gt;

&lt;p&gt;A derived class only describes the validation of its own properties.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AccountViewModel&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ValidatableBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;AccountViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;get&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;propertyName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;propertyName&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Name is required."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Name must be 20 characters or fewer."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nf"&gt;SetErrors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Validate&lt;/code&gt; is called from the constructor so that &lt;code&gt;HasErrors&lt;/code&gt; is correct from the initial state, which blocks the save operation immediately after startup.&lt;/p&gt;

&lt;p&gt;On the XAML side, define an &lt;code&gt;ErrorTemplate&lt;/code&gt; that draws the message and apply it through a &lt;code&gt;Style&lt;/code&gt;.&lt;br&gt;
&lt;code&gt;AdornedElementPlaceholder&lt;/code&gt; marks where the original control sits, and any decoration can be placed around it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;StackPanel&lt;/span&gt; &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"16"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;StackPanel.Resources&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ControlTemplate&lt;/span&gt; &lt;span class="na"&gt;x:Key=&lt;/span&gt;&lt;span class="s"&gt;"FieldErrorTemplate"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;StackPanel&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;Border&lt;/span&gt; &lt;span class="na"&gt;BorderBrush=&lt;/span&gt;&lt;span class="s"&gt;"#D13438"&lt;/span&gt; &lt;span class="na"&gt;BorderThickness=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;AdornedElementPlaceholder&lt;/span&gt; &lt;span class="na"&gt;x:Name=&lt;/span&gt;&lt;span class="s"&gt;"Adorned"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/Border&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;TextBlock&lt;/span&gt; &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"2,2,0,0"&lt;/span&gt; &lt;span class="na"&gt;FontSize=&lt;/span&gt;&lt;span class="s"&gt;"11"&lt;/span&gt; &lt;span class="na"&gt;Foreground=&lt;/span&gt;&lt;span class="s"&gt;"#D13438"&lt;/span&gt;
                           &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"{Binding ElementName=Adorned,
                                  Path=AdornedElement.(Validation.Errors)/ErrorContent}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/ControlTemplate&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;Style&lt;/span&gt; &lt;span class="na"&gt;TargetType=&lt;/span&gt;&lt;span class="s"&gt;"TextBox"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;Setter&lt;/span&gt; &lt;span class="na"&gt;Property=&lt;/span&gt;&lt;span class="s"&gt;"Validation.ErrorTemplate"&lt;/span&gt; &lt;span class="na"&gt;Value=&lt;/span&gt;&lt;span class="s"&gt;"{StaticResource FieldErrorTemplate}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;Setter&lt;/span&gt; &lt;span class="na"&gt;Property=&lt;/span&gt;&lt;span class="s"&gt;"Margin"&lt;/span&gt; &lt;span class="na"&gt;Value=&lt;/span&gt;&lt;span class="s"&gt;"0,0,0,22"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/Style&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/StackPanel.Resources&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;TextBox&lt;/span&gt; &lt;span class="na"&gt;Width=&lt;/span&gt;&lt;span class="s"&gt;"240"&lt;/span&gt; &lt;span class="na"&gt;HorizontalAlignment=&lt;/span&gt;&lt;span class="s"&gt;"Left"&lt;/span&gt;
             &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"{Binding Name, UpdateSourceTrigger=PropertyChanged}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parentheses in &lt;code&gt;AdornedElement.(Validation.Errors)&lt;/code&gt; denote an attached property, and the trailing &lt;code&gt;/ErrorContent&lt;/code&gt; refers to the current item of the collection, which by default is the first error.&lt;br&gt;
Running this XAML with an &lt;code&gt;AccountViewModel&lt;/code&gt; as its &lt;code&gt;DataContext&lt;/code&gt; rendered &lt;code&gt;Name is required.&lt;/code&gt; under the &lt;code&gt;TextBox&lt;/code&gt; right after startup and &lt;code&gt;Name must be 20 characters or fewer.&lt;/code&gt; once 21 characters were entered; entering a valid value removed the adorner entirely.&lt;/p&gt;

&lt;p&gt;The bottom margin in the &lt;code&gt;Style&lt;/code&gt; reserves space for the message.&lt;br&gt;
Adorners take no part in layout, so without that margin the message overlaps the element below it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The default &lt;code&gt;ErrorTemplate&lt;/code&gt; does not display a message.&lt;/strong&gt;
It draws only a red border on the adorner layer.
In the measured run, the &lt;code&gt;ToolTip&lt;/code&gt; of the failing &lt;code&gt;TextBox&lt;/code&gt; stayed &lt;code&gt;null&lt;/code&gt;.
Displaying the message requires either replacing the &lt;code&gt;ErrorTemplate&lt;/code&gt; or setting &lt;code&gt;ToolTip&lt;/code&gt; from a &lt;code&gt;Style&lt;/code&gt; trigger on &lt;code&gt;Validation.HasError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adorners do not expand the layout.&lt;/strong&gt;
Measuring &lt;code&gt;ActualHeight&lt;/code&gt; of the parent panel with and without an error produced the same value.
An &lt;code&gt;ErrorTemplate&lt;/code&gt; that grows vertically therefore overlaps the elements below unless space is reserved in advance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;(Validation.Errors)[0].ErrorContent&lt;/code&gt; reports a binding error when the error clears.&lt;/strong&gt;
A path that addresses the first element by index is re-evaluated the moment the collection becomes empty, which logged &lt;code&gt;System.Windows.Data Error: 17&lt;/code&gt; to the output window.
The display itself clears correctly, so the trace is easy to miss.
Rewriting the path as &lt;code&gt;/ErrorContent&lt;/code&gt;, which refers to the current item, keeps the same display and produces no trace.
How to read output window messages is covered in &lt;a href="https://s-iguchi09.github.io/articles/wpf-binding-error-debugging-output-window/" rel="noopener noreferrer"&gt;Reading WPF Binding Errors and Diagnosing Them with the Output Window&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Mode=OneWay&lt;/code&gt; does not validate user input, and a red border once shown never clears.&lt;/strong&gt;
&lt;code&gt;OneWay&lt;/code&gt; has no target-to-source transfer, so typed input never becomes subject to validation.
That does not mean validation never runs.
Through &lt;code&gt;ValidatesOnTargetUpdated&lt;/code&gt; described above, the rules were evaluated under &lt;code&gt;OneWay&lt;/code&gt; both when the binding was established and when the source property changed, producing a red border for an invalid value.
The problem is that no user action clears that border: typing a valid string into a &lt;code&gt;TextBox&lt;/code&gt; bound with &lt;code&gt;OneWay&lt;/code&gt; left &lt;code&gt;Validation.HasError&lt;/code&gt; at &lt;code&gt;true&lt;/code&gt;.
A field switched to &lt;code&gt;OneWay&lt;/code&gt; for display purposes therefore keeps its error indication permanently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assigning to the target of a &lt;code&gt;OneWay&lt;/code&gt; binding from code removes the binding.&lt;/strong&gt;
After a plain assignment to &lt;code&gt;TextBox.Text&lt;/code&gt;, &lt;code&gt;BindingOperations.GetBinding&lt;/code&gt; returned &lt;code&gt;null&lt;/code&gt; and the red border disappeared.
&lt;code&gt;OneTime&lt;/code&gt; behaves the same way.
User input and &lt;code&gt;SetCurrentValue&lt;/code&gt; both keep the binding intact, so only assignment from code produces this result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An &lt;code&gt;ErrorsChanged&lt;/code&gt; property name that differs from the binding path suppresses the display.&lt;/strong&gt;
Raising &lt;code&gt;ErrorsChanged&lt;/code&gt; with &lt;code&gt;Namee&lt;/code&gt; while the binding path was &lt;code&gt;Name&lt;/code&gt; left &lt;code&gt;Validation.HasError&lt;/code&gt; at &lt;code&gt;false&lt;/code&gt; even though &lt;code&gt;HasErrors&lt;/code&gt; was &lt;code&gt;true&lt;/code&gt;.
Using &lt;code&gt;nameof&lt;/code&gt; instead of string literals prevents this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation results settled asynchronously must be applied on the UI thread.&lt;/strong&gt;
The binding engine subscribes to &lt;code&gt;ErrorsChanged&lt;/code&gt; to update &lt;code&gt;Validation.Errors&lt;/code&gt;, so that notification has to be raised on the UI thread.
When results are settled by background work, move the whole &lt;code&gt;SetErrors&lt;/code&gt; call — the dictionary update and both notifications — onto the UI thread through the &lt;code&gt;Dispatcher&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;Validation.Error&lt;/code&gt; attached event is not raised by default.&lt;/strong&gt;
Handling errors outside the visual layer, for logging or for blocking navigation, requires &lt;code&gt;Binding.NotifyOnValidationError&lt;/code&gt; to be &lt;code&gt;True&lt;/code&gt;.
The default is &lt;code&gt;False&lt;/code&gt;, and without it the handler is never invoked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exceptions thrown by a setter are swallowed by default.&lt;/strong&gt;
Feeding an invalid value to a view model whose setter throws &lt;code&gt;ArgumentOutOfRangeException&lt;/code&gt; left &lt;code&gt;Validation.HasError&lt;/code&gt; at &lt;code&gt;false&lt;/code&gt; without &lt;code&gt;ValidatesOnExceptions&lt;/code&gt;, and the value was not updated.
With &lt;code&gt;ValidatesOnExceptions="True"&lt;/code&gt;, &lt;code&gt;ExceptionValidationRule&lt;/code&gt; caught the exception and &lt;code&gt;Exception.Message&lt;/code&gt; became the error content.
Messages from the &lt;code&gt;ArgumentException&lt;/code&gt; family carry a suffix such as &lt;code&gt;(Parameter 'value')&lt;/code&gt;, which is rarely acceptable as user-facing text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only type conversion failures surface without any setting.&lt;/strong&gt;
Typing &lt;code&gt;abc&lt;/code&gt; into a &lt;code&gt;TextBox&lt;/code&gt; bound to an &lt;code&gt;int&lt;/code&gt; property produced an error with neither &lt;code&gt;ValidatesOnExceptions&lt;/code&gt; nor any validation rule in place, because the binding engine treats a conversion failure as a validation error.
That message is generated by the framework and localized to the running UI language, appearing as &lt;code&gt;Value 'abc' could not be converted.&lt;/code&gt; under &lt;code&gt;en-US&lt;/code&gt;, so replacing it with domain wording requires a custom &lt;code&gt;ValidationRule&lt;/code&gt; or an &lt;code&gt;IValueConverter&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A custom &lt;code&gt;ValidationRule&lt;/code&gt; receives the value before conversion.&lt;/strong&gt;
The default &lt;code&gt;ValidationStep&lt;/code&gt; is &lt;code&gt;RawProposedValue&lt;/code&gt;, and &lt;code&gt;Validate&lt;/code&gt; received a &lt;code&gt;string&lt;/code&gt; even though the binding targeted an &lt;code&gt;int&lt;/code&gt; property.
Validating it as a number requires parsing inside the method or setting &lt;code&gt;ValidationStep="ConvertedProposedValue"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DataGrid&lt;/code&gt; cells behave differently depending on the column type.&lt;/strong&gt;
For columns where the framework creates the editing control at run time, such as &lt;code&gt;DataGridTextColumn&lt;/code&gt;, the official documentation states that &lt;code&gt;Validation.ErrorTemplate&lt;/code&gt; cannot be used the way it is with simple controls and that no dedicated error template exists for cells. Feedback then goes through &lt;code&gt;DataGridBoundColumn.EditingElementStyle&lt;/code&gt; per cell and &lt;code&gt;DataGrid.RowValidationErrorTemplate&lt;/code&gt; per row.
A &lt;code&gt;DataGridTemplateColumn&lt;/code&gt;, by contrast, carries a hand-written editing control in &lt;code&gt;CellEditingTemplate&lt;/code&gt;, and setting &lt;code&gt;Validation.ErrorTemplate&lt;/code&gt; on that control drew the message inside the cell being edited. &lt;code&gt;EditingElementStyle&lt;/code&gt; is a member of &lt;code&gt;DataGridBoundColumn&lt;/code&gt; and does not exist on this column type.
Switching controls between display and edit modes is covered in &lt;a href="https://s-iguchi09.github.io/articles/wpf-datagrid-cell-editing-template/" rel="noopener noreferrer"&gt;Switching Controls Between Display and Edit Modes in WPF DataGrid Cells&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation timing follows &lt;code&gt;UpdateSourceTrigger&lt;/code&gt;.&lt;/strong&gt;
Whether results appear while typing or after focus leaves is a decision about update timing, not about presentation.
The differences between the values are covered in &lt;a href="https://s-iguchi09.github.io/articles/wpf-textbox-updatesourcetrigger-binding-timing/" rel="noopener noreferrer"&gt;Controlling When TextBox Input Reaches the Source with UpdateSourceTrigger in WPF&lt;/a&gt;.
Driving the update from the view when &lt;code&gt;Explicit&lt;/code&gt; is chosen is covered in &lt;a href="https://s-iguchi09.github.io/articles/wpf-textbox-updatesource-from-view-pitfalls/" rel="noopener noreferrer"&gt;Calling TextBox UpdateSource from the View in WPF: Implementation and Pitfalls&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Alternatives / Comparison
&lt;/h2&gt;

&lt;p&gt;The four approaches that produce errors compare as follows.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Activation&lt;/th&gt;
&lt;th&gt;Multiple messages per property&lt;/th&gt;
&lt;th&gt;Deferred or async validation&lt;/th&gt;
&lt;th&gt;Best suited for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Custom &lt;code&gt;ValidationRule&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Add to &lt;code&gt;ValidationRules&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No (only one entry reaches &lt;code&gt;Validation.Errors&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Input format checks kept inside the view&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IDataErrorInfo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ValidatesOnDataErrors="True"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No (a single &lt;code&gt;string&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Existing code already built on &lt;code&gt;IDataErrorInfo&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INotifyDataErrorInfo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Active by default&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes, reported later through &lt;code&gt;ErrorsChanged&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;New implementations where the view model owns validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ValidatesOnExceptions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ValidatesOnExceptions="True"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Domain models that enforce invariants in setters&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Registering several custom &lt;code&gt;ValidationRule&lt;/code&gt; objects still yields at most one entry in &lt;code&gt;Validation.Errors&lt;/code&gt;, because no rule is evaluated after an earlier one fails.&lt;br&gt;
Carrying several messages in a single error is possible instead, since &lt;code&gt;ValidationResult.ErrorContent&lt;/code&gt; is typed as &lt;code&gt;object&lt;/code&gt; and accepts a collection.&lt;br&gt;
Doing so requires a display side that can render a collection, such as an &lt;code&gt;ItemsControl&lt;/code&gt;; binding &lt;code&gt;ErrorContent&lt;/code&gt; to &lt;code&gt;TextBlock.Text&lt;/code&gt; as the implementation above does would show the type name.&lt;/p&gt;

&lt;p&gt;Being active by default is an advantage of &lt;code&gt;INotifyDataErrorInfo&lt;/code&gt; and, at the same time, a path for unintended validation.&lt;br&gt;
Once a view model base class implements the interface, validation is enabled even where nothing is written on the individual bindings.&lt;br&gt;
Disabling it for a specific binding requires an explicit &lt;code&gt;ValidatesOnNotifyDataErrors="False"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Several approaches can be active at once.&lt;br&gt;
Combining &lt;code&gt;IDataErrorInfo&lt;/code&gt; with &lt;code&gt;INotifyDataErrorInfo&lt;/code&gt; accumulated two entries in &lt;code&gt;Validation.Errors&lt;/code&gt; in the measured run.&lt;br&gt;
Adding a custom rule at the &lt;code&gt;RawProposedValue&lt;/code&gt; step to that configuration and making it fail, however, removed the &lt;code&gt;DataErrorValidationRule&lt;/code&gt; entry.&lt;br&gt;
The &lt;code&gt;UpdatedValue&lt;/code&gt; step sits later on the target-to-source validation path, and the path stops at the first failing step.&lt;br&gt;
&lt;code&gt;NotifyDataErrorValidationRule&lt;/code&gt; occupies the same &lt;code&gt;UpdatedValue&lt;/code&gt; step, yet its entry survived, because that error is supplied from the state the view model holds through &lt;code&gt;ErrorsChanged&lt;/code&gt; and is therefore maintained independently of that path.&lt;br&gt;
An &lt;code&gt;ErrorTemplate&lt;/code&gt; that shows only the first entry therefore depends on this evaluation order for which message appears, so keeping to a single approach is easier to reason about unless the display requirements demand otherwise.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Start from the value of &lt;code&gt;Validation.GetHasError&lt;/code&gt; when an error fails to appear.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;false&lt;/code&gt;&lt;/strong&gt; — either no validation rule is associated with the binding, or the typed input has not reached the source yet.
Check &lt;code&gt;ValidatesOnDataErrors="True"&lt;/code&gt; for &lt;code&gt;IDataErrorInfo&lt;/code&gt;, and the entry in &lt;code&gt;ValidationRules&lt;/code&gt; for a custom rule.
Specify &lt;code&gt;UpdateSourceTrigger=PropertyChanged&lt;/code&gt; to react while the user types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;true&lt;/code&gt; with nothing on screen&lt;/strong&gt; — the rendering surface is missing.
Include &lt;code&gt;AdornerDecorator&lt;/code&gt; if the &lt;code&gt;Window&lt;/code&gt; template has been replaced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A red border but no message&lt;/strong&gt; — that is the behavior of the default &lt;code&gt;ErrorTemplate&lt;/code&gt;.
Supply a custom template containing &lt;code&gt;AdornedElementPlaceholder&lt;/code&gt; and display &lt;code&gt;(Validation.Errors)/ErrorContent&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choose the approach on these criteria.&lt;br&gt;
For new implementations where the view model owns validation, adopt &lt;code&gt;INotifyDataErrorInfo&lt;/code&gt;.&lt;br&gt;
Use a custom &lt;code&gt;ValidationRule&lt;/code&gt; only when an input format check belongs inside the view, and combine &lt;code&gt;ValidatesOnExceptions&lt;/code&gt; when the domain model enforces its invariants in setters.&lt;br&gt;
For existing code built on &lt;code&gt;IDataErrorInfo&lt;/code&gt;, check first that &lt;code&gt;ValidatesOnDataErrors="True"&lt;/code&gt; has not been omitted.&lt;/p&gt;




&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-datatemplate-parent-datacontext-binding/" rel="noopener noreferrer"&gt;Binding to the Parent DataContext from Inside a WPF DataTemplate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-selectable-readonly-text-display/" rel="noopener noreferrer"&gt;How to Display Selectable, Copyable Read-Only Text in WPF&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>wpf</category>
      <category>dotnet</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Fixing the Cross-Thread Exception When Updating an ObservableCollection in WPF</title>
      <dc:creator>s-iguchi09</dc:creator>
      <pubDate>Fri, 11 Sep 2026 16:26:05 +0000</pubDate>
      <link>https://dev.to/s-iguchi09/fixing-the-cross-thread-exception-when-updating-an-observablecollection-in-wpf-kl4</link>
      <guid>https://dev.to/s-iguchi09/fixing-the-cross-thread-exception-when-updating-an-observablecollection-in-wpf-kl4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://s-iguchi09.github.io/articles/wpf-observablecollection-cross-thread-update/" rel="noopener noreferrer"&gt;s-iguchi09.github.io&lt;/a&gt;.&lt;br&gt;
A &lt;a href="https://s-iguchi09.github.io/ja/articles/wpf-observablecollection-cross-thread-update/" rel="noopener noreferrer"&gt;Japanese version&lt;/a&gt; is also available.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;By default, modifying an &lt;code&gt;ObservableCollection&amp;lt;T&amp;gt;&lt;/code&gt; bound to an &lt;code&gt;ItemsControl&lt;/code&gt; from a thread other than the UI thread throws a &lt;code&gt;NotSupportedException&lt;/code&gt; (registering &lt;code&gt;BindingOperations.EnableCollectionSynchronization&lt;/code&gt;, described below, lifts this restriction).&lt;br&gt;
The message reads along the lines of "This type of &lt;code&gt;CollectionView&lt;/code&gt; does not support changes to its &lt;code&gt;SourceCollection&lt;/code&gt; from a thread different from the &lt;code&gt;Dispatcher&lt;/code&gt; thread" (the exact wording varies by .NET version and locale).&lt;br&gt;
This article explains that the exception comes from the thread affinity of the &lt;code&gt;CollectionView&lt;/code&gt; rather than the collection itself, and it organizes the fixes based on &lt;code&gt;BindingOperations.EnableCollectionSynchronization&lt;/code&gt; and the &lt;code&gt;Dispatcher&lt;/code&gt;, along with the criteria for choosing between them.&lt;/p&gt;


&lt;h2&gt;
  
  
  Prerequisites / Environment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Framework: .NET 6 or later / WPF (the same applies to .NET Framework 4.5 and later)&lt;/li&gt;
&lt;li&gt;Language: C# / XAML (the code samples use a target-typed &lt;code&gt;new&lt;/code&gt; (&lt;code&gt;= new();&lt;/code&gt;, C# 9 or later); on C# 8 or earlier, use an explicit type such as &lt;code&gt;= new ObservableCollection&amp;lt;string&amp;gt;();&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Target controls: &lt;code&gt;ObservableCollection&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;ItemsControl&lt;/code&gt; (including &lt;code&gt;ListBox&lt;/code&gt;, &lt;code&gt;DataGrid&lt;/code&gt;, &lt;code&gt;ListView&lt;/code&gt;), &lt;code&gt;CollectionView&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Architecture: applicable to both MVVM and code-behind&lt;/li&gt;
&lt;li&gt;Assumption: the collection is updated on a background thread (&lt;code&gt;Task.Run&lt;/code&gt; or a worker thread)&lt;/li&gt;
&lt;li&gt;Verification environment: .NET 10 / Windows 11 (the measured results in this article were obtained here)&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;Modifying a bound collection directly from a background thread raises an exception.&lt;br&gt;
The following example adds items to an &lt;code&gt;ObservableCollection&amp;lt;T&amp;gt;&lt;/code&gt; from work started with &lt;code&gt;Task.Run&lt;/code&gt;.&lt;br&gt;
&lt;code&gt;File.ReadLines&lt;/code&gt; (from &lt;code&gt;System.IO&lt;/code&gt;) is used as a stand-in data source; it enumerates a file's lines lazily.&lt;br&gt;
The snippets in this article are members of a &lt;code&gt;ViewModel&lt;/code&gt; class and assume the namespaces &lt;code&gt;System.Collections.ObjectModel&lt;/code&gt;, &lt;code&gt;System.IO&lt;/code&gt;, &lt;code&gt;System.Threading.Tasks&lt;/code&gt;, &lt;code&gt;System.Windows&lt;/code&gt;, and &lt;code&gt;System.Windows.Data&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ObservableCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadLines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Add from a non-UI thread throws NotSupportedException&lt;/span&gt;
            &lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;Items&lt;/code&gt; is bound to &lt;code&gt;ItemsControl.ItemsSource&lt;/code&gt;, the &lt;code&gt;Add&lt;/code&gt; call reaches the &lt;code&gt;CollectionView&lt;/code&gt; through a &lt;code&gt;CollectionChanged&lt;/code&gt; notification.&lt;br&gt;
Because that notification arrives from a non-UI thread, the &lt;code&gt;CollectionView&lt;/code&gt; throws the exception.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Figure:&lt;/strong&gt; A three-lane diagram of the data flow between threads. The first lane shows a CollectionChanged notification arriving directly at the CollectionView from a background thread and raising an exception. The second shows EnableCollectionSynchronization: the change stays on the background thread while the notification is queued and applied asynchronously to the UI thread's shadow copy. The third shows the Dispatcher moving the collection operation itself onto the UI thread.&lt;/p&gt;

&lt;p&gt;The exception is raised not when the collection is touched, but when the change notification reaches the &lt;code&gt;CollectionView&lt;/code&gt;. The two fixes work differently. &lt;code&gt;EnableCollectionSynchronization&lt;/code&gt; makes the &lt;code&gt;CollectionView&lt;/code&gt; participate in the same synchronization mechanism and applies queued notifications asynchronously to the shadow copy it keeps for the UI thread — the mutation itself may stay on the background thread. The &lt;code&gt;Dispatcher&lt;/code&gt; instead marshals the collection operation onto the UI thread.&lt;/p&gt;

&lt;p&gt;The diagram is rendered in the &lt;a href="https://s-iguchi09.github.io/articles/wpf-observablecollection-cross-thread-update/" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Cause / Background
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ObservableCollection&amp;lt;T&amp;gt;&lt;/code&gt; itself is not thread-safe, but that is a separate matter: the direct cause of this &lt;code&gt;NotSupportedException&lt;/code&gt; is the &lt;code&gt;CollectionView&lt;/code&gt; that WPF routes collection access through when displaying it.&lt;br&gt;
The official documentation states that both the &lt;code&gt;ItemsControl&lt;/code&gt; and the &lt;code&gt;CollectionView&lt;/code&gt; have affinity to the thread on which the &lt;code&gt;ItemsControl&lt;/code&gt; was created, that using them on a different thread is forbidden, and that doing so throws an exception.&lt;br&gt;
In effect, this restriction extends to the bound collection as well.&lt;/p&gt;

&lt;p&gt;Most WPF objects derive from &lt;code&gt;DispatcherObject&lt;/code&gt; and carry thread affinity to their creating thread, which is normally the UI thread.&lt;br&gt;
The &lt;code&gt;CollectionView&lt;/code&gt; also derives from &lt;code&gt;DispatcherObject&lt;/code&gt; and, by default, does not allow its bound collection to be changed from another thread.&lt;br&gt;
As a result, when a &lt;code&gt;CollectionChanged&lt;/code&gt; notification arrives from a non-UI thread, the &lt;code&gt;CollectionView&lt;/code&gt; throws &lt;code&gt;NotSupportedException&lt;/code&gt; because it does not permit cross-thread changes.&lt;br&gt;
The root of the problem is therefore not that the collection was touched on another thread, but that the UI-thread-owned &lt;code&gt;CollectionView&lt;/code&gt; cannot receive a change notification originating from a different thread.&lt;/p&gt;
&lt;h3&gt;
  
  
  Measured: the Outcome Depends on Whether the Collection Is Bound
&lt;/h3&gt;

&lt;p&gt;The distinction is visible by performing the same operation on an &lt;code&gt;ObservableCollection&amp;lt;T&amp;gt;&lt;/code&gt; that is not bound to anything.&lt;br&gt;
The table below records the result of calling &lt;code&gt;Add&lt;/code&gt; from a background thread, varying whether the collection is bound and which countermeasure is applied.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Figure:&lt;/strong&gt; A table of results from calling Add on a background thread. An unbound ObservableCollection raises no exception. Bound to an ItemsControl it raises NotSupportedException. Both Dispatcher.Invoke and EnableCollectionSynchronization raise no exception. Only the EnableCollectionSynchronization row was notified while the lock was held.&lt;/p&gt;

&lt;p&gt;Measured on .NET 10 / Windows 11 by calling &lt;code&gt;ObservableCollection&amp;lt;string&amp;gt;.Add&lt;/code&gt; from inside &lt;code&gt;Task.Run&lt;/code&gt;. The first row is a collection bound to nothing; the remaining rows are bound to &lt;code&gt;ItemsControl.ItemsSource&lt;/code&gt; and displayed in a window. The last column gives how many of the &lt;code&gt;CollectionChanged&lt;/code&gt; notifications fired while &lt;code&gt;Monitor.IsEntered&lt;/code&gt; reported the lock as held, out of the total.&lt;/p&gt;

&lt;p&gt;The diagram is rendered in the &lt;a href="https://s-iguchi09.github.io/articles/wpf-observablecollection-cross-thread-update/" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;An unbound collection can be modified from a background thread without an exception.&lt;/strong&gt;&lt;br&gt;
If &lt;code&gt;ObservableCollection&amp;lt;T&amp;gt;&lt;/code&gt; itself carried thread affinity, that row would fail as well.&lt;br&gt;
The exception appears only once the collection is bound to &lt;code&gt;ItemsControl.ItemsSource&lt;/code&gt; and a &lt;code&gt;CollectionView&lt;/code&gt; sits in between, which confirms that the constraint belongs to the &lt;code&gt;CollectionView&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Note that the absence of an exception on the unbound row &lt;strong&gt;does not mean the collection is thread-safe&lt;/strong&gt;.&lt;br&gt;
&lt;code&gt;ObservableCollection&amp;lt;T&amp;gt;&lt;/code&gt; provides no protection against concurrent access, so competing updates still corrupt it in other ways.&lt;br&gt;
What the row establishes is narrower: the source of &lt;code&gt;NotSupportedException&lt;/code&gt; is the binding target.&lt;/p&gt;

&lt;p&gt;The last column reports whether the lock was held at the moment &lt;code&gt;CollectionChanged&lt;/code&gt; fired.&lt;br&gt;
&lt;strong&gt;Only the &lt;code&gt;EnableCollectionSynchronization&lt;/code&gt; row reads 1/1, meaning the change and its notification happen inside the same lock.&lt;/strong&gt;&lt;br&gt;
The other rows read 0/1, with the notification raised outside any lock.&lt;/p&gt;

&lt;p&gt;That 1/1 is the result &lt;strong&gt;for a configuration where the application wraps the &lt;code&gt;Add&lt;/code&gt; in a &lt;code&gt;lock&lt;/code&gt;&lt;/strong&gt;.&lt;br&gt;
It shows that the lock handed to &lt;code&gt;EnableCollectionSynchronization&lt;/code&gt; is still held when the notification arising from that &lt;code&gt;Add&lt;/code&gt; is raised.&lt;br&gt;
Registering alone does not place notifications inside the lock; wrapping the &lt;code&gt;Add&lt;/code&gt; in the same lock is the application's responsibility.&lt;/p&gt;


&lt;h2&gt;
  
  
  Two Core Approaches
&lt;/h2&gt;

&lt;p&gt;There are two approaches.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Marshal to the UI thread with the &lt;code&gt;Dispatcher&lt;/code&gt;&lt;/strong&gt; — run the collection mutation itself on the UI thread. This is simple and easy to apply to existing code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;BindingOperations.EnableCollectionSynchronization&lt;/code&gt;&lt;/strong&gt; — provide a lock in the application and register it with WPF, which allows direct modification from a background thread. This is less likely to saturate the UI thread even under heavy updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The former moves changes onto the UI thread; the latter lets WPF safely take in changes made on another thread.&lt;/p&gt;
&lt;h3&gt;
  
  
  Marshal to the UI thread with the Dispatcher
&lt;/h3&gt;

&lt;p&gt;Move the collection mutation to the UI thread with &lt;code&gt;Dispatcher.Invoke&lt;/code&gt; (or &lt;code&gt;InvokeAsync&lt;/code&gt;).&lt;br&gt;
Using &lt;code&gt;Application.Current.Dispatcher&lt;/code&gt; obtains the UI thread &lt;code&gt;Dispatcher&lt;/code&gt; even from a view model.&lt;br&gt;
This assumes a single UI thread; in an application with multiple UI threads, &lt;code&gt;Application.Current.Dispatcher&lt;/code&gt; refers to the main thread and may not own the bound &lt;code&gt;CollectionView&lt;/code&gt;, so capture the &lt;code&gt;Dispatcher&lt;/code&gt; associated with the bound &lt;code&gt;ItemsControl&lt;/code&gt; (or its &lt;code&gt;CollectionView&lt;/code&gt;) instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dispatcher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dispatcher&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadLines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Add runs on the UI thread, so no exception occurs&lt;/span&gt;
            &lt;span class="n"&gt;dispatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the mutation runs on the UI thread, no affinity violation occurs in the &lt;code&gt;CollectionView&lt;/code&gt;.&lt;br&gt;
Invoking per item causes many round-trips to the UI thread, however, so processing several items within a single &lt;code&gt;Invoke&lt;/code&gt; is preferable when items can be added in batches.&lt;/p&gt;
&lt;h3&gt;
  
  
  Share a lock with EnableCollectionSynchronization
&lt;/h3&gt;

&lt;p&gt;Provide a lock object and register it with WPF by calling &lt;code&gt;EnableCollectionSynchronization&lt;/code&gt; on the UI thread.&lt;br&gt;
From then on, all application-side modifications must be protected by that same lock.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;_lock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ObservableCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;ViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Call on the UI thread and before using the collection on another thread&lt;/span&gt;
    &lt;span class="n"&gt;BindingOperations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnableCollectionSynchronization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_lock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadLines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_lock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once &lt;code&gt;EnableCollectionSynchronization&lt;/code&gt; is called, the &lt;code&gt;CollectionView&lt;/code&gt; accesses the collection using the registered lock and maintains a "shadow copy" for the UI thread.&lt;br&gt;
Change notifications are queued as they arrive and applied when the UI thread has the opportunity to do so.&lt;br&gt;
This allows &lt;code&gt;Add&lt;/code&gt; to be called directly from a background thread.&lt;br&gt;
As required by the documentation, the call must occur on the UI thread and before the collection is used on another thread (or attached to the control), whichever is later.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Choose
&lt;/h2&gt;

&lt;p&gt;The two core approaches above, plus a variant and a way to avoid the problem entirely, make four in all. Which one applies is settled by the volume and frequency of the updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Occasional updates in small numbers call for &lt;code&gt;Dispatcher&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;
It needs no extra setup and touches little of the existing code. The cost of a per-item round-trip to the UI thread does not matter at low counts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heavy, frequent updates from another thread call for &lt;code&gt;EnableCollectionSynchronization&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;
Running a large number of per-item synchronous &lt;code&gt;Invoke&lt;/code&gt; calls saturates the UI thread and reduces responsiveness. Sharing a lock removes those round-trips by allowing direct modification from the background.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A custom synchronization mechanism such as a semaphore calls for the callback overload.&lt;/strong&gt;&lt;br&gt;
It lets WPF wait on something other than a lock. This is the most complex to implement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Work that can apply all its changes at once calls for batching on the UI thread.&lt;/strong&gt;&lt;br&gt;
This avoids producing a cross-thread modification in the first place. The benefit of doing the work in the background shrinks, but no synchronization design is needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparing the Approaches
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;th&gt;Best suited for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Dispatcher.Invoke&lt;/code&gt; / &lt;code&gt;InvokeAsync&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No extra setup; simple and easy to retrofit&lt;/td&gt;
&lt;td&gt;Per-item round-trips can strain the UI thread&lt;/td&gt;
&lt;td&gt;Low update frequency and volume; occasional add or remove&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;EnableCollectionSynchronization&lt;/code&gt; (simple lock)&lt;/td&gt;
&lt;td&gt;Direct modification from the background; less UI pressure&lt;/td&gt;
&lt;td&gt;Requires consistent locking; slightly more design effort&lt;/td&gt;
&lt;td&gt;High-volume, high-frequency updates on another thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;EnableCollectionSynchronization&lt;/code&gt; (callback)&lt;/td&gt;
&lt;td&gt;Allows non-lock mechanisms such as semaphores&lt;/td&gt;
&lt;td&gt;Most complex to implement&lt;/td&gt;
&lt;td&gt;A design that already has a custom synchronization mechanism&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch on the UI thread&lt;/td&gt;
&lt;td&gt;Avoids the threading issue entirely&lt;/td&gt;
&lt;td&gt;Loses the benefit of background work&lt;/td&gt;
&lt;td&gt;Work that can apply all changes at once after gathering&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protect all application access with the same lock:&lt;/strong&gt; the lock passed to &lt;code&gt;EnableCollectionSynchronization&lt;/code&gt; must guard every read and write in the application, not only WPF's access. Leaving any path unlocked can race with the &lt;code&gt;CollectionView&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomicity of change and notification:&lt;/strong&gt; a change (such as &lt;code&gt;Add&lt;/code&gt;) and its &lt;code&gt;CollectionChanged&lt;/code&gt; notification must be atomic. &lt;code&gt;ObservableCollection&amp;lt;T&amp;gt;&lt;/code&gt; guarantees this as long as all changes are protected by the same synchronization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timing of registration and disabling:&lt;/strong&gt; call both &lt;code&gt;EnableCollectionSynchronization&lt;/code&gt; and &lt;code&gt;DisableCollectionSynchronization&lt;/code&gt; on the UI thread. To use the same collection on multiple UI threads, register it separately on each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI elements remain UI-thread-only:&lt;/strong&gt; this fix relaxes only access to the bound collection. Manipulating a &lt;code&gt;DependencyObject&lt;/code&gt;, such as a control, directly from another thread remains disallowed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A worker started with &lt;code&gt;new Thread&lt;/code&gt; keeps the process alive:&lt;/strong&gt; unlike &lt;code&gt;Task.Run&lt;/code&gt;, it is a foreground thread by default, so a worker left updating the collection prevents the process from exiting even after the window closes. Diagnosis and remedies are covered in &lt;a href="https://s-iguchi09.github.io/articles/wpf-application-not-exiting-shutdownmode-threads/" rel="noopener noreferrer"&gt;Diagnosing a WPF Process That Stays Alive After the Window Closes — ShutdownMode and Foreground Threads&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The exception raised when a bound &lt;code&gt;ObservableCollection&amp;lt;T&amp;gt;&lt;/code&gt; is modified from another thread comes from the thread affinity of the &lt;code&gt;CollectionView&lt;/code&gt;, not the collection.&lt;/p&gt;

&lt;p&gt;The deciding factor is the volume and frequency of the updates. Marshal to the UI thread with &lt;code&gt;Dispatcher&lt;/code&gt; for occasional ones; share a lock with &lt;code&gt;EnableCollectionSynchronization&lt;/code&gt; for heavy, frequent ones.&lt;br&gt;
In every case, design with the understanding that UI elements themselves remain UI-thread-only, and that only collection access is relaxed.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-combobox-itemssource-patterns/" rel="noopener noreferrer"&gt;WPF ComboBox ItemsSource Binding Patterns and Selected Value Retrieval&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-listbox-virtualization-selecteditems/" rel="noopener noreferrer"&gt;How to Prevent SelectedItems from Appearing Lost in a Virtualized WPF ListBox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-bitmapimage-file-lock-cacheoption/" rel="noopener noreferrer"&gt;Releasing the Image File Locked by BitmapImage in WPF with BitmapCacheOption.OnLoad&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-datagrid-sort-reset/" rel="noopener noreferrer"&gt;How to Reset DataGrid Sorting in WPF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-datagrid-sorting/" rel="noopener noreferrer"&gt;How to Implement DataGrid Sorting in WPF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/apps/wpf-standard-control-demo/datagrid.html" rel="noopener noreferrer"&gt;DataGrid | WPF Standard Control Demo App (.NET 10)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>wpf</category>
      <category>dotnet</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Why a WPF ScrollViewer Does Not Scroll and How to Fix It</title>
      <dc:creator>s-iguchi09</dc:creator>
      <pubDate>Thu, 10 Sep 2026 15:04:56 +0000</pubDate>
      <link>https://dev.to/s-iguchi09/why-a-wpf-scrollviewer-does-not-scroll-and-how-to-fix-it-2p9g</link>
      <guid>https://dev.to/s-iguchi09/why-a-wpf-scrollviewer-does-not-scroll-and-how-to-fix-it-2p9g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://s-iguchi09.github.io/articles/wpf-scrollviewer-not-scrolling/" rel="noopener noreferrer"&gt;s-iguchi09.github.io&lt;/a&gt;.&lt;br&gt;
A &lt;a href="https://s-iguchi09.github.io/ja/articles/wpf-scrollviewer-not-scrolling/" rel="noopener noreferrer"&gt;Japanese version&lt;/a&gt; is also available.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;The WPF &lt;code&gt;ScrollViewer&lt;/code&gt; shows scrollbars and enables scrolling when its content is larger than the available viewport.&lt;br&gt;
When a &lt;code&gt;ScrollViewer&lt;/code&gt; is placed inside a &lt;code&gt;StackPanel&lt;/code&gt;, however, the scrollbars never appear and the content keeps growing downward regardless of how many items it holds.&lt;br&gt;
This article explains that the behavior comes from how the layout system measures elements, and it organizes the container-based fixes along with the criteria for choosing between them.&lt;/p&gt;


&lt;h2&gt;
  
  
  Prerequisites / Environment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Framework: .NET 6 or later / WPF&lt;/li&gt;
&lt;li&gt;Language: C# / XAML&lt;/li&gt;
&lt;li&gt;Target controls: &lt;code&gt;ScrollViewer&lt;/code&gt;, &lt;code&gt;StackPanel&lt;/code&gt;, &lt;code&gt;Grid&lt;/code&gt;, &lt;code&gt;DockPanel&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Architecture: applicable to both MVVM and code-behind&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;Placing a &lt;code&gt;ScrollViewer&lt;/code&gt; inside a vertical &lt;code&gt;StackPanel&lt;/code&gt; and filling it with many items often results in no scrollbar; the content extends past the visible area instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;StackPanel&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TextBlock&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"Header"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ScrollViewer&lt;/span&gt; &lt;span class="na"&gt;VerticalScrollBarVisibility=&lt;/span&gt;&lt;span class="s"&gt;"Auto"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;StackPanel&amp;gt;&lt;/span&gt;
            &lt;span class="c"&gt;&amp;lt;!-- many items --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ScrollViewer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though &lt;code&gt;VerticalScrollBarVisibility="Auto"&lt;/code&gt; is set, the &lt;code&gt;ScrollViewer&lt;/code&gt; expands to the full height of its content and no scrollbar is shown.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs-iguchi09.github.io%2Fimages%2Farticles%2Fwpf-scrollviewer-not-scrolling%2Fscrollviewer-stackpanel-vs-grid.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs-iguchi09.github.io%2Fimages%2Farticles%2Fwpf-scrollviewer-not-scrolling%2Fscrollviewer-stackpanel-vs-grid.png" alt="Two regions of identical height showing the same 12 items. The one placed inside a StackPanel has no scrollbar and is cut off at the seventh item, while the one in a Grid star row shows a scrollbar." width="496" height="279"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;The same header and 12 items rendered inside regions of identical height. On the left the &lt;code&gt;ScrollViewer&lt;/code&gt; sits inside a &lt;code&gt;StackPanel&lt;/code&gt;: no scrollbar appears and the content is clipped at the bottom of the region. On the right it sits in a &lt;code&gt;*&lt;/code&gt; row of a &lt;code&gt;Grid&lt;/code&gt;, so a scrollbar appears and the remaining items are reachable.
  &lt;p&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Cause / Background
&lt;/h2&gt;

&lt;p&gt;The cause lies in the available size that the &lt;code&gt;StackPanel&lt;/code&gt; passes to its children during measurement.&lt;br&gt;
In its stacking direction, which is height for a vertical panel, a &lt;code&gt;StackPanel&lt;/code&gt; measures each child with an &lt;strong&gt;infinite available size&lt;/strong&gt;.&lt;br&gt;
The official documentation notes that a &lt;code&gt;StackPanel&lt;/code&gt; does not constrain its children in the direction it stacks.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;ScrollViewer&lt;/code&gt; shows a scrollbar only when its content is larger than the height it is given.&lt;br&gt;
When the &lt;code&gt;StackPanel&lt;/code&gt; passes an infinite height, the &lt;code&gt;ScrollViewer&lt;/code&gt; requests exactly the height needed to fit all of its content, so no overflow occurs.&lt;br&gt;
No scrollbar appears, and the &lt;code&gt;ScrollViewer&lt;/code&gt; itself grows to the full height of its content.&lt;/p&gt;

&lt;p&gt;The root of the problem is therefore not the &lt;code&gt;ScrollViewer&lt;/code&gt; but the surrounding &lt;code&gt;StackPanel&lt;/code&gt;, which never constrains the height.&lt;/p&gt;



&lt;p&gt;The difference can be confirmed by changing only the parent layout and reading the heights off the &lt;code&gt;ScrollViewer&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Figure:&lt;/strong&gt; A table of Extent, Viewport, and Scrollable heights plus scrollbar visibility per parent layout. Inside a StackPanel the viewport equals the extent at 800, Scrollable is 0, and the scrollbar is Collapsed. Inside a Grid or DockPanel the viewport is 200, Scrollable is 600, and the scrollbar is Visible.&lt;/p&gt;

&lt;p&gt;Measured on .NET 10 / Windows 11 with a &lt;code&gt;ScrollViewer&lt;/code&gt; holding 40 rows of 20px each (800px total) inside a parent constrained to 200px. Nothing differs between the rows except the parent layout.&lt;/p&gt;

&lt;p&gt;The diagram is rendered in the &lt;a href="https://s-iguchi09.github.io/articles/wpf-scrollviewer-not-scrolling/" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Inside the &lt;code&gt;StackPanel&lt;/code&gt;, &lt;code&gt;ViewportHeight&lt;/code&gt; equals &lt;code&gt;ExtentHeight&lt;/code&gt; at 800.&lt;/strong&gt; Although the parent is constrained to 200px, the &lt;code&gt;ScrollViewer&lt;/code&gt; still claims the height its full content needs.&lt;br&gt;
&lt;code&gt;ScrollableHeight&lt;/code&gt; is therefore 0 and the scrollbar stays &lt;code&gt;Collapsed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With a &lt;code&gt;Grid&lt;/code&gt; or a &lt;code&gt;DockPanel&lt;/code&gt;, &lt;code&gt;ViewportHeight&lt;/code&gt; settles at 200 and the remaining 600 becomes &lt;code&gt;ScrollableHeight&lt;/code&gt;.&lt;br&gt;
As the last row shows, a &lt;code&gt;StackPanel&lt;/code&gt; produces the same result once the &lt;code&gt;ScrollViewer&lt;/code&gt; is given an explicit height. What matters is that a constraint reaches it.&lt;/p&gt;


&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;Place the &lt;code&gt;ScrollViewer&lt;/code&gt; in a container that constrains its height.&lt;br&gt;
Specifically, put it in a &lt;code&gt;Grid&lt;/code&gt; row sized with &lt;code&gt;*&lt;/code&gt; (star), let a &lt;code&gt;DockPanel&lt;/code&gt; fit it into the remaining space, or give it an explicit &lt;code&gt;Height&lt;/code&gt; or &lt;code&gt;MaxHeight&lt;/code&gt;.&lt;br&gt;
A finite height is then passed to the &lt;code&gt;ScrollViewer&lt;/code&gt;, and a scrollbar appears once the content exceeds that height.&lt;/p&gt;


&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Place it in a star-sized Grid row
&lt;/h3&gt;

&lt;p&gt;Split the &lt;code&gt;Grid&lt;/code&gt; into a fixed-size row and a &lt;code&gt;*&lt;/code&gt; row that fills the remaining space, and put the scrollable &lt;code&gt;ScrollViewer&lt;/code&gt; in the &lt;code&gt;*&lt;/code&gt; row.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Grid&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Grid.RowDefinitions&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;RowDefinition&lt;/span&gt; &lt;span class="na"&gt;Height=&lt;/span&gt;&lt;span class="s"&gt;"Auto"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;RowDefinition&lt;/span&gt; &lt;span class="na"&gt;Height=&lt;/span&gt;&lt;span class="s"&gt;"*"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Grid.RowDefinitions&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;TextBlock&lt;/span&gt; &lt;span class="na"&gt;Grid.Row=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"Header"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;ScrollViewer&lt;/span&gt; &lt;span class="na"&gt;Grid.Row=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;VerticalScrollBarVisibility=&lt;/span&gt;&lt;span class="s"&gt;"Auto"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;StackPanel&amp;gt;&lt;/span&gt;
            &lt;span class="c"&gt;&amp;lt;!-- many items --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ScrollViewer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Grid&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;*&lt;/code&gt; row receives the parent's remaining height, so the &lt;code&gt;ScrollViewer&lt;/code&gt; is given a finite height.&lt;br&gt;
When the content exceeds that height, the scrollbar appears automatically.&lt;/p&gt;
&lt;h3&gt;
  
  
  Fit it into the remaining space with a DockPanel
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;DockPanel&lt;/code&gt; expands its last child into the remaining area through &lt;code&gt;LastChildFill&lt;/code&gt;, which is enabled by default.&lt;br&gt;
Dock the header to the top and place the &lt;code&gt;ScrollViewer&lt;/code&gt; as the last child.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;DockPanel&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TextBlock&lt;/span&gt; &lt;span class="na"&gt;DockPanel.Dock=&lt;/span&gt;&lt;span class="s"&gt;"Top"&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"Header"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;ScrollViewer&lt;/span&gt; &lt;span class="na"&gt;VerticalScrollBarVisibility=&lt;/span&gt;&lt;span class="s"&gt;"Auto"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;StackPanel&amp;gt;&lt;/span&gt;
            &lt;span class="c"&gt;&amp;lt;!-- many items --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ScrollViewer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/DockPanel&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last child without a &lt;code&gt;DockPanel.Dock&lt;/code&gt; value fills the remaining area, so the height of the &lt;code&gt;ScrollViewer&lt;/code&gt; is constrained.&lt;/p&gt;




&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;VerticalScrollBarVisibility="Disabled"&lt;/code&gt; turns scrolling off:&lt;/strong&gt; setting &lt;code&gt;Disabled&lt;/code&gt; prevents scrolling in that direction through user interaction.
To hide the scrollbar while keeping scrolling, use &lt;code&gt;Hidden&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid nesting scrollable controls directly:&lt;/strong&gt; placing a control with its own scrolling, such as a &lt;code&gt;ListBox&lt;/code&gt;, directly inside a &lt;code&gt;ScrollViewer&lt;/code&gt; can make the mouse wheel act on the wrong element.
A &lt;code&gt;ListBox&lt;/code&gt; already scrolls internally, so it is better given a height-constrained layout, such as a &lt;code&gt;*&lt;/code&gt; row of a &lt;code&gt;Grid&lt;/code&gt;, than wrapped in an outer &lt;code&gt;ScrollViewer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Physical versus logical scrolling:&lt;/strong&gt; &lt;code&gt;ScrollViewer.CanContentScroll&lt;/code&gt; defaults to &lt;code&gt;false&lt;/code&gt;, giving physical, pixel-based scrolling; when it is &lt;code&gt;true&lt;/code&gt;, scrolling is logical, by item.
The standard &lt;code&gt;ListBox&lt;/code&gt; template sets it to &lt;code&gt;true&lt;/code&gt;, so a data-bound &lt;code&gt;ListBox&lt;/code&gt; scrolls logically and its &lt;code&gt;VirtualizingStackPanel&lt;/code&gt; virtualizes items.
That virtualization is lost only if &lt;code&gt;CanContentScroll&lt;/code&gt; is forced to &lt;code&gt;false&lt;/code&gt;, so avoid that for long lists.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Alternatives / Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Container&lt;/th&gt;
&lt;th&gt;Scrolling behavior&lt;/th&gt;
&lt;th&gt;Suited for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;StackPanel&lt;/code&gt; (vertical)&lt;/td&gt;
&lt;td&gt;Does not constrain height, so the &lt;code&gt;ScrollViewer&lt;/code&gt; does not scroll unless given an explicit &lt;code&gt;Height&lt;/code&gt; or &lt;code&gt;MaxHeight&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Short content that needs no scrolling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Grid&lt;/code&gt; with a &lt;code&gt;*&lt;/code&gt; row&lt;/td&gt;
&lt;td&gt;Passes the remaining height; scrolls when content overflows&lt;/td&gt;
&lt;td&gt;Screens that separate headers or footers from a variable area&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;DockPanel&lt;/code&gt; (&lt;code&gt;LastChildFill&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Fits into the remaining area; scrolls when content overflows&lt;/td&gt;
&lt;td&gt;Screens combining edge-docked elements with a main area&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit &lt;code&gt;Height&lt;/code&gt; / &lt;code&gt;MaxHeight&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Scrolls once the specified height is exceeded&lt;/td&gt;
&lt;td&gt;A partial list with a fixed height cap&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;StackPanel&lt;/code&gt; passes an infinite height to its children in the stacking direction, so a &lt;code&gt;ScrollViewer&lt;/code&gt; inside it cannot detect overflow and does not scroll.&lt;br&gt;
The root of the problem is the surrounding container that fails to constrain the height, not the &lt;code&gt;ScrollViewer&lt;/code&gt; itself.&lt;/p&gt;

&lt;p&gt;The selection criteria are as follows.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;To separate headers or footers from a variable area:&lt;/strong&gt; place the &lt;code&gt;ScrollViewer&lt;/code&gt; in a &lt;code&gt;*&lt;/code&gt; row of a &lt;code&gt;Grid&lt;/code&gt;.
Rows can be assigned by role, which makes the layout intent explicit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;To combine edge-docked elements with a main area:&lt;/strong&gt; use a &lt;code&gt;DockPanel&lt;/code&gt; and place the &lt;code&gt;ScrollViewer&lt;/code&gt; as the last child in the remaining space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;To cap only the height of a partial list:&lt;/strong&gt; set &lt;code&gt;MaxHeight&lt;/code&gt; and scroll once that height is exceeded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In every case, the key is a layout that passes a finite height to the &lt;code&gt;ScrollViewer&lt;/code&gt;.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-listbox-virtualization-selecteditems/" rel="noopener noreferrer"&gt;How to Prevent SelectedItems from Appearing Lost in a Virtualized WPF ListBox&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-collectionviewsource-filter-not-refreshing/" rel="noopener noreferrer"&gt;Causes of a Stale ICollectionView Filter in WPF and Choosing Between Refresh and Live Filtering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-fluent-thememode-runtime-switch/" rel="noopener noreferrer"&gt;Diagnosing Why Parts of a WPF App Stay Light After Switching ThemeMode at Runtime&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-label-vs-textblock-performance/" rel="noopener noreferrer"&gt;Why WPF Slows Down with Many Labels and When to Switch to TextBlock&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-tray-contextmenu-close-on-focus-loss/" rel="noopener noreferrer"&gt;Fixing a WPF Tray ContextMenu That Does Not Close on Focus Loss&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/apps/wpf-standard-control-demo/scrollviewer.html" rel="noopener noreferrer"&gt;ScrollViewer | WPF Standard Control Demo App (.NET 10)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>wpf</category>
      <category>dotnet</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Why WPF Style Triggers and DataTriggers Do Not Apply — Dependency Property Value Precedence</title>
      <dc:creator>s-iguchi09</dc:creator>
      <pubDate>Wed, 09 Sep 2026 14:13:42 +0000</pubDate>
      <link>https://dev.to/s-iguchi09/why-wpf-style-triggers-and-datatriggers-do-not-apply-dependency-property-value-precedence-1ebg</link>
      <guid>https://dev.to/s-iguchi09/why-wpf-style-triggers-and-datatriggers-do-not-apply-dependency-property-value-precedence-1ebg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://s-iguchi09.github.io/articles/wpf-style-trigger-not-working-local-value/" rel="noopener noreferrer"&gt;s-iguchi09.github.io&lt;/a&gt;.&lt;br&gt;
A &lt;a href="https://s-iguchi09.github.io/ja/articles/wpf-style-trigger-not-working-local-value/" rel="noopener noreferrer"&gt;Japanese version&lt;/a&gt; is also available.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;Trigger&lt;/code&gt; or &lt;code&gt;DataTrigger&lt;/code&gt; declared in &lt;code&gt;Style.Triggers&lt;/code&gt; sometimes has no visible effect even though its condition is met.&lt;br&gt;
The common assumption is a broken binding or a type mismatch in the trigger condition, but a frequent cause is that the trigger fires correctly and its value is simply outranked by a higher-precedence input.&lt;br&gt;
This article explains the cause in terms of dependency property value precedence, shows how to repair markup that carries a local value, and gives criteria for choosing among the available fixes.&lt;/p&gt;


&lt;h2&gt;
  
  
  Prerequisites / Environment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Framework: .NET 6 or later / WPF (the precedence rules are identical on WPF for .NET Framework 4.x)&lt;/li&gt;
&lt;li&gt;Language: C#&lt;/li&gt;
&lt;li&gt;Target feature: &lt;code&gt;Trigger&lt;/code&gt; / &lt;code&gt;DataTrigger&lt;/code&gt; / &lt;code&gt;MultiTrigger&lt;/code&gt; declared in &lt;code&gt;Style.Triggers&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Default theme: Aero2 (the Fluent theme available from .NET 9 differs from what is described below in both the standard control colors and the structure of the default templates)&lt;/li&gt;
&lt;li&gt;Architecture: applicable to both MVVM and code-behind&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;Consider a &lt;code&gt;Style&lt;/code&gt; with a &lt;code&gt;DataTrigger&lt;/code&gt; that changes the background of a frame according to a validation state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Window.Resources&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Style&lt;/span&gt; &lt;span class="na"&gt;x:Key=&lt;/span&gt;&lt;span class="s"&gt;"StatusBox"&lt;/span&gt; &lt;span class="na"&gt;TargetType=&lt;/span&gt;&lt;span class="s"&gt;"Border"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Style.Triggers&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;DataTrigger&lt;/span&gt; &lt;span class="na"&gt;Binding=&lt;/span&gt;&lt;span class="s"&gt;"{Binding HasError}"&lt;/span&gt; &lt;span class="na"&gt;Value=&lt;/span&gt;&lt;span class="s"&gt;"True"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;Setter&lt;/span&gt; &lt;span class="na"&gt;Property=&lt;/span&gt;&lt;span class="s"&gt;"Background"&lt;/span&gt; &lt;span class="na"&gt;Value=&lt;/span&gt;&lt;span class="s"&gt;"#FFD4D4"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/DataTrigger&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/Style.Triggers&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Window.Resources&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;Border&lt;/span&gt; &lt;span class="na"&gt;Style=&lt;/span&gt;&lt;span class="s"&gt;"{StaticResource StatusBox}"&lt;/span&gt; &lt;span class="na"&gt;Background=&lt;/span&gt;&lt;span class="s"&gt;"White"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TextBlock&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"HasError = True"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Border&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;HasError&lt;/code&gt; becomes &lt;code&gt;true&lt;/code&gt;, the background of this &lt;code&gt;Border&lt;/code&gt; stays &lt;code&gt;White&lt;/code&gt;.&lt;br&gt;
The binding resolves correctly and no binding error appears in the Output window (for reading those messages, see &lt;a href="https://s-iguchi09.github.io/articles/wpf-binding-error-debugging-output-window/" rel="noopener noreferrer"&gt;Reading WPF Binding Errors and Diagnosing Them with the Output Window&lt;/a&gt;).&lt;br&gt;
The same result occurs with a property trigger such as &lt;code&gt;Trigger Property="IsMouseOver"&lt;/code&gt;, so the trigger type is not the cause.&lt;/p&gt;


&lt;h2&gt;
  
  
  Cause / Background
&lt;/h2&gt;

&lt;p&gt;A WPF dependency property can receive values from several inputs: local values, styles, templates, and inheritance.&lt;br&gt;
Which one becomes the effective value is decided by &lt;strong&gt;dependency property value precedence&lt;/strong&gt;, and a higher-precedence input silences every lower one.&lt;/p&gt;

&lt;p&gt;The order is as follows, highest precedence first.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;Source of the value&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Property system coercion&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CoerceValueCallback&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Active animations, or animations with a &lt;code&gt;Hold&lt;/code&gt; behavior&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Storyboard&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Local value&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A XAML attribute or property element, &lt;code&gt;SetValue&lt;/code&gt;, or a &lt;code&gt;Binding&lt;/code&gt; / &lt;code&gt;StaticResource&lt;/code&gt; / &lt;code&gt;DynamicResource&lt;/code&gt; written on the element&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TemplatedParent&lt;/code&gt; template property values&lt;/td&gt;
&lt;td&gt;Elements created by a &lt;code&gt;ControlTemplate&lt;/code&gt; or &lt;code&gt;DataTemplate&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Implicit styles&lt;/td&gt;
&lt;td&gt;Applies to the &lt;code&gt;Style&lt;/code&gt; property only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Style triggers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Style.Triggers&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Template triggers&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ControlTemplate.Triggers&lt;/code&gt; / &lt;code&gt;DataTemplate.Triggers&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Style setter values&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;Setter&lt;/code&gt; directly under &lt;code&gt;Style&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Default (theme) styles&lt;/td&gt;
&lt;td&gt;Theme style triggers, then theme style setters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Inheritance&lt;/td&gt;
&lt;td&gt;Inheritable properties such as &lt;code&gt;FontSize&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Default value from dependency property metadata&lt;/td&gt;
&lt;td&gt;The default value in &lt;code&gt;PropertyMetadata&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The problem lies entirely in the gap between rank 3 and rank 6.&lt;br&gt;
A value written as a XAML attribute, such as &lt;code&gt;Background="White"&lt;/code&gt;, is a local value at rank 3 and therefore outranks a style trigger at rank 6.&lt;br&gt;
The trigger condition is evaluated and its &lt;code&gt;Setter&lt;/code&gt; is applied to that lower rank, but the effective value remains the local value, so nothing changes on screen.&lt;/p&gt;

&lt;p&gt;The part that is easy to miss is that &lt;strong&gt;a &lt;code&gt;Binding&lt;/code&gt; or a &lt;code&gt;DynamicResource&lt;/code&gt; written directly on the element also counts as a local value&lt;/strong&gt;.&lt;br&gt;
Writing &lt;code&gt;Background="{Binding NormalBrush}"&lt;/code&gt; only defers evaluation of the value; its precedence is still rank 3, and a style trigger cannot win against it.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;Setter&lt;/code&gt; directly under &lt;code&gt;Style&lt;/code&gt;, on the other hand, sits at rank 8, below the trigger at rank 6.&lt;br&gt;
Supplying the default through a setter rather than a local value therefore restores the intended relationship.&lt;/p&gt;



&lt;p&gt;This precedence can be confirmed by displaying the elements and reading &lt;code&gt;DependencyPropertyHelper.GetValueSource&lt;/code&gt;.&lt;br&gt;
The figure below records the result under varying conditions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Figure:&lt;/strong&gt; A table of the effective Border.Background and where the value came from. With a local value it stays white at Local; once the default moves to a Setter the trigger color applies at StyleTrigger; with the trigger unmet it is white at Style; and after ClearValue removes the local value the trigger color applies at StyleTrigger.&lt;/p&gt;

&lt;p&gt;Measured on .NET 10 / Windows 11. &lt;code&gt;HasError&lt;/code&gt; varies per row: it is &lt;code&gt;False&lt;/code&gt; on the &lt;code&gt;trigger not met&lt;/code&gt; row and &lt;code&gt;True&lt;/code&gt; on the others. The value in parentheses is the &lt;code&gt;BaseValueSource&lt;/code&gt; returned by &lt;code&gt;DependencyPropertyHelper.GetValueSource&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The diagram is rendered in the &lt;a href="https://s-iguchi09.github.io/articles/wpf-style-trigger-not-working-local-value/" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;On the row where the value does not change, &lt;code&gt;BaseValueSource&lt;/code&gt; is &lt;code&gt;Local&lt;/code&gt;.&lt;/strong&gt; That is why the trigger value never replaces it.&lt;br&gt;
Moving the default into the &lt;code&gt;Setter&lt;/code&gt; changes the source to &lt;code&gt;StyleTrigger&lt;/code&gt;, and the trigger color becomes the effective value.&lt;br&gt;
Clearing the local value with &lt;code&gt;ClearValue&lt;/code&gt; produces the same result, which confirms the local value is the cause.&lt;/p&gt;


&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;Remove the local value from the target element and move the default into a &lt;code&gt;Setter&lt;/code&gt; inside the &lt;code&gt;Style&lt;/code&gt;.&lt;br&gt;
The default then comes from rank 8 and the conditional value from rank 6, so the trigger wins whenever its condition holds.&lt;/p&gt;

&lt;p&gt;Only the property that the trigger writes to is affected.&lt;br&gt;
Setting unrelated properties such as &lt;code&gt;Margin&lt;/code&gt; or &lt;code&gt;Width&lt;/code&gt; as local values on the element causes no interference.&lt;/p&gt;


&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;The following markup places two &lt;code&gt;Border&lt;/code&gt; elements one above the other under the same style: one keeps its local value, the other takes its default from the setter.&lt;br&gt;
Both reference the same &lt;code&gt;StatusBox&lt;/code&gt; style, and the relevant difference is whether &lt;code&gt;Background&lt;/code&gt; is present as a local value (the &lt;code&gt;Margin&lt;/code&gt; on the lower one only separates the two vertically and has no bearing on the trigger).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Window.Resources&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Style&lt;/span&gt; &lt;span class="na"&gt;x:Key=&lt;/span&gt;&lt;span class="s"&gt;"StatusBox"&lt;/span&gt; &lt;span class="na"&gt;TargetType=&lt;/span&gt;&lt;span class="s"&gt;"Border"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Setter&lt;/span&gt; &lt;span class="na"&gt;Property=&lt;/span&gt;&lt;span class="s"&gt;"Background"&lt;/span&gt; &lt;span class="na"&gt;Value=&lt;/span&gt;&lt;span class="s"&gt;"White"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Setter&lt;/span&gt; &lt;span class="na"&gt;Property=&lt;/span&gt;&lt;span class="s"&gt;"BorderBrush"&lt;/span&gt; &lt;span class="na"&gt;Value=&lt;/span&gt;&lt;span class="s"&gt;"#9AA4B2"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Setter&lt;/span&gt; &lt;span class="na"&gt;Property=&lt;/span&gt;&lt;span class="s"&gt;"BorderThickness"&lt;/span&gt; &lt;span class="na"&gt;Value=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Setter&lt;/span&gt; &lt;span class="na"&gt;Property=&lt;/span&gt;&lt;span class="s"&gt;"Padding"&lt;/span&gt; &lt;span class="na"&gt;Value=&lt;/span&gt;&lt;span class="s"&gt;"18,6"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Style.Triggers&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;DataTrigger&lt;/span&gt; &lt;span class="na"&gt;Binding=&lt;/span&gt;&lt;span class="s"&gt;"{Binding HasError}"&lt;/span&gt; &lt;span class="na"&gt;Value=&lt;/span&gt;&lt;span class="s"&gt;"True"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;Setter&lt;/span&gt; &lt;span class="na"&gt;Property=&lt;/span&gt;&lt;span class="s"&gt;"Background"&lt;/span&gt; &lt;span class="na"&gt;Value=&lt;/span&gt;&lt;span class="s"&gt;"#FFD4D4"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/DataTrigger&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/Style.Triggers&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Window.Resources&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;StackPanel&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- The local value remains, so the trigger background is not applied --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Border&lt;/span&gt; &lt;span class="na"&gt;Style=&lt;/span&gt;&lt;span class="s"&gt;"{StaticResource StatusBox}"&lt;/span&gt; &lt;span class="na"&gt;Background=&lt;/span&gt;&lt;span class="s"&gt;"White"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;TextBlock&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"HasError = True"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Border&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- The default moved into the setter, so the trigger background is applied --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Border&lt;/span&gt; &lt;span class="na"&gt;Style=&lt;/span&gt;&lt;span class="s"&gt;"{StaticResource StatusBox}"&lt;/span&gt; &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"0,12,0,0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;TextBlock&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"HasError = True"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Border&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;HasError&lt;/code&gt; used in the trigger condition is a property on the view model assigned to &lt;code&gt;DataContext&lt;/code&gt;.&lt;br&gt;
It implements &lt;code&gt;INotifyPropertyChanged&lt;/code&gt; so that runtime changes reach the trigger.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ValidationViewModel&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;INotifyPropertyChanged&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_hasError&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;HasError&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;get&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_hasError&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;set&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_hasError&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;_hasError&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;PropertyChanged&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PropertyChangedEventArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HasError&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="n"&gt;PropertyChangedEventHandler&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;PropertyChanged&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assigning this view model to the &lt;code&gt;DataContext&lt;/code&gt; of the &lt;code&gt;Window&lt;/code&gt; propagates changes of &lt;code&gt;HasError&lt;/code&gt; to the &lt;code&gt;DataTrigger&lt;/code&gt;.&lt;br&gt;
With a plain property that raises no change notification, the trigger is never re-evaluated when the value changes.&lt;/p&gt;

&lt;p&gt;Rendering this markup with &lt;code&gt;HasError&lt;/code&gt; set to &lt;code&gt;true&lt;/code&gt; shows the difference directly.&lt;/p&gt;


  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs-iguchi09.github.io%2Fimages%2Farticles%2Fwpf-style-trigger-not-working-local-value%2Fstyle-trigger-local-value.png" alt="Two Border elements sharing one style. The upper one, whose Background is set as a local value, stays white, while the lower one takes the pale red from the DataTrigger." width="415" height="139"&gt;The result with &lt;code&gt;HasError&lt;/code&gt; set to &lt;code&gt;True&lt;/code&gt;. The upper border keeps &lt;code&gt;Background&lt;/code&gt; as a local value and does not pick up the trigger color; the lower one takes its default from the &lt;code&gt;Setter&lt;/code&gt;, so the trigger color is applied. The labels on the left were added to the figure to show how the two &lt;code&gt;Border&lt;/code&gt; declarations differ (captured on .NET 10 / Windows 11).
  


&lt;p&gt;Precedence also depends on how a value is assigned from code-behind.&lt;br&gt;
The three statements below all target &lt;code&gt;Background&lt;/code&gt;, but each stores the value at a different rank.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Becomes a local value, so style triggers no longer affect Background on this element&lt;/span&gt;
&lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Brushes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;White&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Changes the effective value without writing a local value (a trigger can still take over)&lt;/span&gt;
&lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetCurrentValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Border&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BackgroundProperty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Brushes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;White&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Removes an existing local value, restoring the setter or trigger value&lt;/span&gt;
&lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ClearValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Border&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BackgroundProperty&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SetCurrentValue&lt;/code&gt; is a special assignment that does not appear in the precedence list: it changes the current value without overwriting the source of the value.&lt;br&gt;
It suits cases where a temporary value is needed without discarding an existing binding or trigger.&lt;br&gt;
It only avoids creating a local value, however, and does not remove one that is already set.&lt;br&gt;
While a local value remains on the target property the effective value does not change, so it has to be removed with &lt;code&gt;ClearValue&lt;/code&gt; first.&lt;br&gt;
&lt;code&gt;ClearValue&lt;/code&gt; removes only the local value, so whichever remaining input ranks highest — a theme style, for instance — becomes the effective value.&lt;/p&gt;




&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ClearValue&lt;/code&gt; also removes a binding or a &lt;code&gt;DynamicResource&lt;/code&gt;.&lt;/strong&gt;
Calling it on a property that carries only a binding, with no literal local value, discards the binding itself.
To supply a default through a binding, write the &lt;code&gt;Binding&lt;/code&gt; in the &lt;code&gt;Value&lt;/code&gt; of a &lt;code&gt;Setter&lt;/code&gt; instead of on the element.
To express the trigger condition itself through a binding, use the &lt;code&gt;Binding&lt;/code&gt; property of a &lt;code&gt;DataTrigger&lt;/code&gt;, which is of type &lt;code&gt;BindingBase&lt;/code&gt;.
A &lt;code&gt;Binding&lt;/code&gt; is accepted in that condition binding and in &lt;code&gt;Setter.Value&lt;/code&gt;, but not in the &lt;code&gt;Value&lt;/code&gt; of a &lt;code&gt;Trigger&lt;/code&gt; or &lt;code&gt;DataTrigger&lt;/code&gt;, which holds the value being compared against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assigning a local value replaces a binding.&lt;/strong&gt;
A plain assignment to a property that holds a binding replaces the deferred value outright.
A later &lt;code&gt;ClearValue&lt;/code&gt; call does not restore the binding.
Updating a dependency property of a &lt;code&gt;UserControl&lt;/code&gt; from inside the control with a plain assignment triggers the same replacement and detaches the binding set by the caller, whereas &lt;code&gt;SetCurrentValue&lt;/code&gt; preserves it (see &lt;a href="https://s-iguchi09.github.io/articles/wpf-usercontrol-dependencyproperty-binding-not-working/" rel="noopener noreferrer"&gt;Binding to a WPF UserControl's Own Dependency Property from Inside the Control&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Triggers in a theme style, and in its &lt;code&gt;ControlTemplate&lt;/code&gt;, lose to local values as well.&lt;/strong&gt;
Setting &lt;code&gt;Foreground&lt;/code&gt; as a local value on a &lt;code&gt;Button&lt;/code&gt; suppresses the trigger that greys out the text when the button is disabled.
Depending on how the default theme is implemented, that trigger sits at rank 7 (template triggers) or rank 9 (theme styles), but either way it ranks below a local value at rank 3.
Verify that the standard state feedback of a control is not being broken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If a trigger still has no visible effect after the local value is gone, examine the &lt;code&gt;ControlTemplate&lt;/code&gt;.&lt;/strong&gt;
The default template of a standard control may hard-code appearance such as the mouseover background.
In that case the trigger wins as a property value but never reaches the rendering, and replacing the template is required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A one-way binding or a literal value in an &lt;code&gt;ItemContainerStyle&lt;/code&gt; setter also loses to a local value on the container.&lt;/strong&gt;
When container state is supplied through a style on an &lt;code&gt;ItemsControl&lt;/code&gt;, assigning the same property from code produces a local value, and style values no longer reach that container afterwards.
A &lt;code&gt;Mode=TwoWay&lt;/code&gt; binding in the setter is the exception: the binding survives and the assigned value is written back to the source.
The concrete impact on &lt;code&gt;IsSelected&lt;/code&gt; and &lt;code&gt;IsExpanded&lt;/code&gt; of &lt;code&gt;TreeViewItem&lt;/code&gt; is covered in &lt;a href="https://s-iguchi09.github.io/articles/wpf-treeview-select-item-programmatically/" rel="noopener noreferrer"&gt;Selecting and Expanding a WPF TreeView Node from Code, and Why SelectedItem Is Read-Only&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same precedence does not apply to the &lt;code&gt;Style&lt;/code&gt; property itself.&lt;/strong&gt;
A &lt;code&gt;Style&lt;/code&gt; written on the element is an explicit style with local-value precedence (rank 3), while a style applied from a resource whose key matches the element type is an implicit style at rank 5.
When neither is present, the default (theme) style applies at rank 9.
An implicit style is not applied to an element that already has an explicit style.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource evaluation timing is a separate issue.&lt;/strong&gt;
A &lt;code&gt;StaticResource&lt;/code&gt; that is swapped at runtime and never updates is a matter of evaluation timing rather than precedence (see &lt;a href="https://s-iguchi09.github.io/articles/wpf-staticresource-vs-dynamicresource/" rel="noopener noreferrer"&gt;Why StaticResource Changes Are Not Reflected in WPF and How to Fix It&lt;/a&gt;).
The two problems should not be conflated.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Alternatives / Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;th&gt;Best suited for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Move the default into a &lt;code&gt;Setter&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Stays entirely in XAML and cannot invert the precedence&lt;/td&gt;
&lt;td&gt;Harder to give each element a different default&lt;/td&gt;
&lt;td&gt;The default choice in most situations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assign with &lt;code&gt;SetCurrentValue&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Creates no local value, so triggers keep working&lt;/td&gt;
&lt;td&gt;Requires code-behind&lt;/td&gt;
&lt;td&gt;Applying a temporary value at runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove the local value with &lt;code&gt;ClearValue&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Leaves the existing XAML untouched&lt;/td&gt;
&lt;td&gt;Also removes bindings, and the call timing must be managed&lt;/td&gt;
&lt;td&gt;Clearing a local value applied at runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replace the &lt;code&gt;ControlTemplate&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Controls even the appearance that a template hard-codes&lt;/td&gt;
&lt;td&gt;Verbose, and does not follow theme updates&lt;/td&gt;
&lt;td&gt;A default template that fixes the appearance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Start an animation from &lt;code&gt;EnterActions&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Rank 2, so it overrides a local value&lt;/td&gt;
&lt;td&gt;Stopping and rewinding must be managed, and animating the &lt;code&gt;Color&lt;/code&gt; of an unfrozen shared brush also changes every other element that references it&lt;/td&gt;
&lt;td&gt;State changes that need a transition effect&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;A style trigger that appears broken is often not a mistake in the trigger itself; a local value on the target property is a frequent cause.&lt;br&gt;
Local values sit at rank 3, style triggers at rank 6, and style setters at rank 8, and that ordering alone explains the behavior.&lt;/p&gt;

&lt;p&gt;Selection criteria are as follows.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The default is written in XAML:&lt;/strong&gt;
remove the attribute from the element and move it into a &lt;code&gt;Setter&lt;/code&gt; in the &lt;code&gt;Style&lt;/code&gt;.
This has the fewest side effects and should be considered first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The value changes at runtime from code-behind:&lt;/strong&gt;
use &lt;code&gt;SetCurrentValue&lt;/code&gt; instead of a plain assignment.
Because it does not overwrite the value source, a trigger that fires later still applies.
It has no effect on a property that already carries a local value, so clear that with &lt;code&gt;ClearValue&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A local value is already in place:&lt;/strong&gt;
clear it with &lt;code&gt;ClearValue&lt;/code&gt;, keeping in mind that any binding goes with it.
Supply the default from a &lt;code&gt;Setter&lt;/code&gt; if one is needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Confirming that the target property carries no local value, and keeping every default in a &lt;code&gt;Setter&lt;/code&gt;, removes this class of problem at design time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-binding-stringformat-number-currency-date/" rel="noopener noreferrer"&gt;Formatting Numbers, Currency, and Dates with Binding.StringFormat in WPF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-label-underscore-issue/" rel="noopener noreferrer"&gt;Why WPF Label Hides Underscores and How to Fix It&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-datepicker-custom-format/" rel="noopener noreferrer"&gt;Customising the DatePicker Display Format in WPF&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>wpf</category>
      <category>dotnet</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Controls with Custom Styles Fall Back to the Old Look Under the WPF Fluent Theme</title>
      <dc:creator>s-iguchi09</dc:creator>
      <pubDate>Tue, 08 Sep 2026 16:14:33 +0000</pubDate>
      <link>https://dev.to/s-iguchi09/controls-with-custom-styles-fall-back-to-the-old-look-under-the-wpf-fluent-theme-46ah</link>
      <guid>https://dev.to/s-iguchi09/controls-with-custom-styles-fall-back-to-the-old-look-under-the-wpf-fluent-theme-46ah</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://s-iguchi09.github.io/articles/wpf-fluent-theme-custom-style-not-applied/" rel="noopener noreferrer"&gt;s-iguchi09.github.io&lt;/a&gt;.&lt;br&gt;
A &lt;a href="https://s-iguchi09.github.io/ja/articles/wpf-fluent-theme-custom-style-not-applied/" rel="noopener noreferrer"&gt;Japanese version&lt;/a&gt; is also available.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;After the Fluent theme is introduced into an existing WPF application, some controls may keep their previous squared-off appearance instead of adopting the Fluent look.&lt;br&gt;
The symptom appears on controls that carry a &lt;code&gt;Style&lt;/code&gt; which is not chained to the Fluent style through &lt;code&gt;BasedOn&lt;/code&gt;.&lt;br&gt;
An &lt;strong&gt;implicit style&lt;/strong&gt; such as &lt;code&gt;&amp;lt;Style TargetType="Button"&amp;gt;&lt;/code&gt; is the most common way an application ends up in that state without the author noticing.&lt;br&gt;
Setting &lt;code&gt;Style="{x:Null}"&lt;/code&gt; to opt out of style application leaves the same legacy look.&lt;br&gt;
No exception is raised, no warning appears, and the output window stays silent, so the theme setup itself is easily mistaken for the cause.&lt;/p&gt;

&lt;p&gt;This article explains the behavior from how the Fluent theme is delivered to controls, and presents the fix based on &lt;code&gt;BasedOn&lt;/code&gt;.&lt;br&gt;
It also documents the placements where &lt;code&gt;BasedOn&lt;/code&gt; is written but never resolved, using a results table produced by measurement.&lt;/p&gt;


&lt;h2&gt;
  
  
  Prerequisites / Environment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Framework: WPF on .NET 9 / .NET 10 (&lt;code&gt;net9.0-windows&lt;/code&gt; / &lt;code&gt;net10.0-windows&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;OS: Windows 11 (standard colors; high contrast is out of scope)&lt;/li&gt;
&lt;li&gt;Theme activation: the &lt;code&gt;ThemeMode&lt;/code&gt; property, or merging the &lt;code&gt;Fluent.xaml&lt;/code&gt; resource dictionary directly&lt;/li&gt;
&lt;li&gt;Target: styles declared in &lt;code&gt;Application.Resources&lt;/code&gt;, &lt;code&gt;Window.Resources&lt;/code&gt;, or a separate resource dictionary file&lt;/li&gt;
&lt;li&gt;Architecture: the behavior is identical for MVVM and code-behind&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;ThemeMode&lt;/code&gt; exists on both &lt;code&gt;Application&lt;/code&gt; and &lt;code&gt;Window&lt;/code&gt;, so the theme can be set for the whole application or per window.&lt;br&gt;
The results table below includes combinations whose result depends on which one carries the setting.&lt;/p&gt;

&lt;p&gt;The pack URI for merging &lt;code&gt;Fluent.xaml&lt;/code&gt; directly is the following.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pack://application:,,,/PresentationFramework.Fluent;component/Themes/Fluent.xaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assign this URI to the &lt;code&gt;Source&lt;/code&gt; of a &lt;code&gt;ResourceDictionary&lt;/code&gt;.&lt;br&gt;
With &lt;code&gt;ThemeMode&lt;/code&gt;, that markup is unnecessary.&lt;br&gt;
In measurement, setting &lt;code&gt;ThemeMode&lt;/code&gt; merged the resolved &lt;code&gt;Fluent.Light.xaml&lt;/code&gt; or &lt;code&gt;Fluent.Dark.xaml&lt;/code&gt; automatically.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ThemeMode&lt;/code&gt; is published as an experimental API.&lt;br&gt;
Every implementation in this article sets it as a XAML attribute, so no suppression is required, but as &lt;a href="https://learn.microsoft.com/dotnet/desktop/wpf/whats-new/net90#thememode" rel="noopener noreferrer"&gt;what's new in WPF for .NET 9&lt;/a&gt; states, accessing it from code produces error &lt;code&gt;WPF0001&lt;/code&gt;.&lt;br&gt;
In that case, suppress it with &lt;code&gt;&amp;lt;NoWarn&amp;gt;$(NoWarn);WPF0001&amp;lt;/NoWarn&amp;gt;&lt;/code&gt; in the project file, or with &lt;code&gt;#pragma warning disable WPF0001&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;Enabling the Fluent theme takes a single attribute.&lt;br&gt;
Setting &lt;code&gt;ThemeMode&lt;/code&gt; on the &lt;code&gt;Application&lt;/code&gt; element in &lt;code&gt;App.xaml&lt;/code&gt; gives the whole application the Fluent appearance.&lt;br&gt;
The problem appears when the existing &lt;code&gt;App.xaml&lt;/code&gt; already carries an implicit style.&lt;br&gt;
The following &lt;code&gt;App.xaml&lt;/code&gt; applies the Fluent theme while widening the padding of &lt;code&gt;Button&lt;/code&gt; as before.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Application&lt;/span&gt; &lt;span class="na"&gt;x:Class=&lt;/span&gt;&lt;span class="s"&gt;"MyApp.App"&lt;/span&gt;
             &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://schemas.microsoft.com/winfx/2006/xaml/presentation"&lt;/span&gt;
             &lt;span class="na"&gt;xmlns:x=&lt;/span&gt;&lt;span class="s"&gt;"http://schemas.microsoft.com/winfx/2006/xaml"&lt;/span&gt;
             &lt;span class="na"&gt;StartupUri=&lt;/span&gt;&lt;span class="s"&gt;"MainWindow.xaml"&lt;/span&gt;
             &lt;span class="na"&gt;ThemeMode=&lt;/span&gt;&lt;span class="s"&gt;"Light"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Application.Resources&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Style&lt;/span&gt; &lt;span class="na"&gt;TargetType=&lt;/span&gt;&lt;span class="s"&gt;"Button"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;Setter&lt;/span&gt; &lt;span class="na"&gt;Property=&lt;/span&gt;&lt;span class="s"&gt;"Padding"&lt;/span&gt; &lt;span class="na"&gt;Value=&lt;/span&gt;&lt;span class="s"&gt;"16,6"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Style&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/Application.Resources&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Application&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The style only adds &lt;code&gt;Padding&lt;/code&gt;, and neither the template nor the colors are touched.&lt;br&gt;
Even so, the &lt;code&gt;Button&lt;/code&gt; does not adopt the Fluent appearance at run time.&lt;br&gt;
A &lt;code&gt;CheckBox&lt;/code&gt; placed in the same window renders with the Fluent look, which shows that the theme itself is active.&lt;/p&gt;


  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs-iguchi09.github.io%2Fimages%2Farticles%2Fwpf-fluent-theme-custom-style-not-applied%2Fimplicit-style-shadows-fluent.png" alt="A WPF window with the Fluent theme applied. The Save button has square corners and a gray background in the legacy appearance, while the Overwrite check box below it uses the rounded Fluent appearance." width="286" height="183"&gt;The state produced by the &lt;code&gt;App.xaml&lt;/code&gt; above, with the &lt;code&gt;{x:Type Button}&lt;/code&gt; implicit style placed directly in &lt;code&gt;Application.Resources&lt;/code&gt;. Captured on Windows 11 / .NET 10 with &lt;code&gt;ThemeMode=Light&lt;/code&gt;. The &lt;code&gt;Button&lt;/code&gt; has square corners and a gray background, while the untouched &lt;code&gt;CheckBox&lt;/code&gt; stays Fluent.
  


&lt;p&gt;Inspecting this button's template on .NET 10 with &lt;code&gt;ThemeMode=Light&lt;/code&gt; shows a &lt;code&gt;CornerRadius&lt;/code&gt; of &lt;code&gt;0&lt;/code&gt; and a background of &lt;code&gt;#FFDDDDDD&lt;/code&gt; on the inner &lt;code&gt;Border&lt;/code&gt;.&lt;br&gt;
&lt;a href="https://learn.microsoft.com/dotnet/desktop/wpf/whats-new/net90#thememode" rel="noopener noreferrer"&gt;What's new in WPF for .NET 9&lt;/a&gt; states that the default &lt;code&gt;ThemeMode&lt;/code&gt; of &lt;code&gt;None&lt;/code&gt; uses &lt;a href="https://learn.microsoft.com/dotnet/desktop/wpf/controls/styles-templates-overview#available-built-in-themes" rel="noopener noreferrer"&gt;Aero2&lt;/a&gt;.&lt;br&gt;
The measured values match the Aero2 &lt;code&gt;Button&lt;/code&gt; on Windows 11 with standard colors, which confirms that no part of the Fluent style is in effect.&lt;/p&gt;


&lt;h2&gt;
  
  
  Cause / Background
&lt;/h2&gt;

&lt;p&gt;The cause lies in the path through which the Fluent theme reaches a control.&lt;/p&gt;
&lt;h3&gt;
  
  
  Fluent is delivered as implicit styles, not as a theme style
&lt;/h3&gt;

&lt;p&gt;The earlier WPF themes such as Aero2 are applied as the &lt;strong&gt;theme style&lt;/strong&gt; of a control.&lt;br&gt;
&lt;a href="https://learn.microsoft.com/dotnet/desktop/wpf/properties/dependency-property-value-precedence" rel="noopener noreferrer"&gt;Dependency property value precedence&lt;/a&gt; ranks the theme style as the weakest source among styles, with values from the application's own style layered above it.&lt;br&gt;
Measurement confirms that a control carrying an implicit style that sets nothing but &lt;code&gt;Padding&lt;/code&gt; still receives the Aero2 template.&lt;br&gt;
Unless &lt;a href="https://learn.microsoft.com/dotnet/api/system.windows.frameworkelement.overridesdefaultstyle" rel="noopener noreferrer"&gt;&lt;code&gt;OverridesDefaultStyle&lt;/code&gt;&lt;/a&gt; is set to &lt;code&gt;true&lt;/code&gt;, a theme style keeps supplying the template even when the application assigns &lt;code&gt;Style&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Fluent theme does not use that path.&lt;br&gt;
The reference for the &lt;a href="https://learn.microsoft.com/dotnet/api/system.windows.application.thememode" rel="noopener noreferrer"&gt;&lt;code&gt;Application.ThemeMode&lt;/code&gt; property&lt;/a&gt; states that setting the property loads the Fluent theme dictionaries into the application resources.&lt;br&gt;
As far as style delivery is concerned, Fluent arrives as a &lt;strong&gt;resource dictionary merged into the resources of the element that carries &lt;code&gt;ThemeMode&lt;/code&gt;&lt;/strong&gt; rather than as a theme style.&lt;br&gt;
The reference for the &lt;a href="https://learn.microsoft.com/dotnet/api/system.windows.window.thememode" rel="noopener noreferrer"&gt;&lt;code&gt;Window.ThemeMode&lt;/code&gt; property&lt;/a&gt; likewise states that setting it on a &lt;code&gt;Window&lt;/code&gt; loads the Fluent theme dictionaries into that window's resources.&lt;br&gt;
The dictionary also holds many brushes and numeric resources; the implicit styles keyed by values such as &lt;code&gt;{x:Type Button}&lt;/code&gt; are one part of it.&lt;/p&gt;

&lt;p&gt;This difference is observable in measurement.&lt;br&gt;
A control with &lt;code&gt;Style="{x:Null}"&lt;/code&gt;, which explicitly opts out of style application, renders with the Aero2 appearance even while the Fluent theme is active.&lt;br&gt;
If Fluent were supplied as a theme style, clearing &lt;code&gt;Style&lt;/code&gt; would leave the Fluent appearance in place.&lt;/p&gt;
&lt;h3&gt;
  
  
  A style with the same key hides the Fluent style
&lt;/h3&gt;

&lt;p&gt;The key of the Fluent implicit style is exactly the key that &lt;code&gt;&amp;lt;Style TargetType="Button"&amp;gt;&lt;/code&gt; produces in application code: &lt;code&gt;{x:Type Button}&lt;/code&gt;.&lt;br&gt;
When the same key exists in two places, resource lookup rules determine which one is used.&lt;br&gt;
In both placements described below, the application style is the one selected.&lt;/p&gt;

&lt;p&gt;For a style placed directly in &lt;code&gt;Application.Resources&lt;/code&gt;, the outcome follows the precedence between a dictionary and the dictionaries merged into it.&lt;br&gt;
The article on &lt;a href="https://learn.microsoft.com/dotnet/desktop/wpf/systems/xaml-resources-merged-dictionaries" rel="noopener noreferrer"&gt;merged resource dictionaries&lt;/a&gt; states that when a key is defined in the primary dictionary and also in a merged dictionary, the resource returned comes from the primary dictionary.&lt;br&gt;
&lt;code&gt;ThemeMode&lt;/code&gt; adds Fluent as a merged dictionary, so the style written directly in &lt;code&gt;Application.Resources&lt;/code&gt; takes precedence.&lt;/p&gt;

&lt;p&gt;For a style placed in an inner scope such as &lt;code&gt;Window.Resources&lt;/code&gt;, the outcome follows scope proximity.&lt;br&gt;
The article on &lt;a href="https://learn.microsoft.com/dotnet/desktop/wpf/controls/styles-templates-overview#shared-resources-and-themes" rel="noopener noreferrer"&gt;styles and templates&lt;/a&gt; explains that the search for an element's style walks up the element tree, then looks in the application resources, and consults the theme last.&lt;br&gt;
&lt;code&gt;Window.Resources&lt;/code&gt; is examined before &lt;code&gt;Application.Resources&lt;/code&gt;, so the application style is selected.&lt;/p&gt;

&lt;p&gt;In both cases the application style does not &lt;strong&gt;extend&lt;/strong&gt; the Fluent style; it &lt;strong&gt;replaces and hides&lt;/strong&gt; it entirely.&lt;br&gt;
Once hidden, the Fluent template is no longer supplied, and the control falls back to the built-in WPF theme style, Aero2.&lt;br&gt;
That is why a style adding nothing but &lt;code&gt;Padding&lt;/code&gt; discards the whole Fluent appearance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All of this describes a style written without &lt;code&gt;BasedOn&lt;/code&gt;.&lt;/strong&gt; Where the original can be inherited through &lt;code&gt;BasedOn&lt;/code&gt;, the template survives while your own setters still apply.&lt;br&gt;
The last two rows of the figure below measure that for a &lt;code&gt;TextBox&lt;/code&gt; style placed in &lt;code&gt;Window.Resources&lt;/code&gt;.&lt;br&gt;
Depending on where the style lives, though, &lt;code&gt;BasedOn&lt;/code&gt; itself may fail to resolve. The Solution section covers that condition.&lt;/p&gt;



&lt;p&gt;Which source supplied the template can be told apart by the named parts inside it.&lt;br&gt;
The Fluent &lt;code&gt;TextBox&lt;/code&gt; template holds a &lt;code&gt;DeleteButton&lt;/code&gt;; the classic theme does not.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Figure:&lt;/strong&gt; A table of the named parts in the TextBox template per way the theme reaches the control. DeleteButton is present on the row where ThemeMode is set and on the row merging Fluent.xaml directly. An implicit style without BasedOn removes DeleteButton on either route, leaving only PART_ContentHost, while the rows whose implicit style inherits through BasedOn keep DeleteButton on both routes.&lt;/p&gt;

&lt;p&gt;Measured on .NET 10 / Windows 11. The &lt;code&gt;Style applied&lt;/code&gt; column reports whether the &lt;code&gt;Style&lt;/code&gt; property is filled in (an implicit style) or left &lt;code&gt;null&lt;/code&gt; (a classic theme style).&lt;/p&gt;

&lt;p&gt;The diagram is rendered in the &lt;a href="https://s-iguchi09.github.io/articles/wpf-fluent-theme-custom-style-not-applied/" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The point is the second row, where &lt;code&gt;Style applied&lt;/code&gt; reads &lt;code&gt;implicit style&lt;/code&gt;.&lt;/strong&gt; Setting &lt;code&gt;ThemeMode&lt;/code&gt; alone fills in the &lt;code&gt;Style&lt;/code&gt; property, showing that Fluent arrives as an implicit style rather than a theme style.&lt;br&gt;
On the first row, without &lt;code&gt;ThemeMode&lt;/code&gt;, &lt;code&gt;Style&lt;/code&gt; stays &lt;code&gt;null&lt;/code&gt; and the template comes from the classic theme style.&lt;/p&gt;

&lt;p&gt;On the third row, an application-side implicit style under the same key that carries no &lt;code&gt;BasedOn&lt;/code&gt; makes &lt;code&gt;DeleteButton&lt;/code&gt; disappear.&lt;br&gt;
&lt;code&gt;Padding&lt;/code&gt; reads 8, so the application style did take effect. &lt;strong&gt;It is precisely because it took effect that the Fluent style was replaced and its template lost with it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The last two rows inherit the original through &lt;code&gt;BasedOn&lt;/code&gt;. On both routes — &lt;code&gt;ThemeMode&lt;/code&gt; and a direct merge of &lt;code&gt;Fluent.xaml&lt;/code&gt; — &lt;code&gt;Padding&lt;/code&gt; reads 8 just the same and &lt;code&gt;DeleteButton&lt;/code&gt; survives.&lt;/p&gt;

&lt;p&gt;What this figure measures is an implicit &lt;code&gt;TextBox&lt;/code&gt; style placed in &lt;code&gt;Window.Resources&lt;/code&gt;.&lt;br&gt;
A &lt;code&gt;Button&lt;/code&gt;, or a style placed directly in &lt;code&gt;Application.Resources&lt;/code&gt;, lands elsewhere; the table in the Solution section covers those.&lt;/p&gt;


&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;Add &lt;code&gt;BasedOn&lt;/code&gt; to the style so that it inherits the Fluent implicit style.&lt;br&gt;
Writing &lt;code&gt;BasedOn="{StaticResource {x:Type Button}}"&lt;/code&gt; layers the local &lt;code&gt;Setter&lt;/code&gt; elements on top of the Fluent style.&lt;/p&gt;

&lt;p&gt;This markup carries one constraint that must be respected.&lt;br&gt;
&lt;strong&gt;When the key passed to &lt;code&gt;BasedOn&lt;/code&gt; is the same as the style's own key, and that key also exists in a dictionary merged into the dictionary declaring the style (including nested merged dictionaries), &lt;code&gt;BasedOn&lt;/code&gt; is left unresolved and stays &lt;code&gt;null&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;
No exception is raised, and the legacy appearance remains.&lt;br&gt;
This condition was derived from measurement on .NET 9 and .NET 10; it is not a description of how &lt;code&gt;StaticResource&lt;/code&gt; resolves internally.&lt;/p&gt;

&lt;p&gt;Declaring an implicit style directly inside &lt;code&gt;Application.Resources&lt;/code&gt; meets that condition.&lt;br&gt;
&lt;code&gt;ThemeMode&lt;/code&gt; adds the Fluent dictionaries as merged dictionaries of the very &lt;code&gt;Application.Resources&lt;/code&gt; that declares the style.&lt;/p&gt;

&lt;p&gt;The rule above states only when the reference &lt;strong&gt;fails&lt;/strong&gt;, and the converse does not hold: avoiding the condition does not guarantee that &lt;code&gt;BasedOn&lt;/code&gt; reaches Fluent.&lt;br&gt;
As the table below shows, some placements fail to reach Fluent without meeting the condition.&lt;/p&gt;

&lt;p&gt;The figure below contrasts placing the style directly in &lt;code&gt;Application.Resources&lt;/code&gt; with moving it into a dedicated resource dictionary file, referred to here as &lt;code&gt;Styles.xaml&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Figure:&lt;/strong&gt; On the left, a style placed directly in Application.Resources, where the path toward Fluent.Light.xaml inside MergedDictionaries is blocked and BasedOn becomes null. On the right, Styles.xaml added to MergedDictionaries, where the style inside it reaches Fluent.Light.xaml in the same MergedDictionaries.&lt;/p&gt;

&lt;p&gt;The relationship between the &lt;code&gt;BasedOn&lt;/code&gt; target and the dictionary that declares the style. A dashed frame is &lt;code&gt;MergedDictionaries&lt;/code&gt;, a white box is a resource dictionary, and the lighter box inside it is a style. On the left the style sits directly in &lt;code&gt;Application.Resources&lt;/code&gt;, so the referenced key matches the style's own key and also exists in a dictionary merged into that same dictionary, leaving it unresolved. On the right the style lives in a separate file inside &lt;code&gt;MergedDictionaries&lt;/code&gt;, so it reaches the Fluent entry in the same &lt;code&gt;MergedDictionaries&lt;/code&gt;. Both set &lt;code&gt;ThemeMode&lt;/code&gt; on &lt;code&gt;Application&lt;/code&gt;, and both were confirmed on .NET 9 and .NET 10 running Windows 11.&lt;/p&gt;

&lt;p&gt;The diagram is rendered in the &lt;a href="https://s-iguchi09.github.io/articles/wpf-fluent-theme-custom-style-not-applied/" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A failed reference can be spotted quickly by inspecting the style at run time.&lt;br&gt;
For a style placed in &lt;code&gt;Application.Resources&lt;/code&gt;, &lt;code&gt;basedOn&lt;/code&gt; below being &lt;code&gt;null&lt;/code&gt; means it did not resolve.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Style&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Style&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;Style&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;basedOn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;BasedOn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a style placed in &lt;code&gt;Window.Resources&lt;/code&gt;, read the target window's &lt;code&gt;Resources&lt;/code&gt; the same way instead of &lt;code&gt;Application.Current.Resources&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This check misses cases, however.&lt;br&gt;
A non-&lt;code&gt;null&lt;/code&gt; &lt;code&gt;basedOn&lt;/code&gt; can still point at the default theme style rather than at Fluent.&lt;br&gt;
Make the final call by rendering the control and looking for Fluent-specific traits such as rounded corners.&lt;/p&gt;

&lt;p&gt;The configuration that reliably inherits Fluent is therefore the following.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set &lt;code&gt;ThemeMode&lt;/code&gt; on &lt;code&gt;Application&lt;/code&gt;, not on a &lt;code&gt;Window&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Keep the style out of &lt;code&gt;Application.Resources&lt;/code&gt; itself (moving it into a dedicated resource dictionary file and merging that file is the most manageable way to do so).&lt;/li&gt;
&lt;li&gt;Do not merge the Fluent dictionary inside that file.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;BasedOn="{StaticResource {x:Type Button}}"&lt;/code&gt; to the style.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Move the style into a separate resource dictionary
&lt;/h3&gt;

&lt;p&gt;First, extract the implicit style into &lt;code&gt;Styles.xaml&lt;/code&gt;.&lt;br&gt;
Specify &lt;code&gt;{StaticResource {x:Type Button}}&lt;/code&gt; for &lt;code&gt;BasedOn&lt;/code&gt; so the Fluent style becomes the base.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ResourceDictionary&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://schemas.microsoft.com/winfx/2006/xaml/presentation"&lt;/span&gt;
                    &lt;span class="na"&gt;xmlns:x=&lt;/span&gt;&lt;span class="s"&gt;"http://schemas.microsoft.com/winfx/2006/xaml"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Style&lt;/span&gt; &lt;span class="na"&gt;TargetType=&lt;/span&gt;&lt;span class="s"&gt;"Button"&lt;/span&gt; &lt;span class="na"&gt;BasedOn=&lt;/span&gt;&lt;span class="s"&gt;"{StaticResource {x:Type Button}}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Setter&lt;/span&gt; &lt;span class="na"&gt;Property=&lt;/span&gt;&lt;span class="s"&gt;"Padding"&lt;/span&gt; &lt;span class="na"&gt;Value=&lt;/span&gt;&lt;span class="s"&gt;"16,6"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/Style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ResourceDictionary&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not merge the Fluent dictionary into this file.&lt;br&gt;
Merging &lt;code&gt;Fluent.xaml&lt;/code&gt; inside &lt;code&gt;Styles.xaml&lt;/code&gt; makes the target a merged dictionary of &lt;code&gt;Styles.xaml&lt;/code&gt;, and the reference stops resolving.&lt;/p&gt;
&lt;h3&gt;
  
  
  Merge the dictionary from App.xaml
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;App.xaml&lt;/code&gt; is then limited to setting &lt;code&gt;ThemeMode&lt;/code&gt; and merging &lt;code&gt;Styles.xaml&lt;/code&gt;.&lt;br&gt;
Keeping the style itself out of &lt;code&gt;Application.Resources&lt;/code&gt; is the essential point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Application&lt;/span&gt; &lt;span class="na"&gt;x:Class=&lt;/span&gt;&lt;span class="s"&gt;"MyApp.App"&lt;/span&gt;
             &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://schemas.microsoft.com/winfx/2006/xaml/presentation"&lt;/span&gt;
             &lt;span class="na"&gt;xmlns:x=&lt;/span&gt;&lt;span class="s"&gt;"http://schemas.microsoft.com/winfx/2006/xaml"&lt;/span&gt;
             &lt;span class="na"&gt;StartupUri=&lt;/span&gt;&lt;span class="s"&gt;"MainWindow.xaml"&lt;/span&gt;
             &lt;span class="na"&gt;ThemeMode=&lt;/span&gt;&lt;span class="s"&gt;"Light"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Application.Resources&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ResourceDictionary&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ResourceDictionary.MergedDictionaries&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ResourceDictionary&lt;/span&gt; &lt;span class="na"&gt;Source=&lt;/span&gt;&lt;span class="s"&gt;"pack://application:,,,/MyApp;component/Styles.xaml"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/ResourceDictionary.MergedDictionaries&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ResourceDictionary&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/Application.Resources&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Application&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Fluent dictionary merged by &lt;code&gt;ThemeMode&lt;/code&gt; and &lt;code&gt;Styles.xaml&lt;/code&gt; become sibling merged dictionaries within &lt;code&gt;Application.Resources&lt;/code&gt;.&lt;br&gt;
With this layout, &lt;code&gt;BasedOn&lt;/code&gt; resolves to the Fluent implicit style and only the &lt;code&gt;Padding&lt;/code&gt; setter is layered on top.&lt;br&gt;
In measurement, the Fluent dictionary added by &lt;code&gt;ThemeMode&lt;/code&gt; precedes &lt;code&gt;Styles.xaml&lt;/code&gt;, so the ordering issue described later does not arise.&lt;br&gt;
For a file in the same project whose build action is &lt;code&gt;Resource&lt;/code&gt;, a relative path such as &lt;code&gt;Source="Styles.xaml"&lt;/code&gt; also works.&lt;/p&gt;


  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs-iguchi09.github.io%2Fimages%2Farticles%2Fwpf-fluent-theme-custom-style-not-applied%2Fimplicit-style-basedon-fluent.png" alt="A WPF window with the Fluent theme applied. The Save button now has rounded corners and a light background in the Fluent appearance, matching the Overwrite check box below it." width="286" height="183"&gt;The state produced by applying the &lt;code&gt;Styles.xaml&lt;/code&gt; and &lt;code&gt;App.xaml&lt;/code&gt; above as written. Captured on Windows 11 / .NET 10 with &lt;code&gt;ThemeMode=Light&lt;/code&gt;. Compared with the previous figure, the &lt;code&gt;Button&lt;/code&gt; now has rounded corners and a lighter background. In measurement, the &lt;code&gt;Padding&lt;/code&gt; setter survives unchanged in this state.
  

&lt;h3&gt;
  
  
  Placement and resolution results
&lt;/h3&gt;

&lt;p&gt;Whether &lt;code&gt;BasedOn&lt;/code&gt; resolves depends on the combination of where the style lives and where Fluent comes from.&lt;br&gt;
The following results were measured for &lt;code&gt;Button&lt;/code&gt; on both .NET 9 and .NET 10.&lt;br&gt;
The &lt;code&gt;BasedOn&lt;/code&gt; column shows the key passed to &lt;code&gt;BasedOn="{StaticResource ...}"&lt;/code&gt;.&lt;br&gt;
For the row with &lt;code&gt;x:Key&lt;/code&gt;, the keyed style was applied explicitly through &lt;code&gt;Style="{StaticResource ...}"&lt;/code&gt;, with no implicit &lt;code&gt;{x:Type Button}&lt;/code&gt; style placed alongside it.&lt;br&gt;
The table covers only placements where the custom style actually applies to the control; for placements where the custom style never applies at all, see the notes below.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Placement of the style&lt;/th&gt;
&lt;th&gt;Source of Fluent&lt;/th&gt;
&lt;th&gt;&lt;code&gt;BasedOn&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Directly in &lt;code&gt;Application.Resources&lt;/code&gt; (implicit)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ThemeMode&lt;/code&gt; on &lt;code&gt;Application&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;Legacy look&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Directly in &lt;code&gt;Application.Resources&lt;/code&gt; (implicit)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ThemeMode&lt;/code&gt; on &lt;code&gt;Application&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{x:Type Button}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Legacy look (&lt;code&gt;BasedOn&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Directly in &lt;code&gt;Application.Resources&lt;/code&gt; (implicit)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ThemeMode&lt;/code&gt; on &lt;code&gt;Application&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DefaultButtonStyle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fluent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Directly in &lt;code&gt;Application.Resources&lt;/code&gt; (with &lt;code&gt;x:Key&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ThemeMode&lt;/code&gt; on &lt;code&gt;Application&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{x:Type Button}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fluent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Separate file merged into &lt;code&gt;Application.Resources&lt;/code&gt; (implicit)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ThemeMode&lt;/code&gt; on &lt;code&gt;Application&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{x:Type Button}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fluent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Window.Resources&lt;/code&gt; (implicit)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ThemeMode&lt;/code&gt; on &lt;code&gt;Application&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{x:Type Button}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fluent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Separate file merged into &lt;code&gt;Window.Resources&lt;/code&gt; (implicit)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ThemeMode&lt;/code&gt; on &lt;code&gt;Application&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{x:Type Button}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fluent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Window.Resources&lt;/code&gt; (implicit)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ThemeMode&lt;/code&gt; on the same &lt;code&gt;Window&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{x:Type Button}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Legacy look (&lt;code&gt;BasedOn&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Separate file merged into &lt;code&gt;Window.Resources&lt;/code&gt; (implicit)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ThemeMode&lt;/code&gt; on the same &lt;code&gt;Window&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{x:Type Button}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Legacy look (&lt;code&gt;BasedOn&lt;/code&gt; is not &lt;code&gt;null&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Separate file that merges &lt;code&gt;Fluent.xaml&lt;/code&gt; itself (implicit)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Fluent.xaml&lt;/code&gt; in that same file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{x:Type Button}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Legacy look (&lt;code&gt;BasedOn&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each of the three rows where &lt;code&gt;BasedOn&lt;/code&gt; becomes &lt;code&gt;null&lt;/code&gt; meets the same condition: the key passed to &lt;code&gt;BasedOn&lt;/code&gt; matches the style's own key, and that key also exists in a dictionary merged into the dictionary declaring the style.&lt;br&gt;
The row referencing &lt;code&gt;DefaultButtonStyle&lt;/code&gt;, whose key differs, and the row whose style carries an &lt;code&gt;x:Key&lt;/code&gt; do not meet that condition, so they resolve even though they sit directly in &lt;code&gt;Application.Resources&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The row pairing a separate file merged into &lt;code&gt;Window.Resources&lt;/code&gt; with &lt;code&gt;ThemeMode&lt;/code&gt; on that same &lt;code&gt;Window&lt;/code&gt;, by contrast, keeps the legacy look without meeting the condition.&lt;br&gt;
The dictionary declaring the style in that row is the separate file, and Fluent sits outside it in the merged dictionaries of &lt;code&gt;Window.Resources&lt;/code&gt;, so the condition does not apply.&lt;br&gt;
Even so, &lt;code&gt;BasedOn&lt;/code&gt; was not &lt;code&gt;null&lt;/code&gt; in measurement; it resolved to the default theme style instead of Fluent.&lt;br&gt;
When &lt;code&gt;ThemeMode&lt;/code&gt; is set on a &lt;code&gt;Window&lt;/code&gt;, moving the style into a separate file is not enough.&lt;br&gt;
Moving &lt;code&gt;ThemeMode&lt;/code&gt; up to &lt;code&gt;Application&lt;/code&gt; resolves it.&lt;br&gt;
In measurement, both a style directly in &lt;code&gt;Window.Resources&lt;/code&gt; and one in a file merged into &lt;code&gt;Window.Resources&lt;/code&gt; reached Fluent that way.&lt;br&gt;
Moving the style into &lt;code&gt;Application.Resources&lt;/code&gt; itself is a separate matter, and the second row of the table shows that it does not resolve.&lt;/p&gt;
&lt;h3&gt;
  
  
  Alternative: when the App.xaml structure cannot change
&lt;/h3&gt;

&lt;p&gt;When the file layout is fixed, reference &lt;code&gt;DefaultButtonStyle&lt;/code&gt;, which the Fluent theme dictionary provides.&lt;br&gt;
Because that key differs from the style's own &lt;code&gt;{x:Type Button}&lt;/code&gt; key, it resolves even directly inside &lt;code&gt;Application.Resources&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Application.Resources&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Style&lt;/span&gt; &lt;span class="na"&gt;TargetType=&lt;/span&gt;&lt;span class="s"&gt;"Button"&lt;/span&gt; &lt;span class="na"&gt;BasedOn=&lt;/span&gt;&lt;span class="s"&gt;"{StaticResource DefaultButtonStyle}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Setter&lt;/span&gt; &lt;span class="na"&gt;Property=&lt;/span&gt;&lt;span class="s"&gt;"Padding"&lt;/span&gt; &lt;span class="na"&gt;Value=&lt;/span&gt;&lt;span class="s"&gt;"16,6"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/Style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Application.Resources&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Measurement shows that the Fluent &lt;code&gt;{x:Type Button}&lt;/code&gt; style carries no setter of its own and that its &lt;code&gt;BasedOn&lt;/code&gt; is &lt;code&gt;DefaultButtonStyle&lt;/code&gt; itself.&lt;br&gt;
For &lt;code&gt;Button&lt;/code&gt; the two are effectively equivalent, so this form loses no setter.&lt;br&gt;
&lt;code&gt;DefaultButtonStyle&lt;/code&gt; is not a documented key and depends on the internal structure of &lt;code&gt;Fluent.xaml&lt;/code&gt;, so treat it as a stopgap.&lt;br&gt;
Applying the same approach to another control requires checking that control's key name, and whether its implicit style is equivalent to the style under that key.&lt;br&gt;
In measurement the &lt;code&gt;TargetType&lt;/code&gt; of &lt;code&gt;DefaultButtonStyle&lt;/code&gt; is &lt;code&gt;ButtonBase&lt;/code&gt;, not &lt;code&gt;Button&lt;/code&gt;, so the referenced style may target a base type.&lt;/p&gt;
&lt;h3&gt;
  
  
  Alternative: confining the change to part of the UI
&lt;/h3&gt;

&lt;p&gt;A keyed style resolves &lt;code&gt;BasedOn&lt;/code&gt; against &lt;code&gt;{x:Type Button}&lt;/code&gt; even directly inside &lt;code&gt;Application.Resources&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Application.Resources&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Style&lt;/span&gt; &lt;span class="na"&gt;x:Key=&lt;/span&gt;&lt;span class="s"&gt;"WideButton"&lt;/span&gt; &lt;span class="na"&gt;TargetType=&lt;/span&gt;&lt;span class="s"&gt;"Button"&lt;/span&gt;
         &lt;span class="na"&gt;BasedOn=&lt;/span&gt;&lt;span class="s"&gt;"{StaticResource {x:Type Button}}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Setter&lt;/span&gt; &lt;span class="na"&gt;Property=&lt;/span&gt;&lt;span class="s"&gt;"Padding"&lt;/span&gt; &lt;span class="na"&gt;Value=&lt;/span&gt;&lt;span class="s"&gt;"16,6"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/Style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Application.Resources&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because this style does not occupy the &lt;code&gt;{x:Type Button}&lt;/code&gt; key itself, the target of the reference is the Fluent implicit style.&lt;br&gt;
A keyed style is not applied automatically, so every control needs an explicit &lt;code&gt;Style="{StaticResource WideButton}"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This form reaches Fluent only as long as no implicit &lt;code&gt;{x:Type Button}&lt;/code&gt; style remains directly in &lt;code&gt;Application.Resources&lt;/code&gt;.&lt;br&gt;
In measurement, keeping an implicit style alongside it made &lt;code&gt;BasedOn&lt;/code&gt; resolve to that implicit style instead, leaving the legacy appearance.&lt;br&gt;
A keyed style applied without &lt;code&gt;BasedOn&lt;/code&gt; also falls back to the legacy appearance, just as an implicit one does.&lt;/p&gt;




&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nothing reports the failure.&lt;/strong&gt; An unresolved &lt;code&gt;BasedOn&lt;/code&gt; produces no exception and no warning in the output window. Only the rendered appearance reveals it, so verify migrations visually against Fluent-specific traits such as rounded corners.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set &lt;code&gt;ThemeMode&lt;/code&gt; on &lt;code&gt;Application&lt;/code&gt;.&lt;/strong&gt; Setting it per window creates placements where &lt;code&gt;BasedOn&lt;/code&gt; resolves to the default theme style rather than Fluent, even with the style moved into a separate file. A separate problem also appears: with &lt;code&gt;ThemeMode&lt;/code&gt; on a &lt;code&gt;Window&lt;/code&gt; and the custom styles merged into &lt;code&gt;Application.Resources&lt;/code&gt;, the appearance does turn Fluent but the custom setters have no effect. The Fluent implicit style that &lt;code&gt;ThemeMode&lt;/code&gt; placed in &lt;code&gt;Window.Resources&lt;/code&gt; is found before the custom style in the outer &lt;code&gt;Application.Resources&lt;/code&gt;, so the custom style never applies. Unless individual windows need different light/dark modes, keep the setting on &lt;code&gt;Application&lt;/code&gt;. Note that once &lt;code&gt;Application&lt;/code&gt; carries anything other than &lt;code&gt;None&lt;/code&gt;, a &lt;code&gt;Window&lt;/code&gt; can no longer be set back to &lt;code&gt;None&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each control type needs its own fix.&lt;/strong&gt; Implicit style keys are per type, so repairing &lt;code&gt;Button&lt;/code&gt; leaves the styles of &lt;code&gt;TextBox&lt;/code&gt; or &lt;code&gt;CheckBox&lt;/code&gt; untouched. Enumerate the declared styles by type before migrating an existing application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The set of controls Fluent covers differs by version.&lt;/strong&gt; &lt;a href="https://learn.microsoft.com/dotnet/desktop/wpf/whats-new/net100#fluent-style-changes" rel="noopener noreferrer"&gt;What's new in WPF for .NET 10&lt;/a&gt; added styles for controls such as &lt;code&gt;GroupBox&lt;/code&gt;. Measurement confirms that the implicit style for &lt;code&gt;GroupBox&lt;/code&gt; is absent from Fluent on .NET 9 and present on .NET 10. For a control that Fluent does not style implicitly, &lt;code&gt;BasedOn&lt;/code&gt; resolves to the default theme style instead of becoming &lt;code&gt;null&lt;/code&gt;, so a &lt;code&gt;null&lt;/code&gt; check does not catch it; judge by the unchanged appearance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Style="{x:Null}"&lt;/code&gt; opts out of Fluent.&lt;/strong&gt; Disabling style application leaves that control with the Aero2 appearance. Review any place that uses &lt;code&gt;{x:Null}&lt;/code&gt; to restore a default look before adopting Fluent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When using &lt;code&gt;ThemeMode&lt;/code&gt;, do not merge the Fluent theme dictionaries by hand.&lt;/strong&gt; The reference for the &lt;a href="https://learn.microsoft.com/dotnet/api/system.windows.application.thememode" rel="noopener noreferrer"&gt;&lt;code&gt;Application.ThemeMode&lt;/code&gt; property&lt;/a&gt; recommends against adding the Fluent theme dictionaries manually when the property is set, because the manually added ones take precedence. The same reference notes that &lt;code&gt;ThemeMode&lt;/code&gt; also controls the window backdrop and dark mode, so it is not equivalent to merging &lt;code&gt;Fluent.xaml&lt;/code&gt; by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The order of merged dictionaries changes the result.&lt;/strong&gt; As the article on &lt;a href="https://learn.microsoft.com/dotnet/desktop/wpf/systems/xaml-resources-merged-dictionaries" rel="noopener noreferrer"&gt;merged resource dictionaries&lt;/a&gt; states, when the same key appears twice in one &lt;code&gt;MergedDictionaries&lt;/code&gt;, the later entry wins. In measurement, merging &lt;code&gt;Fluent.xaml&lt;/code&gt; after the custom styles discarded the custom setters. Place &lt;code&gt;Fluent.xaml&lt;/code&gt; before the custom styles when merging it manually.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A direct &lt;code&gt;Fluent.xaml&lt;/code&gt; merge follows the Windows theme setting.&lt;/strong&gt; In measurement, merging &lt;code&gt;Fluent.xaml&lt;/code&gt; on a machine set to dark mode loaded the dark dictionary at startup. Whether it follows a theme switch made while the application is running was not verified for this article. To pin the appearance, drop the manual merge and set &lt;code&gt;ThemeMode&lt;/code&gt; to &lt;code&gt;Light&lt;/code&gt; or &lt;code&gt;Dark&lt;/code&gt;. Adding &lt;code&gt;ThemeMode&lt;/code&gt; while keeping the manual merge leaves the control colors unpinned, because the manual merge wins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ThemeMode&lt;/code&gt; and Fluent are still changing.&lt;/strong&gt; &lt;code&gt;ThemeMode&lt;/code&gt; remains an experimental API as of .NET 10, and its reference notes that it may be removed in a future version. The Fluent style implementation is also still in progress. The results table above reflects .NET 9 and .NET 10, and later versions warrant a recheck.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Alternatives / Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;th&gt;Best suited for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Separate dictionary with &lt;code&gt;BasedOn="{StaticResource {x:Type Button}}"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Relies only on documented markup, and implicit styles keep applying automatically&lt;/td&gt;
&lt;td&gt;Requires changing the file layout&lt;/td&gt;
&lt;td&gt;The standard case of migrating an existing app to Fluent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BasedOn="{StaticResource DefaultButtonStyle}"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Leaves the &lt;code&gt;App.xaml&lt;/code&gt; structure untouched&lt;/td&gt;
&lt;td&gt;Depends on a key that is not documented&lt;/td&gt;
&lt;td&gt;A stopgap when the file layout cannot change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyed style applied explicitly with &lt;code&gt;BasedOn&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Limits the scope of the change to the chosen controls&lt;/td&gt;
&lt;td&gt;Every usage needs an explicit &lt;code&gt;Style&lt;/code&gt; reference&lt;/td&gt;
&lt;td&gt;Adjusting only certain screens or controls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keep the existing styles without Fluent&lt;/td&gt;
&lt;td&gt;No migration work&lt;/td&gt;
&lt;td&gt;Loses the Windows 11 appearance and does not follow the light/dark theme&lt;/td&gt;
&lt;td&gt;Applications that already ship a fully custom design&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;As far as style delivery is concerned, the Fluent theme arrives as implicit styles inside a resource dictionary rather than as a theme style.&lt;br&gt;
An application style using the same &lt;code&gt;{x:Type Button}&lt;/code&gt; key therefore hides the Fluent style, and the control falls back to the Aero2 appearance.&lt;/p&gt;

&lt;p&gt;The fix is inheritance through &lt;code&gt;BasedOn="{StaticResource {x:Type Button}}"&lt;/code&gt;, but when the key passed to &lt;code&gt;BasedOn&lt;/code&gt; matches the style's own key and also exists in a dictionary merged into the dictionary declaring the style, &lt;code&gt;BasedOn&lt;/code&gt; stays unresolved and the legacy look remains with no error to signal it.&lt;br&gt;
For migrating an existing application, set &lt;code&gt;ThemeMode&lt;/code&gt; on &lt;code&gt;Application&lt;/code&gt; and make the default structure a dedicated resource dictionary file holding the styles, merged from &lt;code&gt;App.xaml&lt;/code&gt;.&lt;br&gt;
Setting &lt;code&gt;ThemeMode&lt;/code&gt; per window creates placements where the style is never chained to Fluent, even when it lives in a separate file.&lt;br&gt;
Reference &lt;code&gt;DefaultButtonStyle&lt;/code&gt; only when the &lt;code&gt;App.xaml&lt;/code&gt; structure cannot be changed, and choose keyed styles with explicit application when the change should stay confined to part of the UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-fluent-design-with-systemcolors/" rel="noopener noreferrer"&gt;Applying Fluent Design in WPF Without Extra Libraries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-fluent-textbox-hide-clear-button/" rel="noopener noreferrer"&gt;Hiding the Clear Button on a Fluent-Themed WPF TextBox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/wpf-staticresource-vs-dynamicresource/" rel="noopener noreferrer"&gt;Why StaticResource Changes Are Not Reflected in WPF and How to Fix It&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/articles/csharp-natural-sort-strcmplogicalw-icomparer/" rel="noopener noreferrer"&gt;Replicating Windows Explorer Sort Order in C# with StrCmpLogicalW and IComparer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s-iguchi09.github.io/apps/wpf-standard-control-demo.html" rel="noopener noreferrer"&gt;WPF Standard Control Demo App (.NET 10)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>wpf</category>
      <category>dotnet</category>
      <category>csharp</category>
    </item>
  </channel>
</rss>
