Referencing a StaticResource in another StaticResource

你说的曾经没有我的故事 提交于 2019-12-23 02:58:07

问题


I'm trying to set up my styles properly. Thus I've created an external ResourceDictionary for all common style attributes, in which I've defined a default font family like this:

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

This way the family changes at all places when I change this single line.

using and referencing StaticResource

Now I want to use this default font family wherever nothing else is defined, which is in most places (but not all). However, I want to retain the ability of defining other font families for any place this is used. So I went with the examples I've found here and here, and defined the default font explicitly for a group box header:

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

I use this on a TextBlock that is included in a template of my group box.

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

So far, this is working. However, as soon as I add another line:

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

I get this exception thrown:

Exception: Cannot find resource named 'Hsetu.GroupBox.HeaderFontFamily'. Resource names are case sensitive.

So I've experienced that WPF cannot find an Element directly addressed when followed by a StaticResource (Yes, this also counts for elements other than StaticResources. eg, if I tried to address the font family "Default.FontFamily" directly I would get the same error, because it precedes a StaticResource element)

using DynamicResource and referencing StaticResource

I've tried using a DynamicResource as suggested in the 2nd example I've provided a link to above:

<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>

This throws the following error:

ArgumentException: 'System.Windows.ResourceReferenceExpression' is not a valid value for property 'FontFamily'.

using and referencing DynamicResource

Using DynamicResource in my group box style only changed the error message:

<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: 'Unable to cast object of type 'System.Windows.ResourceReferenceExpression' to type 'System.Windows.Media.FontFamily'.'

adding a dummy element

So, as this problem only occurs when my StaticResource is followed by another, I've got the idea of including a dummy element between the resources.

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

Now, this works. Hooray! But wait a minute... continuing on, I tried to use the second resource "FormLabel.FontFamily"

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

This throws another exception now:

System.InvalidCastException: 'Unable to cast object of type 'System.Windows.Controls.Separator' to type 'System.Windows.Media.FontFamily'.'

Bug?

I'm not even using the Separator at all, so what is going on? I assume, when addressing a StaticResource, WPF actually tries to use the preceding element - which only worked in the beginning because the preceding element was a FontFamily by chance - and not the element that is referenced with the ResourceKey. At the same time, rendering the preceding element inaccessible directly. In order to confirm my suspicion, I've replaced the Separator with another FontFamily.

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

And indeed, it worked, but the Labels are now using the Courier New font instead of the referenced Impact font.

Btw. this does not only happen with font families but also other attributes (FontSize, BorderThickness, FontWeight, etc.). So, is this actually a bug in WPF or are StaticResources supposed to act like this (which wouldn't make any sense to me)? How can I get to use my font family in multiple places only defining it once?


回答1:


Not sure what is going on with the odd referencing, but if you alias a resource using DynamicResource you have to look that up using StaticResource. Maybe there is a way to make the dynamic resource referencing another dynamic resource resolve to the original value (e.g. using a custom markup extension), but that is not what happens by default.

<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>

So the steps are:

  1. Declare static
  2. Re-declare/alias dynamic
  3. Look up static

To resolve the value yourself you can write a custom markup extension that uses a MultiBinding internally to get a reference to the bound element and then resolve the resource on it.

<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();
    }
}

The ugly try/catch exists because the ResourceReferenceExpressionConverter has no proper implementation of CanConvertFrom and unfortunately the ResourceReferenceExpression is internal, so this is probably still the cleanest way of doing it. It still assumes some internals like the conversion to MarkupExtension, though.

This extension resolves any level of aliasing, e.g. with two aliases:

<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>



回答2:


Simply inheriting form StaticResourceExtension works for me. The designer doesn't always like it, but at run time I haven't encountered any issues.

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

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


来源:https://stackoverflow.com/questions/51968177/referencing-a-staticresource-in-another-staticresource

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!