Why can't I style a control with the Aero them

2019-01-16 05:33发布

问题:

I recently converted a project from WPF 3.5 to WPF 4.0. Functionally, everything works, but the DataGrid style I was applying on top of the Aero theme has suddenly stopped working. As you can see from the before/after pictures below, my DataGrids went from having an Aero look plus bold headings, extra padding, and alternating row formats to just looking plain "Aero". Besides removing all references to the WPF Toolkit (since the DataGrid is now native to WPF 4.0), I really didn't change anything about my code/markup.

Before (WPF Toolkit DataGrid)

After (.NET 4.0 DataGrid)

As I learned in an earlier question, I am able to get the custom DataGrid styling to work again if I stop referencing the Aero resource dictionary, but then everything looks "Luna" on Windows XP (which is not what I want).

So, how do I ensure that my app always uses the Aero theme, but still apply styling on top of that theme in WPF 4.0?

Here is my App.xaml code:

<Application
    x:Class="TempProj.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary
                    Source="/PresentationFramework.Aero,
                        Version=3.0.0.0,
                        Culture=neutral,
                        PublicKeyToken=31bf3856ad364e35,
                        ProcessorArchitecture=MSIL;component/themes/aero.normalcolor.xaml" />
                <ResourceDictionary Source="/CommonLibraryWpf;component/ResourceDictionaries/DataGridResourceDictionary.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

Here is my DataGridResourceDictionary.xaml code:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style x:Key="DataGrid_ColumnHeaderStyle" TargetType="DataGridColumnHeader">
        <Setter Property="FontWeight" Value="Bold" />
        <Setter Property="TextBlock.TextAlignment" Value="Center" />
        <Setter Property="TextBlock.TextWrapping" Value="WrapWithOverflow" />
    </Style>
    <Style x:Key="DataGrid_CellStyle" TargetType="DataGridCell">
        <Setter Property="Padding" Value="5,5,5,5" />
        <Setter Property="TextBlock.TextAlignment" Value="Center" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="DataGridCell">
                    <Border Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}">
                        <ContentPresenter />
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <Style TargetType="DataGrid">
        <Setter Property="ColumnHeaderStyle" Value="{StaticResource DataGrid_ColumnHeaderStyle}" />
        <Setter Property="CellStyle" Value="{StaticResource DataGrid_CellStyle}" />
        <Setter Property="Background" Value="White" />
        <Setter Property="AlternatingRowBackground" Value="#F0F0F0" />
        <Setter Property="VerticalGridLinesBrush" Value="LightGray" />
        <Setter Property="HeadersVisibility" Value="Column" />
        <Setter Property="SelectionMode" Value="Single" />
        <Setter Property="SelectionUnit" Value="FullRow" />
        <Setter Property="GridLinesVisibility" Value="Vertical" />
        <Setter Property="AutoGenerateColumns" Value="False" />
        <Setter Property="CanUserAddRows" Value="False" />
        <Setter Property="CanUserDeleteRows" Value="False" />
        <Setter Property="CanUserReorderColumns" Value="True" />
        <Setter Property="CanUserResizeColumns" Value="True" />
        <Setter Property="CanUserResizeRows" Value="False" />
        <Setter Property="CanUserSortColumns" Value="True" />
        <Setter Property="IsReadOnly" Value="True" />
        <Setter Property="BorderBrush" Value="#DDDDDD" />
        <Setter Property="HorizontalGridLinesBrush" Value="#DDDDDD" />
        <Setter Property="VerticalGridLinesBrush" Value="#DDDDDD" />
    </Style>
    <Style x:Key="DataGrid_FixedStyle" TargetType="DataGrid" BasedOn="{StaticResource {x:Type DataGrid}}">
        <Setter Property="CanUserReorderColumns" Value="False" />
        <Setter Property="CanUserResizeColumns" Value="False" />
        <Setter Property="CanUserResizeRows" Value="False" />
        <Setter Property="CanUserSortColumns" Value="False" />
    </Style>
</ResourceDictionary>

Here's a usage sample:

<DataGrid
    Grid.Row="0"
    Grid.Column="0"
    Style="{StaticResource DataGrid_FixedStyle}"
    ItemsSource="{Binding Coordinates}">
    <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding X}" Header="X" />
        <DataGridTextColumn Binding="{Binding Y}" Header="Y" />
        <DataGridTextColumn Binding="{Binding Z}" Header="Z" />
    </DataGrid.Columns>
</DataGrid>

Edit

It just occurred to me that maybe the problem is that I'm referencing the wrong version of the Aero framework.

Here's what I have now:

