Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

25 Oct 2020

再读[ResourceDictionary and XAML resource references]

XAML的资源一般指的是那些可以被共享的字符串,样式、模板、笔刷、动画等等。资源的主要组织方式是ResourceDictionary。把资源放在ResourceDictionary内,然后设置一个x:Key,就可以通过StaticResource或者ThemeResource这些XAML标签扩展来引用。ResourceDictionary又可以进一步分为MergedDictionaries,用以整合其他XAML文件中定义的ResourceDictionary;以及ThemeDictionaries,用于整合和主题相关的资源。

XAML的资源跟Visual Studio支持的.resw资源文件不是一回事儿。

资源必须是可以被共享的对象,像控件(controls)、形状(shapes)以及其他一些不能被共享的FrameworkElements,是不能放在ResouceDictionary中被复用的。

ResourceDictionary里面的资源必须有键值,但是有一些特殊情况:

  • Style和ControlTemplate需要指定一个TargetType。如果x:Key没有指定的情况下,这个TargetType的对象会被作为键值使用。 DataTempplate也是如此。
  • x:Name可以用来替代x:Key。但是x:Name会在XAML对应的后部代码中生成一个相应的字段。这个字段在加载的时候需要初始化,会导致额外的开销。

StaticResource标签扩展会通过x:Key或者x:Name来找到指定名字的资源。但是呢,如果元素上没有设置Style或者ContentTemplate或者ItemTemplate,XAML框架也会去查找隐性的样式和模板资源(也就是只指定了TargetType的资源)。

下面例子中,Page.Resources中定义的Style的键值默认为typeof(Button),所以会被接下来定义的Button所使用。

<Page
    x:Class="MSDNSample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Page.Resources>
        <Style TargetType="Button">
            <Setter Property="Background" Value="Red"/>
        </Style>
    </Page.Resources>
    <Grid>
       <!-- This button will have a red background. -->
       <Button Content="Button" Height="100" VerticalAlignment="Center" Width="100"/>
    </Grid>
</Page>

可以在后部代码中通过代码的形式查找资源,可是和StaticeResource标记扩展不同,代码形式的查找不会在查完Page.Resources之后又去查找Application.Resources.

下面是一个例子:

<Page
    x:Class="MSDNSample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Page.Resources>
        <Style TargetType="Button" x:Key="redButtonStyle">
            <Setter Property="Background" Value="red"/>
        </Style>
    </Page.Resources>
</Page>

<Application
    x:Class="MSDNSample.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:SpiderMSDN">
    <Application.Resources>
        <Style TargetType="Button" x:Key="appButtonStyle">
            <Setter Property="Background" Value="red"/>
        </Style>
    </Application.Resources>

</Application>
// 只查Page.Resources
MainPage::MainPage()
    {
        InitializeComponent();
        Windows::UI::Xaml::Style style = Resources().TryLookup(winrt::box_value(L"redButtonStyle")).as<Windows::UI::Xaml::Style>();
    }
// 只查Application.Resources
MainPage::MainPage()
    {
        InitializeComponent();
        Windows::UI::Xaml::Style style = Application::Current().Resources()
                                                               .TryLookup(winrt::box_value(L"appButtonStyle"))
                                                               .as<Windows::UI::Xaml::Style>();
    }

对于Application.Resources,你可以在代码向其添加样式,但是有两点需要注意:

  • 必须在样式被使用前添加
  • 不能再App的构造函数中添加

所以,一个好的时间点是在Application.OnLaunched的时候添加:

// App.cpp

