Search code examples
c#wpfstylestemplatebindingdynamicresource

How to make a WPF resource be recalculated when a trigger's run?


In short: I've got a Style. It uses TemplateBinding a fair bit to make it parametrized instead of repeating myself over and over again. However, when a trigger for that style gets used and a resource gets used in a setter in that trigger, it just doesn't show up! Not even the default value gets shown. Here's a small program that replicates this issue:

TestDictionary.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:lcl="clr-namespace:MyNamespace">
    <Style TargetType="Button" x:Key="BtnTest">
        <Style.Resources>
            <Label Content="{TemplateBinding lcl:TestClass.String}" x:Key="innerLabel"/>
        </Style.Resources>
        <Style.Triggers>
            <Trigger Property="IsEnabled" Value="True">
                <Setter Property="Content" Value="{DynamicResource innerLabel}"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</ResourceDictionary>

MainWindow.xaml

<Window x:Class="MyNamespace.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:lcl="clr-namespace:MyNamespace"
        Title="Test" Width="500" Height="350">
    <Window.Resources>
        <ResourceDictionary Source="TestDictionary.xaml"/>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Button Content="Enable/Disable" Click="Click"/>
        <Button Grid.Column="1" x:Name="btn" Style="{DynamicResource BtnTest}" lcl:TestClass.String="TESTING"/>
    </Grid>
</Window>

MainWindow.xaml.cs

using System.Windows;

namespace MyNamespace
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private void Click(object sender, RoutedEventArgs e)
        {
            btn.IsEnabled = !btn.IsEnabled;
        }
    }
    public class TestClass
    {
        public static string GetString(DependencyObject obj)
        {
            return (string)obj.GetValue(StringProperty);
        }

        public static void SetString(DependencyObject obj, string value)
        {
            obj.SetValue(StringProperty, value);
        }
        public static readonly DependencyProperty StringProperty =
            DependencyProperty.RegisterAttached("String", typeof(string), typeof(TestClass), new PropertyMetadata("Default!"));
    }
}

Instead of using a TemplateBinding, I also tried this:

{Binding Path=lcl:TestClass.String, RelativeSource={RelativeSource AncestorType={x:Type Button}}}

It still didn't work. I know I'm probably doing something wrong, but the question is: what is it?


Solution

  • All you really need to make this work is to use RelativeSource in your binding. Since you are setting the attached property on the Button, in your style trigger, you can just bind to the attached property on self:

    <Style TargetType="Button" x:Key="BtnTest">
        <Style.Triggers>
            <Trigger Property="IsEnabled" Value="True">
                <Setter Property="Content" 
                        Value="{Binding Path=(lcl:TestClass.String), RelativeSource={RelativeSource Self}}"/>
            </Trigger>
        </Style.Triggers>
    </Style>
    

    One cool thing about using your approach, since Button is a ContentControl, you're attached property can be any object, not just strings.

    And to clarify what went wrong in your previous approach -

    • As others have said, TemplateBinding only works in ControlTemplates. It also only works when the DependencyProperty is defined on the class you are creating the template for (so you can never do a TemplateBinding to Grid.Row for example)
    • When binding to an attached property, the whole thing needs to be in parentheses, otherwise WPF will try to bind to a property of a property. Otherwise your RelativeSource binding was close!
    • I think if you want to have a Label inside the Button as the content, it may work (I didn't test that), but it doesn't seem like the best idea, as your Button can host any object you want.

    EDIT for more complex example

    So, if you need to display more than one dynamic property, I would recommend using a DataTemplate:

    <Style TargetType="Button" x:Key="BtnTest">
        <Style.Triggers>
            <Trigger Property="IsEnabled" Value="True">
                <Setter Property="ContentTemplate">
                    <Setter.Value>
                        <DataTemplate>
                            <Label Content="{Binding Path=(lcl:TestClass.String), RelativeSource={RelativeSource AncestorType={x:Type Button}}}" />
                        </DataTemplate>
                    </Setter.Value>
                </Setter> 
            </Trigger>
        </Style.Triggers>
    </Style>
    

    Also, I want to point out that a DataTemplateSelector might be more applicable if you have multiple different criteria for changing the look of the content.