<ResourceDictionary
    Source="/PresentationFramework.Aero,
        Version=3.0.0.0,
        Culture=neutral,
        PublicKeyToken=31bf3856ad364e35,
        ProcessorArchitecture=MSIL;component/themes/aero.normalcolor.xaml" />

Should this be updated to version 4.0? What is the PublicKeyToken for version 4 (or how do I figure this out)?

回答1:

Relatively Short Answer

Loading a theme’s resources is not the same as changing the theme at the OS level. Loading a theme’s resources may cause adverse effects. From WPF’s point of view, a large number of implicit Styles are now present in the application. These Styles may trump other Styles. The bottom line is treating a theme like an application skin may not work without refinements.

There are some alternative ways of simulating a theme change.

  • This answer to a similar question lists some ideas.
  • The final point in this knowledge base article uses a small dose of reflection and must be used before the application is loaded.
  • This codeplex project uses a large dose of reflection and can be used at any time.

This problem showcases some fairly complex WPF functionality, and a portion of it appears to be undocumented. However, it does not appear to be a bug. If it’s not a bug - that is, if all of it is intentional WPF behavior - you might well argue the WPF DataGrid is poorly designed in a few areas.

Meleak’s answer was very much on the right track. However, the problem is solvable and it can be solved without compromising your design or requiring repetitive Style setting. And perhaps more importantly, the problem is debuggable.

The following XAML works. I left old XAML commented out just to make the changes more visible. For a more in-depth look at the problem, please see the long answer.

DataGridResourceDictionary.xaml:

<ResourceDictionary    
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"    
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <!--
    <Style x:Key="DataGrid_ColumnHeaderStyle" TargetType="DataGridColumnHeader">
    -->
    <Style TargetType="DataGridColumnHeader" BasedOn="{StaticResource {x:Type DataGridColumnHeader}}">

        <!--New-->
        <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
        <!---->

        <Setter Property="FontWeight" Value="Bold" />
        <Setter Property="TextBlock.TextAlignment" Value="Center" />
        <Setter Property="TextBlock.TextWrapping" Value="WrapWithOverflow" />
    </Style>

    <!--
    <Style x:Key="DataGrid_CellStyle" TargetType="DataGridCell">
    -->
    <Style TargetType="DataGridCell" BasedOn="{StaticResource {x:Type DataGridCell}}">
        <Setter Property="Padding" Value="5,5,5,5" />
        <Setter Property="TextBlock.TextAlignment" Value="Center" />
        <Setter Property="Template">
            <Setter.Value>
                <!--
                <ControlTemplate TargetType="DataGridCell">
                    <Border Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}">
                        <ContentPresenter />
                    </Border>
                </ControlTemplate>
                -->
                <ControlTemplate TargetType="{x:Type DataGridCell}">
                    <Border 
                        Padding="{TemplateBinding Padding}"
                        Background="{TemplateBinding Background}" 
                        BorderBrush="{TemplateBinding BorderBrush}"  
                        BorderThickness="{TemplateBinding BorderThickness}" 
                        SnapsToDevicePixels="True">
                        <ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>

        <!--Additional Feature-->
        <!--
            Remove keyboard focus cues on cells and tabbing on cells when
            only rows are selectable and the DataGrid is readonly.

            Note that having some kind of keyboard focus cue is
            typically desirable.  For example, the lack of any keyboard 
            focus cues could be confusing if an application has multiple
            controls and each control is showing something selected, yet
            there is no keyboard focus cue.  It's not necessarily obvious
            what would happen if Control+C or Tab is pressed.

            So, when only rows are selectable and the DataGrid is readonly,
            is would be ideal to make cells not focusable at all, make
            the entire row focusable, and make sure the row has a focus cue.
            It would take much more investigation to implement this.
        -->
        <Style.Triggers>
            <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                    <Condition Binding="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=SelectionUnit}" Value="FullRow"/>
                    <Condition Binding="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=IsReadOnly}" Value="True"/>
                </MultiDataTrigger.Conditions>
                <Setter Property="BorderBrush" Value="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Background}" />
                <Setter Property="FocusVisualStyle" Value="{x:Null}" />
                <Setter Property="IsTabStop" Value="False" />
            </MultiDataTrigger>
        </Style.Triggers>
        <!---->
    </Style>

    <!--
    <Style TargetType="DataGrid">
    --> 
    <Style TargetType="DataGrid" BasedOn="{StaticResource {x:Type DataGrid}}">

        <!--Unworkable Design-->
        <!--
        <Setter Property="ColumnHeaderStyle" Value="{StaticResource DataGrid_ColumnHeaderStyle}" />
        <Setter Property="CellStyle" Value="{StaticResource DataGrid_CellStyle}" />
        -->

        <Setter Property="Background" Value="White" />
        <Setter Property="AlternatingRowBackground" Value="#F0F0F0" />


        <!--This was a duplicate of the final PropertySetter.-->
        <!-- 
        <Setter Property="VerticalGridLinesBrush" Value="LightGray" />
        -->

        <Setter Property="HeadersVisibility" Value="Column" />
        <Setter Property="SelectionMode" Value="Single" />
        <Setter Property="SelectionUnit" Value="FullRow" />
        <Setter Property="GridLinesVisibility" Value="Vertical" />
        <Setter Property="AutoGenerateColumns" Value="False" />
        <Setter Property="CanUserAddRows" Value="False" />
        <Setter Property="CanUserDeleteRows" Value="False" />
        <Setter Property="CanUserReorderColumns" Value="True" />
        <Setter Property="CanUserResizeColumns" Value="True" />
        <Setter Property="CanUserResizeRows" Value="False" />
        <Setter Property="CanUserSortColumns" Value="True" />
        <Setter Property="IsReadOnly" Value="True" />
        <Setter Property="BorderBrush" Value="#DDDDDD" />
        <Setter Property="HorizontalGridLinesBrush" Value="#DDDDDD" />
        <Setter Property="VerticalGridLinesBrush" Value="#DDDDDD" />
    </Style>

    <Style x:Key="DataGrid_FixedStyle" TargetType="DataGrid" BasedOn="{StaticResource {x:Type DataGrid}}">
        <Setter Property="CanUserReorderColumns" Value="False" />
        <Setter Property="CanUserResizeColumns" Value="False" />
        <Setter Property="CanUserResizeRows" Value="False" />
        <Setter Property="CanUserSortColumns" Value="False" />
    </Style>
