Referencing a StaticResource in another StaticResource
Asked Answered
U

2

5

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?

Underclassman answered 22/8, 2018 at 13:37 Comment(0)
U
3

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>
Understructure answered 23/8, 2018 at 8:31 Comment(8)
Well, I've already covered this example and it's throwing an exception. I've extended the code samples so you can better see I already did this.Underclassman
@OttoAbnormalverbraucher: You are using it in a style setter, which likely comes with its own deferral mechanism (as it just describes what to set instead of actually setting a value directly), thus it will not resolve the resource reference. As with referencing the resource dynamically you probably have to resolve it manually.Understructure
@OttoAbnormalverbraucher: I worked out a solution for the styling case, see the edit.Understructure
I'm doing both actually, in different places. I will try this as soon as I have time and finished the rest of my styling issues, which might take a few days but thanks so far.Underclassman
While this does work it is increasing loading times massively due to the try-catch block. We're using this resource resolver quite often. Is this the only way?Underclassman
As we never use any reference stacked deeper or less deep than one level I've removed the while loop and simply called the try-catch block directly after object value = target.FindResource(resourceKey);. Now it's back to normal load times. Thanks for your help.Underclassman
Another Problem I've come across: When I create a new window in code behind and pass the theme of the current window on to the newly created window, I run into a runtime exception complaining that "FormLabel.FontFamily" could not be found at this line object value = target.FindResource(resourceKey);.Underclassman
You could add some custom error handling if the resource cannot resolved at all times. But then you may have to trigger another conversion at a later point if that does not happen automatically.Understructure
D
1

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)
    {
    }
}
Dejection answered 20/11, 2019 at 11:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.