MicrosoftDocs / windows-dev-docs

Conceptual and overview content for developing Windows apps
Creative Commons Attribution 4.0 International
690 stars 1.2k forks source link

UWP Inline Resource and Resource Dictionary Issue #2790

Open SetTrend opened 3 years ago

SetTrend commented 3 years ago

UWP documentation claims that styles can be applied to control types as a whole, by providing a TargetType attribute.

For instance:

  1. UWP - XAML styles
  2. ResourceDictionary and XAML resource references

However, that doesn't work.

I created two repositories to provide a reproducable project, demonstrating this issue with the UWP resource handling system:

  1. UWP-Inline-Resource-Issue
  2. UWP-Resource-Dictionary-Issue

To verify this issue, I posted two corresponding questions at StackOverflow:

  1. UWP - Why are resources not used in this page?
  2. UWP - How can I set page background color using ResoureDictionary?

The responses at StackOverflow make me believe that either the Microsoft documentation or the implementation of UWP is faulty.

Before I report this issue to the Microsoft UWP product team, I'll jump off by reporting this issue to the Microsoft UWP documentation team.


Document Details

Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.


Document Details

Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.

jwmsft commented 3 years ago

Hi, thanks for contacting us regarding these issues. Hopefully I can provide some clarification.

Why are resources not used in this page?

Implicit styles do work as intended to apply a style based on control type; the issue here is the scope of the Style resource. In order for this implicit style to work inside of the DataTemplate, the Style resource also needs to be inside of the DataTemplate, like this:

<ListView.ItemTemplate>
  <DataTemplate x:DataType="x:String">
    <Grid>
      <Grid.Resources>
        <Style TargetType="TextBlock">
          <Setter Property="Foreground" Value="Red" />
        </Style>
      </Grid.Resources>
      <TextBlock Text="{x:Bind}"/>
    </Grid>
  </DataTemplate>
</ListView.ItemTemplate>

How can I set page background color using ResourceDictionary?

When Visual Studio creates a page, it actually creates a new class that's derived from the Page class. This is seen most clearly in the declaration in the code page. For example, in MainPage.xaml.cs, you can see that the new class is MainPage and it's derived from Page.

public sealed partial class MainPage : Page

In UWP, implicit styles are not applied to derived classes, only to the type specified in the Style. To implicitly apply a Style to MainPage, the TargetType has to be MainPage, like this:

<Style TargetType="local:MainPage">
    <Setter Property="Background" Value="Red" />
</Style>

However, as you noticed, the Visual Studio preview handles this differently, and applies the style when TargetType is Page, but not when it's MainPage. I would guess that this is a bug in Visual Studio.

SetTrend commented 3 years ago

Thanks a lot, @jwmsft, for your informative reply!

I understood from the documentation that I can apply styles to a whole app in order to give it a unified skin, or, appearance.

So, taking your explanation into consideration, given I just wanted to have ...

  1. all Pages in my app to have a Red background
  2. all TextBlock content in my app, regardless of position, to have a Black foreground color

What should I do?

jwmsft commented 3 years ago

Hi, @SetTrend, the preferred way to change colors in your app is lightweight styling, which is overriding the system brushes that controls use by creating your own brush with the same key. The class documentation for most controls has a list of resources that you can override.

For the Page background, you override the ApplicationPageBackgroundThemeBrush resource:

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.ThemeDictionaries>
            <ResourceDictionary x:Key="Default">
                <SolidColorBrush x:Key="ApplicationPageBackgroundThemeBrush" Color="Red"/>
                <Style TargetType="TextBlock">
                    <Setter Property="Foreground" Value="Black"/>
                </Style>
            </ResourceDictionary>
        </ResourceDictionary.ThemeDictionaries>
    </ResourceDictionary>
</Application.Resources>