</ResourceDictionary>

App.xaml:

<Application    
    x:Class="TempProj.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"    
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <!--
                <ResourceDictionary                    
                    Source="/PresentationFramework.Aero,                        
                            Version=3.0.0.0,                     
                            Culture=neutral,                        
                            PublicKeyToken=31bf3856ad364e35,                        
                            ProcessorArchitecture=MSIL;component/themes/aero.normalcolor.xaml" />
                -->
                <ResourceDictionary                    
                    Source="/PresentationFramework.Aero,                        
                            Version=4.0.0.0,                     
                            Culture=neutral,                        
                            PublicKeyToken=31bf3856ad364e35,                        
                            ProcessorArchitecture=MSIL;component/themes/aero.normalcolor.xaml" />
                <!--New-->
                <!--
                    This is a modified replica of the DataGridRow Style in the Aero skin that's 
                    evaluated next.  We are hiding that Style and replacing it with this.
                -->
                <ResourceDictionary>
                    <Style x:Key="{x:Type DataGridRow}" TargetType="{x:Type DataGridRow}">
                        <!--
                            DataGridRow.Background must not be set in this application.  DataGridRow.Background
                            must only be set in the theme.  If it is set in the application, 
                            DataGrid.AlternatingRowBackground will not function properly.

                            See: https://stackoverflow.com/questions/4239714/why-cant-i-style-a-control-with-the-aero-theme-applied-in-wpf-4-0

                            The removal of this Setter is the only modification we have made.
                        -->
                        <!--
                        <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}" />
                        -->

                        <Setter Property="SnapsToDevicePixels" Value="true"/>
                        <Setter Property="Validation.ErrorTemplate" Value="{x:Null}" />
                        <Setter Property="ValidationErrorTemplate">
                            <Setter.Value>
                                <ControlTemplate>
                                    <TextBlock Margin="2,0,0,0" VerticalAlignment="Center" Foreground="Red" Text="!" />
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate TargetType="{x:Type DataGridRow}">
                                    <Border x:Name="DGR_Border"
                                            Background="{TemplateBinding Background}"
                                            BorderBrush="{TemplateBinding BorderBrush}"
                                            BorderThickness="{TemplateBinding BorderThickness}"
                                            SnapsToDevicePixels="True">
                                        <SelectiveScrollingGrid>
                                            <Grid.ColumnDefinitions>
                                                <ColumnDefinition Width="Auto"/>
                                                <ColumnDefinition Width="*"/>
                                            </Grid.ColumnDefinitions>

                                            <Grid.RowDefinitions>
                                                <RowDefinition Height="*"/>
                                                <RowDefinition Height="Auto"/>
                                            </Grid.RowDefinitions>

                                            <DataGridCellsPresenter Grid.Column="1"
                                             ItemsPanel="{TemplateBinding ItemsPanel}"
                                             SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>

                                            <DataGridDetailsPresenter  SelectiveScrollingGrid.SelectiveScrollingOrientation="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path=AreRowDetailsFrozen, Converter={x:Static DataGrid.RowDetailsScrollingConverter}, ConverterParameter={x:Static SelectiveScrollingOrientation.Vertical}}"
                                                Grid.Column="1" Grid.Row="1"
                                                Visibility="{TemplateBinding DetailsVisibility}" />

                                            <DataGridRowHeader SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical"  Grid.RowSpan="2"
                                                Visibility="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path=HeadersVisibility, Converter={x:Static DataGrid.HeadersVisibilityConverter}, ConverterParameter={x:Static DataGridHeadersVisibility.Row}}"/>
                                        </SelectiveScrollingGrid>
                                    </Border>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </ResourceDictionary>
                <!---->

                <ResourceDictionary Source="/CommonLibraryWpf;component/ResourceDictionaries/DataGridResourceDictionary.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

