3

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?

It'sNotALie.
  • 22,289
  • 12
  • 68
  • 103

3 Answers3

1

Now I see the details. What you should write before relative source is like:

Binding Path=(lcl:TestClass.String)

Do not forget to add parenthesis.

Bill Zhang
  • 1,909
  • 13
  • 10
  • I copied your code to a new project, replaced your binding code and it worked. If you do not mind you can send me a small project. Saying "Does not work" is easy but does not help solving the problem, right? – Bill Zhang Jun 25 '13 at 13:17
  • It is. However it was tried and it didn't work. I'll try to clean and rebuild. – It'sNotALie. Jun 25 '13 at 16:08
  • Let's clear what you expected first. In your code, the attached String is set to "Default!" initially. When UI shows up, Label shows "TESTING" since you gave it the value in initialization of the button. When you click the button, the Label shows nothing since you do not give it a content outside the trigger. Am I correct? – Bill Zhang Jun 25 '13 at 17:15
  • Not really, it was just to see if either of them showed up. Also, cleaned and rebuilt the solution. No luck. – It'sNotALie. Jun 25 '13 at 17:17
  • I shared my project here: https://skydrive.live.com/redir?resid=99DD12AC5EE8E440!236 You can take a look to see whether it is what you expect. – Bill Zhang Jun 25 '13 at 17:32
  • It works. Seems to be working, but NOT in the label. (because in reality I need to host a control in another instance of the same control pretty much, so the ancestortype doesn't work) – It'sNotALie. Jun 25 '13 at 20:10
1

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.

It'sNotALie.
  • 22,289
  • 12
  • 68
  • 103
Abe Heidebrecht
  • 30,090
  • 7
  • 62
  • 66
  • I prefer this one over the other solution, not only because this also provides me a simpler RelativeSource, but also actually tells me it outside of the comments. – It'sNotALie. Jun 25 '13 at 20:06
  • However, I've got another problem. I need to do it on the label (because in reality it's a Label in a VisualBrush in a TextBox, with more than one property needing to be dynamic). This kind of works on the label, but it only shows the default value. I've got everything as a `DynamicResource`. Thanks for your help though! – It'sNotALie. Jun 25 '13 at 20:09
  • Okay, well, I thought that might be the case. I think you will probably want to do something with a DataTemplate (maybe even DataTemplateSelector). I'll edit the answer to elaborate. – Abe Heidebrecht Jun 25 '13 at 20:14
  • Few questions: what's a DataTemplateSelector? And yeah, maybe a DataTemplate's what's right. – It'sNotALie. Jun 28 '13 at 18:42
  • A `DataTemplateSelector` is a class you write that is given the item to template, and the container for it (in your case the button), and it returns a `DataTemplate`. These can be defined in XAML and referenced, or built in code (a pain in the butt). You can use whatever criteria you need to select a template, making it more powerful than what can be done in XAML alone. Learn more here: http://msdn.microsoft.com/en-us/library/system.windows.controls.datatemplateselector.selecttemplate.aspx – Abe Heidebrecht Jun 28 '13 at 19:32
  • Now it kind of works, **but** it only fetches the default value. (This is with inline content). – It'sNotALie. Jun 29 '13 at 13:39
  • Well, if you set `Content` directly on a your `Button`, nothing in the style can override it. This is because of `DependencyProperty` value precedence, described here: http://msdn.microsoft.com/en-us/library/ms743230.aspx Instead, you should set the `Content` in the `Style`. – Abe Heidebrecht Jun 29 '13 at 14:45
  • I mean as in directly in the setter. – It'sNotALie. Jun 29 '13 at 15:17
  • Can you post a complete example on pastebin or something? I can't figure out what is going on based on these short comments. – Abe Heidebrecht Jun 29 '13 at 15:28
  • Just move the fixed resource to a Setter.Value tag. – It'sNotALie. Jun 29 '13 at 15:54
0

Your example does not work because TemplateBinding only works in a ControlTemplate. To achieve something akin to a TemplateBinding in Resources you need to do other stuff. Here's an example.

In order for TemplateBinding to work, you need to fix the code a little bit (this is just an example with no resources):

<Style x:Key="BtnTest" TargetType="{x:Type Button}">
    <Setter Property="MinHeight" Value="100" />
    <Setter Property="MinWidth" Value="200" />
    <Setter Property="BorderThickness" Value="2" />
    <Setter Property="BorderBrush" Value="Blue" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Button}">
                <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2" Background="{TemplateBinding Background}">
                    <ContentPresenter RecognizesAccessKey="True" Content="{TemplateBinding lcl:TestClass.String}" HorizontalAlignment="Center" VerticalAlignment="Center" />
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Style.Triggers>
        <Trigger Property="IsEnabled" Value="False">
            <Setter Property="Opacity" Value="0.5" />
        </Trigger>
    </Style.Triggers>
</Style>

Useful links about this topic: Here, and here too.

EDIT:

You can also use the application settings instead of TestClass. Open "Project -> Properties: MyNamespace... -> Settings" and add your settings:

Name--------Type--------Scope--------Value

LabelText---string--------User----------Default

Set the your value for the LabelText in code. For example:

    public MainWindow()
    {
        InitializeComponent();

        MyNamespace.Properties.Settings.Default.LabelText = "Testing";
    }

And use this ResourceDictionary:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:properties="clr-namespace:MyNamespace.Properties"
                xmlns:lcl="clr-namespace:MyNamespace">

<Style TargetType="Button" x:Key="BtnTest">
    <Style.Resources>
        <Label x:Key="innerLabel" Content="{Binding Source={x:Static properties:Settings.Default}, Path=LabelText, Mode=TwoWay}" />
    </Style.Resources>

    <Style.Triggers>
        <Trigger Property="IsEnabled" Value="True">
            <Setter Property="Content" Value="{DynamicResource innerLabel}"/>
        </Trigger>
    </Style.Triggers>
</Style>

Community
  • 1
  • 1
Anatoliy Nikolaev
  • 22,370
  • 15
  • 69
  • 68
  • Not only does that stop the button from having the usual animations (and a weird border too), it also doesn't do what I want it to do...? – It'sNotALie. Jun 22 '13 at 19:11
  • Ok, what do you exactly want? The resource can not just use a `TemplateBinding`, so you do not see the value. Use hacks to make Binding resource. – Anatoliy Nikolaev Jun 22 '13 at 19:23
  • It's throwing build errors, is your second solution. Also, even if you fix it, that ruins the point of XAML: XAML's meant to be about separating the view and the model (MVVM). – It'sNotALie. Jun 23 '13 at 10:58
  • For me, the second solution works, personally checked. As for separating the view and the model (`MVVM`) I completely agree... but in the example you want to use resources from the bindings, but it does not support the standard XAML because the resource is not part of the visual tree, or part of the template so you have to look for alternatives. One example of an alternative, and I'll show... there may be several. I can advise not to use Binding in resources, or to seek other alternatives if you do not like mine. – Anatoliy Nikolaev Jun 23 '13 at 11:10
  • Sorry, my fault for forgetting to clean the solution. It works. However, I can't understand the rest of the comment... sorry. – It'sNotALie. Jun 23 '13 at 11:18
  • You can't understand the rest of the comment because of my bad `English` :), or because of the technical part? – Anatoliy Nikolaev Jun 23 '13 at 11:37
  • Because of your bad english, I'm afraid. – It'sNotALie. Jun 23 '13 at 11:40