(Since it's a ThemeResource, it needs to be in a theme dictionary. I'm using a dictionary with the key "Default" to keep it simple, but it's better to use both "Light" and "Dark" resources as discussed in XAML theme resources.)

This also assumes that the page background is set to this resource, which it is by default in Visual Studio templates, like this: Background="{ThemeResource ApplicationPageBackgroundThemeBrush}".

TextBlock is different from most controls in that it doesn't have an editable template, so, unfortunately, it doesn't support lightweight styling. You have to use a Style to set the foreground. And, as we already know, a Page- or App-level implicit style won't be applied to TextBlocks inside a DataTemplate, so you have to use a separate implicit Style inside the DataTemplate or a Style with a key to change those TextBlocks . As far as I know, there isn't any way around this.

Hope that helps.

SetTrend commented 3 years ago

Thank you very much, Jim, for enlightening me! 👍

In the last couple of months I did a thorough research on UWP, and I see a number of issues regarding the resource system regarding brushes/colors. The UWP color system has a number of workarounds (RequestedTheme, ThemeResource etc.) to overcome its design flaws.

I believe it's a good time to revise and clean up the whole system. Not sure if I should open a corresponding discussion on GitHub in the UWP team space or rather ProjectReunion.

SetTrend commented 3 years ago

@jwmsft:

Today, I tried your suggestion, but it doesn't seem to work.

Although I created the lightweight styling attribute you suggested, the page still is displayed in black, not in green as it should have been:

screenshot

I created a corresponding repository for steps to reproduce.

jwmsft commented 3 years ago

@SetTrend, it looks like your MainPage.xaml Background is not set to the theme resource. Make sure this line is included in the opening Page tag, Background="{ThemeResource ApplicationPageBackgroundThemeBrush}", and it should work.

<Page
    x:Class="GreenStyling.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
</Page>
SetTrend commented 3 years ago

I'm confused ...

Following the documentation on lightweight styling, that's not "lightweight" styling.

If I'm required to explicitly reference a style from an object then that's "ordinary" styling.

Would you mind elaborating: Where in particular is the lightweight part in your suggestion?

I couldn't find anywhere in the documentation which of the styles provided by WinRT are utilized by any of the UWP components. Would you mind pointing me to a direction, so I'll be able to tell which WinRT styles to override if I want to change a style aspect application wide?

jwmsft commented 3 years ago

I agree this is confusing, and we need to improve the documentation for styles and themes.

First, we should be clear about the difference between a Style and a resource such as a color brush. As we discussed before, you can apply a Style implicitly by setting the TargetType. This is not true of resources - you must explicitly reference the resource.

With this line of code, Background="{ThemeResource ApplicationPageBackgroundThemeBrush}", you're not applying a Style, you're setting the Page's Background property to a value that is defined in a ThemeResource, which must be done explicitly. Again, Page is a special case because it doesn't have a default Style, so you have to set the property directly in the XAML for the page.

Most controls have a default Style where these references are set, so all you need to do is override the resource value and they are picked up automatically by the default Style. (You don't need to create an explicit reference to your resource definition because the reference already exists in the default Style.)

For example, the default Style for the Button control looks like this (this is just partial):

    <!-- Default style for Windows.UI.Xaml.Controls.Button -->
    <Style TargetType="Button">
        <Setter Property="Background" Value="{ThemeResource ButtonBackground}" />
        <Setter Property="BackgroundSizing" Value="OuterBorderEdge" />
        <Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
        ...
    />

    ...

Ordinary styling would be to create a new Style, set new values, and apply the new Style to a Button.

    <Style TargetType="Button" x:Key="newButtonStyle">
        <Setter Property="Background" Value="Green" />
        <Setter Property="Foreground" Value="White" /> 
    etc.
    ...
    </Style>

    <Button Style={StaticResource newButtonStyle}/>

(To see how much code this takes, add a Button to a page in Visual Studio, then right-click the Button on the design surface and select Edit Template > Edit a Copy. Before lightweight styling, this was the only way to change many of the control's colors.)

Lightweight styling (which should maybe be called themeing instead, because you're not changing the control's Style) is just re-defining the theme resources that are referenced by the control's Style.

I'm not sure if this answers your last question, but the default control styles and theme resources can be seen in the Generic.xaml file that's installed with the SDK. By default, that's in C:\Program Files (x86)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\10.0.19041.0\Generic.

Finally, and I should have thought of this earlier, the XAML team has created a tool that you can use to define color themes for your app: Fluent XAML Theme Editor. Hopefully, this tool will make it easier for you to apply the colors you want accross your app, and the GitHub page provides some more information about themeing and the color resources.

The tool also shows how lightweight styling lets you change colors accross your app without re-styling individual controls by letting you override the resources at different levels. For example, the ButtonBackground color is actually defined like this:

<StaticResource x:Key="ButtonBackground" ResourceKey="SystemControlBackgroundBaseLowBrush" />
<SolidColorBrush x:Key="SystemControlBackgroundBaseLowBrush" Color="{StaticResource SystemBaseLowColor}" />
<Color x:Key="SystemBaseLowColor">#33FFFFFF</Color>

Each color in a control Style has a resource key that references a system brush resource, which in turn references a theme Color resource. You can override the base resource (ButtonBackground) at the App, Page, or Control level. At the App level only, you can also override the system brush resource, which will change all resources that reference that brush; or override the theme color resource, which will change all the brushes that use that color accross controls. This is what you see in the output of the Fluent XAML Theme Editor.

SetTrend commented 3 years ago

Thank you, @jwmsft, for your continued support and your very comprehensive and elaborated reply 👍👍👍

I have read the referenced sources you mentioned. They have been very informative.

(Doing so I noticed that the <StaticResource> element used in generic.xaml and themeresources.xaml isn't documented.)

So, I updated my repository (SetTrend/UWP-LightWeight-Styling-Issue) to use a Style for a Page's background:

<Application.Resources>
  <ResourceDictionary>
    <ResourceDictionary.MergedDictionaries>
      <ResourceDictionary Source="Dictionary1.xaml"/>
    </ResourceDictionary.MergedDictionaries>
    <Style TargetType="Page">
      <Setter Property="Background" Value="{ThemeResource ApplicationPageBackgroundThemeBrush}" />
    </Style>
  </ResourceDictionary>
</Application.Resources>

So, why does Visual Studio 2019 adhere to my style, while the running app doesn't:

Styling issue

What's wrong here?

jwmsft commented 3 years ago

@SetTrend, you're very welcome. Regarding the green background working in Visual Studio, but not in the running app, refer back to my initial response to this issue, specifically the section "How can I set page background color using ResourceDictionary?". The TargetType for your style is Page, but it isn't applied to sub-classes of Page. I believe the fact that this is handled differently in the VS preview and the running app is a bug in VS. (Although it might be worth asking in the WinUI repo whether it's a bug in XAML.)

StaticResource is documented, but unfortunately, it's hidden away in a hard-to-find area of the docs: https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/staticresource-markup-extension. Hopefully we can do a better job of exposing this content soon. Thanks again for your time in providing this feedback to help us improve.

mdtauk commented 3 years ago

For those new to XAML, it could be useful to reference Xaml's Styling, Templates, and Resources with reference to CSS and SASS paradigms.

SetTrend commented 3 years ago

@jwmsft:

Yes, right. On Nov, 3rd, you wrote:

When Visual Studio creates a page, it actually creates a new class that's derived from the Page class. This is seen most clearly in the declaration in the code page. For example, in MainPage.xaml.cs, you can see that the new class is MainPage and it's derived from Page.

[On Nov, 4th, I then replied]():

So, taking your explanation into consideration, given I just wanted to have ...

  1. all Pages in my app to have a Red background [...]

What should I do?

So, on Nov, 5th, you replied:

For the Page background, you override the ApplicationPageBackgroundThemeBrush resource:

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.ThemeDictionaries>
            <ResourceDictionary x:Key="Default">
                <SolidColorBrush x:Key="ApplicationPageBackgroundThemeBrush" Color="Red"/>
                [...]
            </ResourceDictionary>
        </ResourceDictionary.ThemeDictionaries>
    </ResourceDictionary>
</Application.Resources>


That didn't work, so I replied on Nov, 12th:

Today, I tried your suggestion, but it doesn't seem to work.

Next, you replied on Nov, 12th:

@SetTrend, it looks like your MainPage.xaml Background is not set to the theme resource. Make sure this line is included in the opening Page tag, Background="{ThemeResource ApplicationPageBackgroundThemeBrush}", and it should work.

But that's not been the answer to my question. My original question was:

How can I assign the same background color to all pages without explicitly assigning a reference to that color to each of the pages manually?

So, on Nov, 13th I wrote:

I'm confused ...

On Nov, 13th you then replied:

With this line of code, Background="{ThemeResource ApplicationPageBackgroundThemeBrush}", you're not applying a Style, you're setting the Page's Background property to a value that is defined in a ThemeResource, which must be done explicitly. Again, Page is a special case because it doesn't have a default Style, so you have to set the property directly in the XAML for the page.

This was a truely enlightening detail 🚀 ... yet it didn't answer my question. From your reply, though, I concluded you were implying that applying a Style would be the answer.

So I updated my sample repository to utilize a Style referencing the ApplicationPageBackgroundThemeBrush property.

But, still, that didn' work.

I have the impression we're going around in circles ...

The quest still is the same: Is it possible to assign a themed style to all of an app's pages without referencing that style in each of the pages? Or is that just some weird request of mine that's not possible to accomplish at the moment?

jwmsft commented 3 years ago

Yes, I think we've come full circle, and I apologize if I've made it more complicated than needed. The short answer, then, is no, you can't set the page theme for all pages without referencing the style in each page.

However, (at the risk of complicating things again) if by style we mean only the Background, this is already set to ApplicationPageBackgroundThemeBrush in each page when you create it in Visual Studio, so the recommended way to change it is to leave that reference and override the ApplicationPageBackgroundThemeBrush resource in App.xaml. But the reference to the resource still needs to exist in each page.

SetTrend commented 3 years ago

But the reference to the resource still needs to exist in each page.

Aha..!*

OK, with all that valuable explanation of yours that makes sense.

So, this extraordinary Page behaviour is due to the fact that code-behind custom properties, methods and events get added to the final, derived app Page class, which makes a page particular from all the other framework components which are immutable (Page basically being immutable, too, that's why that derivation step becomes necessary at all - which I suppose to be technically realized by inclusion rather than inheritance, due to COM components' inability to be inheritable. Thus, an app's pages don't inherit styles assigned to the Page component, because they "are" no pages, according to OOP.).

So, as a final result of this thread (please correct me if I'm wrong):

  1. This behaviour is not an error in the UWP XAML framework
  2. This behaviour is a flaw in the Visual Studio designer
  3. This behaviour is an important piece of information missing in the documentation

Thus, as a round-up, I suppose the issue is correctly placed here in this present repository. And I propose the documentation to be amended by:

I now created a corresponding Visual Studio 2019 issue here.


* German: "I see".

jwmsft commented 3 years ago

That's a good summary of the issues; thank you for that. I'll open internal work items to track these doc updates.

groovykool commented 1 year ago

This issue also applies to GRID. Works in visual studio. Not in standalone app.,