MainWindow.xaml:

<Window 
    x:Class="TempProj.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <Vector3DCollection x:Key="Coordinates">
            <Vector3D X="1" Y="0" Z="0"/>
            <Vector3D X="0" Y="22" Z="0"/>
            <Vector3D X="0" Y="0" Z="333"/>
            <Vector3D X="0" Y="4444" Z="0"/>
            <Vector3D X="55555" Y="0" Z="0"/>
        </Vector3DCollection>
    </Window.Resources>
    <Grid>
        <DataGrid
            Grid.Row="0"    
            Grid.Column="0"    
            Style="{StaticResource DataGrid_FixedStyle}"
            ItemsSource="{StaticResource Coordinates}">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding X}" Header="X" />
                <DataGridTextColumn Binding="{Binding Y}" Header="Y" />
                <DataGridTextColumn Binding="{Binding Z}" Header="Z" />
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>


回答2:

Long Answer

The previous short answer provides some XAML to remedy the problem as well as a brief summary of what's causing it.

Loading a theme’s resources is not the same as changing the theme at the OS level. Loading a theme’s resources may cause adverse effects. From WPF’s point of view, a large number of implicit Styles are now present in the application. These Styles may trump other Styles. The bottom line is treating a theme like an application skin may not work without refinements.

The following long answer will provide a more in-depth discussion of the problem. A few background topics will be covered first. This will answer some of the peripheral questions asked and it will also provide a better basis for understanding the issues at hand. After that, individual aspects of the problem will be dissected and addressed with an effective debugging strategy.

Theme Versus Skin

This was great question partly because hundreds of bloggers and forum threads recommend loading a theme from file as a way of “changing your theme”. Some of the authors making this recommendation work at Microsoft and many of the authors are obviously high caliber software engineers. This approach appears to work much of the time. However, as you noticed, this approach didn’t exactly work in your scenario and required a number of refinements.

Some of this problem stems from imprecise terminology. Unfortunately, the word theme has become hopelessly overloaded. A precise definition for theme that would avoid confusion is simply the system theme. A system theme defines the default appearance of Win32 visuals on the machine. My OS is Vista. My installed themes are located at C:\WINDOWS\Resources\Themes. In that folder are two files: aero.theme and Windows Classic.theme. If I want to change the theme I go to either [Personalize | Theme] or [Personalize | Window Color and Appearance | Color scheme]. Although it's not immediately obvious, the options I can select from all boil down to either Aero or Classic plus some additional refinements. Because a WPF window renders its client area rather than compositing a bunch of Win32 controls, the client area will not automatically respect the theme. The theme assemblies (e.g. PresentationFramework.Aero.dll) provide a basis for extending theme functionality into WPF windows.

The more general definition of theme is any look and feel configuration, at any level of granularity (OS, Application, Control). When people use the general definition, there is the potential for various degrees of confusion. Note that MSDN switches between the precise definition and the general definition without warning!

Many people would say you were loading an application skin, not a theme. Either word is arguably correct, but I would recommend this mental model simply because it causes less confusion.

So, how do I ensure that my app always uses the Aero theme…? [emphasis added]

Again, it could be said you are loading Aero’s resources as a skin. Specifically, you’re loading the ResourceDictionary located inside PresentationFramework.Aero.dll. Those resources were previously given special treatment as they were default resources. However, once inside the application, they will be treated like any other arbitrary collection of resources. Of course, Aero’s ResourceDictionary is comprehensive. Since it will be loaded at the application scope, it will effectively hide every default Style provided by the theme (Luna, in your case), plus a few other Styles, which is causing the problem. Note that ultimately, the theme is still the same (Luna).

