在另一个静态资源中引用静态资源

5

我正在尝试正确设置我的样式。因此,我创建了一个外部的ResourceDictionary用于所有常见的样式属性,在其中我定义了默认字体系列,如下所示:

<FontFamily x:Key="Default.FontFamily">Impact</FontFamily>

当我更改这一行时,家族中的所有地方都会发生变化。

使用和引用StaticResource

现在,我想在任何没有定义其他字体族的地方都使用这个默认字体族,大多数情况下是这样(但不是所有情况)。然而,我希望保留在任何使用此字体族的地方定义其他字体族的能力。因此,我采用了我在这里这里找到的示例,并为一个组框标题明确定义了默认字体。

<StaticResource x:Key="GroupBox.HeaderFontFamily" ResourceKey="Default.FontFamily"/>

我将用于我的GroupBox模板中包含的TextBlock上。
<Style x:Key="GroupBoxHeaderTextStyle" TargetType="{x:Type TextBlock}">
    <Setter Property="FontFamily" Value="{StaticResource GroupBox.HeaderFontFamily}"/>
</Style>

到目前为止,这个是有效的。但是,一旦我添加另一行:
<StaticResource x:Key="GroupBox.HeaderFontFamily" ResourceKey="Default.FontFamily"/>
<StaticResource x:Key="FormLabel.FontFamily" ResourceKey="Default.FontFamily"/>

我遇到了以下异常:

异常:找不到名为“Hsetu.GroupBox.HeaderFontFamily”的资源。资源名称区分大小写。

所以我发现当使用StaticResource直接寻址一个元素时,WPF无法找到它(是的,这也适用于其他元素,例如,如果我尝试直接寻址字体族"Default.FontFamily",我会得到相同的错误,因为它在StaticResource元素之前)

使用DynamicResource并引用StaticResource

我尝试了上面提供链接中第二个示例中建议的使用DynamicResource

<DynamicResource x:Key="GroupBox.HeaderFontFamily" ResourceKey="Default.FontFamily"/>
<DynamicResource x:Key="FormLabel.FontFamily" ResourceKey="Default.FontFamily"/>

<Style x:Key="GroupBoxHeaderTextStyle" TargetType="{x:Type TextBlock}">
    <Setter Property="FontFamily" Value="{StaticResource GroupBox.HeaderFontFamily}"/>
</Style>

这会抛出以下错误:

ArgumentException: “System.Windows.ResourceReferenceExpression” 不是属性“FontFamily”的有效值。

使用和引用 DynamicResource

在我的分组框样式中使用 DynamicResource 只会更改错误消息:

<DynamicResource x:Key="GroupBox.HeaderFontFamily" ResourceKey="Default.FontFamily"/>
<DynamicResource x:Key="FormLabel.FontFamily" ResourceKey="Default.FontFamily"/>

<Style x:Key="GroupBoxHeaderTextStyle" TargetType="{x:Type TextBlock}">
    <Setter Property="FontFamily" Value="{DynamicResource GroupBox.HeaderFontFamily}"/>
</Style>

System.InvalidCastException: '无法将类型为 'System.Windows.ResourceReferenceExpression' 的对象强制转换为类型 'System.Windows.Media.FontFamily'。'

添加虚拟元素

因此,由于这个问题只在我的StaticResource后面跟着另一个资源时才会出现,我想到了在资源之间包含一个虚拟元素的方法。

<StaticResource x:Key="GroupBox.HeaderFontFamily" ResourceKey="Default.FontFamily"/>
<Separator x:Key="Dummy"/>
<StaticResource x:Key="FormLabel.FontFamily" ResourceKey="Default.FontFamily"/>

现在,这个可以工作了。万岁!但是等一下……继续进行,我尝试使用第二个资源"FormLabel.FontFamily"

<Style x:Key="FormLabelStyle" TargetType="{x:Type Label}">
    <Setter Property="FontFamily" Value="{StaticResource FormLabel.FontFamily}"/>
</Style>

现在会抛出另一个异常:

System.InvalidCastException:“无法将类型为'System.Windows.Controls.Separator'的对象转换为类型'System.Windows.Media.FontFamily'。”

有 Bug 吗?

我根本没有使用 Separator,那么到底发生了什么?我猜想,在引用静态资源时,WPF 实际上会尝试使用前面的元素 - 一开始只是因为前面的元素恰好是 FontFamily 才能正常工作,并不是使用 ResourceKey 引用的元素。同时,前面的元素也无法直接访问。为了确认我的猜想,我已经用另一个 FontFamily 替换了 Separator

<StaticResource x:Key="GroupBox.HeaderFontFamily" ResourceKey="Default.FontFamily"/>
<FontFamily x:Key="Dummy">Courier New</FontFamily>
<StaticResource x:Key="FormLabel.FontFamily" ResourceKey="Default.FontFamily"/>

确实,这个方法可以解决问题,但是标签现在使用 Courier New 字体而不是 Impact 字体。