void App::OnLaunched(LaunchActivatedEventArgs const& e)
{
    Frame rootFrame{ nullptr };
    auto content = Window::Current().Content();
    if (content)
    {
        rootFrame = content.try_as<Frame>();
    }

    // Do not repeat app initialization when the Window already has content,
    // just ensure that the window is active
    if (rootFrame == nullptr)
    {
        Windows::UI::Xaml::Media::SolidColorBrush brush{ Windows::UI::ColorHelper::FromArgb(255, 0, 255, 0) };
        Resources().Insert(winrt::box_value(L"brush"), winrt::box_value(brush));
        // … Other code that VS generates for you …

值得注意的是,每个FrameworkElement都有Resources下属,可以往里面添加资源。

<Page
    x:Class="MSDNSample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Page.Resources>
        <x:String x:Key="greeting">Hello world</x:String>
    </Page.Resources>
    
    <StackPanel>
        <!-- Displays "Hello world" -->
        <TextBlock x:Name="textBlock1" Text="{StaticResource greeting}"/>

        <Border x:Name="border">
            <Border.Resources>
                <x:String x:Key="greeting">Hola mundo</x:String>
            </Border.Resources>
            <!-- Displays "Hola mundo" -->
            <TextBlock x:Name="textBlock2" Text="{StaticResource greeting}"/>
        </Border>

        <!-- Displays "Hola mundo", set in code. -->
        <TextBlock x:Name="textBlock3"/>
    </StackPanel>
</Page>

通过ResourceDictionary.MergedDictionaries,引用其他XAML文件中的ResourceDictionary,从而增加了ResourceDictionary的可组织性。

<!-- Dictionary1.xaml -->
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:MSDNSample">

    <SolidColorBrush x:Key="brush" Color="Red"/>

</ResourceDictionary>
<Page
    x:Class="MSDNSample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Page.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Dictionary1.xaml"/>
            </ResourceDictionary.MergedDictionaries>

            <x:String x:Key="greeting">Hello world</x:String>

        </ResourceDictionary>
    </Page.Resources>

    <TextBlock Foreground="{StaticResource brush}" Text="{StaticResource greeting}" VerticalAlignment="Center"/>
</Page>

事实上,当往Page.Resources添加资源的时候,XAML框架会隐式地帮你定义一个ResourceDictionary对象。而上面地例子中ResourceDictionary对象则是显示定义的。可以看到,在ResourceDictionary.MergedDictionaries之后,依然可以指定额外的资源。

从资源查找的角度,MergedDictionarie中的资源的优先级排在ResourceDictionary中定义的资源之后。如果有多个MergedDictionarie,那么查找顺序和它们在ResourceDictionary中出现的顺序相反。在下面的例子中,Dictionary2.xaml中的资源会优先于Dictionary1.xaml中的资源:

<Page
    x:Class="MSDNSample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Page.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Dictionary1.xaml"/>
                <ResourceDictionary Source="Dictionary2.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Page.Resources>

    <TextBlock Foreground="{StaticResource brush}" Text="greetings!" VerticalAlignment="Center"/>
</Page>

同一个ResourceDictionary指定的资源的键值必须是不一样的。但是这个要求不会引申到ResourceDictionary所引用的MergedDictionaries中。

除了StaticResource外,ResourceDictionary里面还有ThemeResouce,定义在ThemeDictionaries中。后者作为主题资源,可以随主题变化而改变。ThemeDictionaries中的资源必须指定x:Key的值为Default、Dark、Light或者HighContrast。

下面是一个例子:

<!-- Dictionary1.xaml -->
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:MSDNSample">

    <SolidColorBrush x:Key="brush" Color="Red"/>

</ResourceDictionary>

<!-- Dictionary2.xaml -->
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:MSDNSample">

    <SolidColorBrush x:Key="brush" Color="blue"/>

</ResourceDictionary>

<Page
    x:Class="MSDNSample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Page.Resources>
        <ResourceDictionary>
            <ResourceDictionary.ThemeDictionaries>
                <ResourceDictionary Source="Dictionary1.xaml" x:Key="Light"/>
                <ResourceDictionary Source="Dictionary2.xaml" x:Key="Dark"/>
            </ResourceDictionary.ThemeDictionaries>
        </ResourceDictionary>
    </Page.Resources>
    <TextBlock Foreground="{StaticResource brush}" Text="hello world" VerticalAlignment="Center"/>
</Page>

可以查看\(Program Files)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\<SDK version>\Generic.xaml来得知默认的主题定义是什么样子的。和Generic.xaml同目录的还有一个 themeresources.xaml文件,内容来自于Generic.xaml,但是不包含default control templates。在Visual Studio中如果拷贝某个元素的控件或者模板,那么这些拷贝的来源就是来自于上面的上述的xaml文件。

XAML框架在查找资源的时候,会沿着对象树往上,对于路径上的每个对象,都会去看看它的Resources下属是否存在,以及是否包含想要的资源。找到目标资源,则停止查找,否则就会一直往上,直到触达树的根节点。如果没有在对象树中找到目标资源,下一步是查找Application.Resources。最后一步是查找Platform资源(其中定义了所有控件的默认样式和模板)。从技术上讲,Platform资源是作为一个MergedDictionaries被引用的。

如果上述所有的查找都失败了,那么会抛出XAML parsing error/exception。通常情况下在XAML编译的时候能够发现这些错误,但是有些时候只有在运行时才能发现。

XAML中的资源必须先定义再使用。所以App级别的资源无法引用Page级别的资源。这种前向引用(Forward Reference)是不支持的。在组织XAML资源的时候必须考虑到这一点。

然后XAML中的资源必须是可共享的。XAML资源会被对象树中不同的的节点引用。下列资源是可共享的:

  • 样式和模板
  • 笔刷和颜色
  • 动画以及Storyboard
  • 变换(GeneralTransform)
  • Matrix以及Matrix3D
  • Point
  • 一些UI相关的结构,比如Thickness和CornerRadius
  • XAML intrinsic 数据类型(x:Boolean、x:String、x:Double、x:Int32等等)

自定义的对象也可以作为资源,比如对IValueConverter的实现。自定义的对象必须有一个默认构造函数,并且不能是UIElement的派生类型。(UIElement被设计为不可共享)。

对于UserControl,有定义范围(definition scope)和使用范围(usage scope)的区分。定义范围不能访问app级别资源;而使用范围可以访问app级别资源。

如果使用XamlReader.Load来加载资源,那么需要注意的是,加载的时候不考虑其他ResourceDictionary,甚至不考虑Application.Resources,也不考虑{ThemeResource}。

在代码中访问XAML资源的时候,查找的范围不会从临近资源跨越到App资源,这一点和XAML的行为是不一致的。但是MergedDictionaries所引用的资源是可以被查找到的,这一点和XAML的行为是一致的。可以在代码中往ResourceDictionary添加资源,但是这是发生在XAML加载之后,所以添加的资源不会影响已经使用了的资源。

XAML的ResourceDictionary可能一开始会用到需要本地化的字符串。随着项目进展,需要把这些字符串移到项目中,设定其XUIDValue.PropertyName。然后再XAML中使用x:Uid directive来索引。

少数情况下,可能需要对XAML的资源查找行为进行自定义。可以实现CustomXamlResourceLoader来达到此目的。可以通过CustomResource标记扩展,而不是StaticResource和ThemeResource来访问自定义加载的资源。

(完)

Categories

Tags

comments powered by Disqus