As alluded to above, the theme is involved in Style precedence, which is itself a form of Dependency Property precedence. These precedence rules greatly demystify the observed behavior in the problem.

Explicit style. The Style property is set directly. In most scenarios, the style is not defined inline, but instead is referenced as a resource, by explicit key…

Implicit style. The Style property is not set directly. However, the Style exists at some level in the resource lookup sequence (page, application) and is keyed using a resource key that matches the type the style is to be applied to…

Default style, also known as theme style. The Style property is not set directly, and in fact will read as null... In this case, the style comes from the run-time theme evaluation that is part of the WPF presentation engine.

This blog entry takes a much deeper look at style versus default style.

.NET Assembly Inspection

This was also a great question partly because there are so many moving parts. Without an effective debugging strategy it will be almost impossible to understand what’s going on. With that in mind, .NET assembly inspection is a natural place to start.

From WPF’s point of view, a theme is essentially a ResourceDictionary serialized as BAML and embedded in a regular .NET assembly (e.g. PresentationFramework.Aero.dll). Later on, it will be necessary to view the themes as plain XAML to verify behavior in the problem.

Fortunately, Microsoft provides the 4.0 themes as XAML for developers’ convenience. I’m not sure if pre-4.0 themes are downloadable in any form from Microsoft.

For general assemblies (including pre-4.0 theme assemblies), you can use the (previously free) tool Reflector with the BamlViewer plugin to decompile the BAML back into XAML. Although not as flashy, ILSpy is a free alternative with a built in BAML decompiler.

.NET assemblies are littered throughout your hard drive and it’s kind of confusing. Here are their paths on my machine, which I sort of have a gut feeling for and sometimes manage to remember without trial and error.

Aero 3.0

C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\PresentationFramework.Aero.dll

Aero 4.0

C:\WINDOWS\Microsoft.NET\assembly\GAC_MSIL\PresentationFramework.Aero\v4.0_4.0.0.0__31bf3856ad364e35\PresentationFramework.Aero.dll

What is the PublicKeyToken for version 4 (or how do I figure this out)?

The easiest thing to do is use Reflector. The PublicKeyToken is the same as before: 31bf3856ad364e35

Additionally, sn.exe (from the Windows SDK) can extract assembly information.

On my machine, the command is:

C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin>sn.exe -Tp "C:\WINDOWS\Microsoft.NET\assembly\GAC_MSIL\PresentationFramework.Aero\v4.0_4.0.0.0__31bf3856ad364e35\PresentationFramework.Aero.dll"

Loading Themes as Skins

Should (the PresentationFramework.Aero reference) be updated to version 4.0?

Most definitely. The DataGrid did not exist in the .NET FCL prior to 4.0. There are several ways to confirm this, but the most intuitive one is that, by your own admission, you previously accessed it via the WPF Toolkit. If you choose to not load PresentationFramework.Aero 4.0 in App.xaml, Aero’s DataGrid Style won’t be in the application resources.

Now, it turns out it doesn’t even matter. I’ll use the original XAML, break in the debugger when loading, and inspect the application-scoped resources.

As expected, there are two ResourceDictionaries in the application’s MergedDictionaries property, and the first ResourceDictionary is allegedly the 3.0 version of PresentationFramework.Aero. However, I see there are 266 resources in the first ResourceDictionary. At this point, it just so happens I know there are 266 resources in the Aero 4.0 theme, and only 243 resources in the Aero 3.0 theme. Moreover, there’s even a DataGrid entry! This ResourceDictionary is, in fact, the Aero 4.0 ResourceDictionary.

Perhaps someone else can explain why WPF is loading the 4.0 assembly when 3.0 was explicitly specified. What I can tell you is if the projects are retargeted to .NET 3.0 (and the compile errors are fixed), the 3.0 version of Aero will be loaded instead.

As you correctly deduced, Aero 4.0 should be loaded anyway. It’s just useful to know what’s going on while debugging this.

Problem #1: Aero’s DataGrid Style is not Being Used

The DataGrid in this application will have zero or more Styles chained together depending on how you configure Style.BasedOn properties.

It will also have a default Style, which, in your case, is embedded in the Luna theme.

I knew just by looking at the original XAML there was a Style inheritance issue. The big DataGrid Style with ~20 Setters does not set its BasedOn property.

You have a Style chain of length two and your default Style is from the Luna theme. The DataGrid Style in Aero’s ResourceDictionary is simply not being used.