顺便说一下,这种情况不仅发生在字体族中,还发生在其他属性(FontSizeBorderThicknessFontWeight等)中。那么,这到底是 WPF 中的 bug 还是 StaticResource 应该像这样工作(这对我来说毫无意义)?如何在只定义一次的情况下在多个位置使用我的字体族?

2个回答

3

不确定奇怪引用的情况,但是如果您使用DynamicResource别名资源,则必须使用StaticResource查找它。也许有一种方法可以让动态资源引用另一个动态资源解析为原始值(例如使用自定义标记扩展),但这不是默认情况下发生的。

<Grid>
    <Grid.Resources>
        <FontFamily x:Key="Default.FontFamily">Impact</FontFamily>
        <DynamicResource x:Key="FormLabel.FontFamily" ResourceKey="Default.FontFamily"/>
    </Grid.Resources>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <Label Grid.Column="0" FontFamily="{StaticResource FormLabel.FontFamily}">Test</Label>
    <TextBox Grid.Column="1"/>
</Grid>

所以步骤如下:
  1. 声明静态
  2. 重新声明/别名动态
  3. 查找静态

如果要自己解决该值,可以编写自定义的标记扩展程序,在内部使用 MultiBinding 获取绑定元素的引用,然后解析其上的资源。
<FontFamily x:Key="Default.FontFamily">Impact</FontFamily>
<DynamicResource x:Key="FormLabel.FontFamily" ResourceKey="Default.FontFamily"/>

<Style TargetType="{x:Type Label}">
    <Setter Property="FontFamily" Value="{local:CascadingDynamicResource FormLabel.FontFamily}"/>
</Style>

public class CascadingDynamicResourceExtension : MarkupExtension
{
    public object ResourceKey { get; set; }

    public CascadingDynamicResourceExtension() { }
    public CascadingDynamicResourceExtension(object resourceKey)
    {
        ResourceKey = resourceKey;
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var binding = new MultiBinding { Converter = new CascadingDynamicResourceResolver() };
        binding.Bindings.Add(new Binding { RelativeSource = new RelativeSource(RelativeSourceMode.Self) });
        binding.Bindings.Add(new Binding { Source = ResourceKey });

        return binding;
    }
}

internal class CascadingDynamicResourceResolver : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var target = (FrameworkElement)values[0];
        var resourceKey = values[1];

        var converter = new ResourceReferenceExpressionConverter();

        object value = target.FindResource(resourceKey);

        while (true)
        {
            try
            {
                var dynamicResource = (DynamicResourceExtension)converter.ConvertTo(value, typeof(MarkupExtension));
                value = target.FindResource(dynamicResource.ResourceKey);
            }
            catch (Exception)
            {
                return value;
            }
        }
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

丑陋的try/catch存在的原因是ResourceReferenceExpressionConverter没有适当实现CanConvertFrom,并且不幸的是ResourceReferenceExpression是内部的。所以这可能仍然是最清洁的方法。尽管如此,它仍然假设一些内部操作,比如转换为MarkupExtension

该扩展可以解决任何级别的别名问题,例如使用两个别名:

<FontFamily x:Key="Default.FontFamily">Impact</FontFamily>
<DynamicResource x:Key="FormLabel.FontFamily" ResourceKey="Default.FontFamily"/>
<DynamicResource x:Key="My.FontFamily" ResourceKey="FormLabel.FontFamily"/>

<Style TargetType="{x:Type Label}">
    <Setter Property="FontFamily" Value="{local:CascadingDynamicResource My.FontFamily}"/>
</Style>

好的,我已经涵盖了这个例子,但是它抛出了异常。我已经扩展了代码示例,以便您更好地看到我已经完成了这项工作。 - Otto Abnormalverbraucher
我实际上在不同的地方同时进行这两件事。一旦我有时间并解决了其余的样式问题,我会尽快尝试这个,这可能需要几天时间,但到目前为止谢谢。 - Otto Abnormalverbraucher
虽然这样做是有效的,但由于try-catch块增加了加载时间,导致效率大幅下降。我们经常使用此资源解析器。这是唯一的方法吗? - Otto Abnormalverbraucher
由于我们从不使用比一级更深或更浅的任何引用,因此我已经删除了while循环,并在object value = target.FindResource(resourceKey);之后直接调用了try-catch块。现在加载时间恢复正常。感谢您的帮助。 - Otto Abnormalverbraucher
我遇到的另一个问题是:当我在代码后台创建一个新窗口并将当前窗口的主题传递给新创建的窗口时,我遇到了运行时异常,抱怨在此行object value = target.FindResource(resourceKey);中找不到“FormLabel.FontFamily”。 - Otto Abnormalverbraucher
显示剩余3条评论

1

对我来说,只需继承StaticResourceExtension即可。设计师可能不喜欢它,但在运行时我没有遇到任何问题。

public class StaticResourceExtension : System.Windows.StaticResourceExtension
{
    public StaticResourceExtension()
    {
    }

    public StaticResourceExtension(object resourceKey) : base(resourceKey)
    {
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接