phibel
phibel

Reputation: 171

TemplateBinding not working on INotifyPropertyChanged in WPF custom control

I recently created an IconButton in WPF as CustomControl. It is using TemplateBinding for the DependencyProperties:

IconButton.cs

public class IconButton : Button
{
  public static readonly DependencyProperty TextProperty;
  public static readonly DependencyProperty MDL2IconCodeProperty;

  public string Text
  {
    get { return (string)GetValue(TextProperty); }
    set { SetValue(TextProperty, value); }
  }


  public string MDL2IconCode
  {
    get { return (string)GetValue(MDL2IconCodeProperty); }
    set { SetValue(MDL2IconCodeProperty, value); }
  }


  static IconButton()
  {
    DefaultStyleKeyProperty.OverrideMetadata(typeof(IconButton),
                                             new FrameworkPropertyMetadata(typeof(IconButton)));

    TextProperty = DependencyProperty.Register("Text",
                                               typeof(string),
                                               typeof(IconButton),
                                               new PropertyMetadata("Button text", OnTextChanged));

    MDL2IconCodeProperty = DependencyProperty.Register("MDL2IconCode",
                                                       typeof(string),
                                                       typeof(IconButton),
                                                       new PropertyMetadata("\uf13e", OnIconTextChanged));
  }

  static void OnTextChanged(DependencyObject o,
                            DependencyPropertyChangedEventArgs e)
  {
    var iconButton = o as IconButton;
    if (iconButton == null)
    {
      return;
    }
    string newText = e.NewValue as string;
    iconButton.Text = newText;
  }

  static void OnIconTextChanged(DependencyObject o,
                                DependencyPropertyChangedEventArgs e)
  {
    var iconButton = o as IconButton;
    if (iconButton == null)
    {
      return;
    }

    string newText = e.NewValue as string;
    iconButton.MDL2IconCode = newText;
  }
}

Generic.xaml

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:UI.CustomControls">


  <Style TargetType="{x:Type local:IconButton}" 
         BasedOn="{StaticResource {x:Type Button}}">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="{x:Type local:IconButton}">
          <Button Style="{StaticResource {x:Static ToolBar.ButtonStyleKey}}"
                  Command="{TemplateBinding Command}"
                  CommandParameter="{TemplateBinding CommandParameter}"
                  CommandTarget="{TemplateBinding CommandTarget}">
            <StackPanel>
              <TextBlock HorizontalAlignment="Center"
                         Text="{TemplateBinding MDL2IconCode}"
                         FontFamily="Segoe MDL2 Assets"
                         FontSize="16"
                         x:Name="iconTextBlock"/>
              <TextBlock HorizontalAlignment="Center" 
                         Text="{TemplateBinding Text}"
                         x:Name="textTextBlock"/>
            </StackPanel>

          </Button>

        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>

But it works only half. I soon realized that binding to the DependencyProperties only works from the XAML designer but not from a ViewModel. So, when I set the Text property in the designer it works. But binding to it from a ViewModel, the property is neither set initially nor updated on INotifyPropertyChanged events.

So as a test I changed the TemplateBinding of Text property to

{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Text}

But it didn’t help.

What could be the problem in my code?

Is INotifyPropertyChanged supported by WPF custom controls with TemplateBinding at all?

Upvotes: 0

Views: 307

Answers (1)

user2250152
user2250152

Reputation: 20778

The issue is that you set new values of Text and MDL2IconCode in a way which breaks the Binding, so changes are not propagated to UI.

iconButton.Text = newText;
....
iconButton.MDL2IconCode = newText;

The correct way is to use SetCurrentValue method that changes the effective value of the property, but existing triggers, data bindings, and styles will continue to work.

static void OnTextChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
    var iconButton = o as IconButton;
    if (iconButton == null)
    {
        return;
    }
    string newText = e.NewValue as string;
    iconButton.SetCurrentValue(TextProperty, newText);
}

static void OnIconTextChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
    var iconButton = o as IconButton;
    if (iconButton == null)
    {
        return;
    }
    string newText = e.NewValue as string;
    iconButton.SetCurrentValue(MDL2IconCodeProperty, newText);
}

But if you don't have any special logic in OnTextChanged and OnIconTextChanged then you can get rid of PropertyChangedCallbacks and it will still work.

TextProperty = DependencyProperty.Register("Text",
                                                   typeof(string),
                                                   typeof(IconButton),
                                                   new PropertyMetadata("Button text"));

MDL2IconCodeProperty = DependencyProperty.Register("MDL2IconCode",
                                                           typeof(string),
                                                           typeof(IconButton),
                                                           new PropertyMetadata("\uf13e"));

Upvotes: 1

Related Questions