There are two big questions here. First, how can something like this be debugged in the first place? Second, what are the implications?

Debugging Style Chains

I would recommend using Snoop and/or WPF Inspector to debug WPF issues like this.

Version 0.9.9 of WPF Inspector even has a Style chain viewer. (I must warn you that this feature is currently buggy and not very useful for debugging this part of the application. Also note that it chooses to depict the default Style as part of the chain.)

The power of these tools is their ability to view and edit values of deeply nested elements at run-time. You can simply mouse over an element and its information will immediately surface in the tool.

Alternatively, if you simply want to look at a top-level element like the DataGrid, name the element in the XAML (e.g. x:Name="dg"), then break in the debugger when loading and put the element’s name in a Watch window. There you can inspect the Style chain via the BasedOn property.

Below, I’ve broken in the debugger while using the solution XAML. The DataGrid has three Styles in the Style chain with 4, 17, and 9 Setters respectively. I can drill a little deeper and deduce the first Style is “DataGrid_FixedStyle”. As expected, the second is the large, implicit DataGrid Style from the same file. Finally, the third Style appears to be from Aero’s ResourceDictionary. Note that the default Style is not represented in this chain.

At this point it should be noted that there is actually no variation between each theme's DataGrid Style. You can verify this by taking the DataGrid Styles from their respective 4.0 themes, copying them into separate text files, and then comparing them with a diff tool.

In fact, a moderate number of Styles are simply identical from theme to theme. This is good to be aware of. To verify this, simply run a diff on the entire XAML held in two different themes.

Note that there are many different elements nested within the DataGrid (e.g. DataGridRow) and each has its own Style. Even though DataGrid Styles are currently identical from theme to theme, the Styles for these nested elements could vary. Based on observed behavior in the problem, it's clear that some do.  

Implications of Original XAML not Incorporating Aero’s DataGrid Style

Since DataGrid Styles are identical across the 4.0 themes, adding Aero’s DataGrid Style to the end of the Style chain is, in this case, basically superfluous. Aero’s DataGrid Style will be the same as the default DataGrid Style (from Luna, in your case). Of course, future themes could always have variations with respect to the DataGrid Style.

Regardless of whether there are any implications, since you intended to incorporate the Styles from Aero, it’s clearly more correct to do so until there’s a specific reason to not do so (which will be discussed later).

Most importantly, it's just useful to know what's going on.

Style.BasedOn Only Has Meaning within the Context in which it’s Used

In the solution XAML, DataGridResourceDictionary.xaml works exactly the way you want it to work. It’s important to understand why, and it’s important to understand that using it this way precludes using it in other ways.

Let’s say the final Styles in DataGridResourceDictionary.xaml’s Style chains set their BasedOn properties to a Type key (e.g. BasedOn="{StaticResource {x:Type DataGrid}}"). If they do that, then they will inherit from an implicit Style that matches this key. However, the Style that they inherit from depends on where DataGridResourceDictionary.xaml is loaded. If, for example, DataGridResourceDictionary.xaml is loaded into a merged dictionary right after Aero’s resources are loaded, then its Styles will inherit from the appropriate Aero Styles. Now, if, for example, DataGridResourceDictionary.xaml is the only ResourceDictionary loaded in the entire application, its Styles will actually inherit from the relevant Styles in the current theme (Luna, in your case). Note that the theme’s Styles will of course also be the default Styles!

Now let’s say the final Styles in DataGridResourceDictionary.xaml’s Style chains do not set their BasedOn properties. If they do that, then they will be the final Styles in their respective Style chains, and the only other Styles evaluated will be the default Styles (always located in the theme). Note that this would kill your intended design of loading Aero as a skin and selectively refining parts of it.

Note that in the previous examples, if the final key was a string (e.g. x:Key="MyStringKey") instead of a Type, the same sorts of things would happen, but there would not be any matching Styles in the themes or in the Aero skin. An Exception would be thrown at load time. That said, dangling string keys could theoretically work if a context always existed in which a matching Style was found.

In the solution XAML, DataGridResourceDictionary.xaml has been modified. Styles at the end of each Style chain now inherit from an additional, implicit Style. When loaded in App.xaml, these will resolve to Aero Styles.

Problem #2: DataGrid.ColumnHeaderStyle and DataGrid.CellStyle

This is a nasty problem and it’s responsible for some of the strange behavior you saw. DataGrid.ColumnHeaderStyle and DataGrid.CellStyle get trumped by implicit DataGridColumnHeader and DataGridCell Styles. That is to say, they are incompatible with the Aero skin. Thus, they are simply removed from the solution XAML.

The rest of this subsection is a thorough investigation of the problem. DataGridColumnHeader and DataGridCell, like all FrameworkElements, have a Style property. In addition, there are a couple of very similar properties on DataGrid: ColumnHeaderStyle and CellStyle. You could call those two properties “helper properties”. They map, at least conceptually, to DataGridColumnHeader.Style and DataGridCell.Style. How they actually get used though is undocumented, so we must dig deeper.

The properties DataGridColumnHeader.Style and DataGridCell.Style use value coercion. That means that when either Style is queried, special callbacks are used to determine what Style is actually returned to the caller (internal WPF code, for the most part). These callbacks can return any value they want. Ultimately, DataGrid.ColumnHeaderStyle and DataGrid.CellStyle are candidate return values in the respective callbacks.

With Reflector I can easily determine all of this. (If necessary, it’s also possible to step through .NET source code.) Starting in DataGridColumnHeader's static constructor, I locate the Style property and see it is being assigned additional metadata. Specifically, a coercion callback is specified. Beginning with that callback, I click through a sequence of method calls and quickly see what’s going on. (Note that DataGridCell does the same thing so I won't cover it.)

The final method, DataGridHelper.GetCoercedTransferPropertyValue, essentially compares the source of DataGridColumnHeader.Style and DataGrid.ColumnHeaderStyle. Whichever source has higher precedence wins. The precedence rules in this method are based on Dependency Property predecence.

At this point, DataGrid.ColumnHeaderStyle will be inspected in both the original XAML and the solution XAML. A small matrix of information will be collected. Ultimately, this will explain the observed behavior in each application.

In the original XAML, I break in the debugger and see that DataGrid.ColumnHeaderStyle has a ‘Style’ source. This makes sense as it was set inside a Style.

In the solution XAML, I break in the debugger and see that DataGrid.ColumnHeaderStyle has a ‘Default’ source. This makes sense as that value was not set in a Style (or anywhere else).

The other value to inspect is DataGridColumnHeader.Style. DataGridColumnHeader is a deeply nested element that isn’t conveniently accessible when debugging in VisualStudio. Realistically, a tool like Snoop or WPF Inspector would be used to inspect the property.

With the original XAML, DataGridColumnHeader.Style has an ‘ImplicitStyleReference’ source. This makes sense. DataGridColumnHeaders are instantiated deep down in internal WPF code. Their Style property is null and so they will look for an implicit Style. The tree is traversed from the DataGridColumnHeader element to the root element. As expected, no Styles are found. Then the application resources are checked. You have a string key ("DataGrid_ColumnHeaderStyle") set on the lone DataGridColumnHeader Style. This effectively hides it in this lookup and so it isn’t used. Then, the Aero skin is searched and a typical implicit Style is found. This is the Style that is used.

If this step is repeated with the solution XAML, the result is the same: 'ImplicitStyleReference'. This time, however, the implicit Style is the lone DataGridColumnHeader Style in DataGridResourceDictionary.xaml, now implicitly keyed.

Finally, if this step is repeated once again with the original XAML, and the Aero skin is not loaded, the result is now ‘Default’! This is because there are simply no implicit DataGridColumnHeader Styles in the entire application.

Therefore, DataGrid.ColumnHeaderStyle will be used if the Aero skin is not loaded, but will not be used if the Aero skin is loaded! As advertised, loading a theme's resources may cause adverse effects.

It’s a lot to keep straight and the names all sound the same. The following diagram recaps all the action. Remember, the property with the higher precedence wins.

It may not be what you want, but this is how the DataGrid works as of WPF 4.0. Taking this into account, you could theoretically set DataGrid.ColumnHeaderStyle and DataGrid.CellStyle at a very broad scope, and still have the ability to override the DataGridColumnHeader and DataGridCell Styles at a more narrow scope using implicit Styles.

Again, DataGrid.ColumnHeaderStyle and DataGrid.CellStyle get trumped by implicit DataGridColumnHeader and DataGridCell Styles. That is to say, they are incompatible with the Aero skin. Thus, they are simply removed from the solution XAML.

Problem #3: DataGridRow.Background

If the recommended changes up to this point have been implemented, something resembling the following should be on your screen. (Keep in mind I set my theme to Classic in order to debug this problem.)

The DataGrid has an Aero look, but AlternatingRowBackground isn’t being respected. Every other row ought to have a gray background.

Using the debugging techniques discussed up to now, it will be found that this is the exact same type of problem as Problem #2. An implicit DataGridRow Style inside the Aero skin is now being loaded. DataGridRow.Background uses property coercion. DataGrid.AlternatingRowBackground is a candidate value that may be returned in the coercion callback. DataGridRow.Background is another candidate. Where these values originate from will influence which value the coercion callback chooses.

By now it should be clear, but if not, it must be reiterated. Loading a theme's resources may cause adverse effects.

The short answer to this sub-problem is DataGridRow.Background must only be set in the theme. Specifically, it must not be set by a Style Setter anywhere in the application. Unfortunately, that’s now precisely what’s happening in the Aero skin. There are at least two ways to address this problem.

A blank implicit Style can be added after the Aero skin. This hides the offending Style in Aero. There are no values in the blank Style so values from the default Style end up being used. In the end, this only works because DataGridRow Styles are identical in every 4.0 theme.

Alternatively, Aero’s DataGridRow Style can be copied, the Background Setter can be removed, and the remainder of the Style can be added after the Aero skin. The solution XAML employs this technique. By expanding the Style out, the application is more likely to continue looking Aero in future scenarios. By isolating this expansion in App.xaml, DataGridResourceDictionary.xaml can be used more freely in other contexts. However, note that it may make more sense to add it to DataGridResourceDictionary.xaml, depending on how that file is used in the future. In terms of this application, either way works.

Problem #4: DataGridColumnHeader Layout

The final change is fairly superficial. If the application is run after making the recommended changes so far, the DataGridColumnHeaders will have content that is left-aligned rather than centered. This problem can be drilled into easily with Snoop or WPF Inspector. The root of the problem appears to be the DataGridColumnHeaders have HorizontalContentAlignment set to ‘Left’.

Set it to ‘Stretch’ and it works as expected.

There’s some interplay between the Layout properties and TextBlock formatting properties. Snoop and WPF Inspector allow for experimentation and make it easy to determine what works in any given situation.

Final Thoughts

To summarize, loading a theme’s resources is not the same as changing the theme at the OS level. Loading a theme’s resources may cause adverse effects. From WPF’s point of view, a large number of implicit Styles are now present in the application. These Styles may trump other Styles. The bottom line is treating a theme like an application skin may not work without refinements.

That said, I’m not completely sold on the current WPF implementation with respect to "helper properties" (e.g. DataGrid.ColumnHeaderStyle) being used via a coercion callback with precedence rules. I have to wonder why they can’t just be locally assigned to their intended targets (e.g. DataGridColumnHeader.Style) at initialization time, if the targets do not already have an explicitly assigned value. I haven't thought enough about this to know what the various problems might be, but if it's possible, it might make the "helper property" model more intuitive, more consistent with other properties, and more foolproof.

Finally, although it wasn’t the focus of this answer, it’s very important to note that loading a theme’s resources to simulate changing the theme is particularly bad because there is a substantial maintainability cost. Existing Styles in the application will not automatically be based on the Styles inside the theme’s ResourceDictionary. Each Style in the application would have to set its BasedOn property to a Type key (or be based, directly or indirectly, on another Style which does). This is extremely burdensome and error-prone. Additionally, there is a maintainability cost with respect to theme-aware custom controls. The theme resources for these custom controls would also have to be loaded to effect this simulation. And of course, after doing that, you might face Style precedence issues similar to the ones you faced here!

Anyway, there’s more than one way to skin a WPF app (no pun intended!). I hope this answer provides additional insight into your problem, and helps you and others solve similar problems.



回答3:

I think the problem isn't PresentationFramework.Aero by itself, but rather that you get implicit DataGrid Styles by including it. This can also be seen by just adding this in App.xaml

<Application.Resources>
    <Style TargetType="{x:Type DataGridColumnHeader}"/>
</Application.Resources>

This will cause all your DataGridColumnHeader's to lose their style if they're not set explicitly.

This will work

<DataGrid ColumnHeaderStyle="{StaticResource DataGrid_ColumnHeaderStyle}" ../>

However, this won't

<DataGrid Style="{StaticResource DataGrid_FixedStyle}" ../>

<Style x:Key="DataGrid_FixedStyle" TargetType="DataGrid">
    <Setter Property="ColumnHeaderStyle"
            Value="{StaticResource DataGrid_ColumnHeaderStyle}" />
</Style>

I'm not sure of a good way around this. The only thing I can think of is to set all the styles explicitly on the DataGrid itself but that might be inconvenient, especially if you're using this style in many places.

<DataGrid Style="{StaticResource DataGrid_FixedStyle}"
          ColumnHeaderStyle="{StaticResource DataGrid_ColumnHeaderStyle}"
          CellStyle="{StaticResource DataGrid_CellStyle}"
          ... >