Binding with StringFormat on a custom control
Asked Answered
Q

2

5

I'm trying to use a custom control in a WPF app, and I have some problem using a StringFormat binding.

The problem is easy to reproduce. First, let's create a WPF application and call it "TemplateBindingTest". There, add a custom ViewModel with only one property (Text), and assign it to the DataContext of the Window. Set the Text property to "Hello World!".

Now, add a custom control to the solution. The custom control is as simple as it can get:

using System.Windows;
using System.Windows.Controls;

namespace TemplateBindingTest
{
    public class CustomControl : Control
    {
        static CustomControl()
        {
            TextProperty = DependencyProperty.Register(
                "Text",
                typeof(object),
                typeof(CustomControl),
                new FrameworkPropertyMetadata(null));

            DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl), new FrameworkPropertyMetadata(typeof(CustomControl)));
        }

        public static DependencyProperty TextProperty;

        public object Text
        {
            get
            {
                return this.GetValue(TextProperty);
            }

            set
            {
                SetValue(TextProperty, value);
            }
        }
    }
}

When adding the custom control to the solution, Visual Studio automatically created a Themes folder, with a generic.xaml file. Let's put the default style for the control in there:

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

    <Style TargetType="{x:Type local:CustomControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:CustomControl}">
                    <TextBlock Text="{TemplateBinding Text}" />
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Now, just add the control to the window, and set a binding on the Text property, using a StringFormat. Also add a simple TextBlock to be sure that the binding syntax is correct:

<Window x:Class="TemplateBindingTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:TemplateBindingTest="clr-namespace:TemplateBindingTest" Title="MainWindow" Height="350" Width="525">
<StackPanel>
    <TemplateBindingTest:CustomControl Text="{Binding Path=Text, StringFormat=Test1: {0}}"/>
    <TextBlock Text="{Binding Path=Text, StringFormat=Test2: {0}}" />
</StackPanel>

Compile, run, aaaaand... The text displayed on the window is:

Hello World!

Test2: Hello World!

On the custom control, the StringFormat is completely ignored. No error is visible on VS output window. What's going on?

Edit: The workaround.

Ok, the TemplateBinding was misleading. I found the cause and a dirty workaround.

First, notice that the problem is the same with the Content property of Button:

<Button Content="{Binding Path=Text, StringFormat=Test3: {0}}" />

So, what's going on? Let's use Reflector and dive to the StringFormat property of the BindingBase class. The 'Analyse' feature shows that this property is used by the internal DetermineEffectiveStringFormat method. Let's see this method:

internal void DetermineEffectiveStringFormat()
{
    Type propertyType = this.TargetProperty.PropertyType;
    if (propertyType == typeof(string))
    {
         // Do some checks then assign the _effectiveStringFormat field
    }
}

The problem is right here. The effectiveStringFormat field is the one used when resolving the Binding. And this field is assigned only if the DependencyProperty is of type String (mine is, as Button's Content property, Object).

Why Object? Because my custom control is a bit more complex than the one I pasted, and like the Button I want the control's user to be able to provide child controls rather than just text.

So, what now? We're running into a behaviour existing even in WPF core controls, so I can just leave it "as is". Still, as my custom control is used only on an internal project, and I want it to be easier to use from XAML, I decided to use this hack:

using System.Windows;
using System.Windows.Controls;

namespace TemplateBindingTest
{
    public class CustomControl : Control
    {
        static CustomControl()
        {
            TextProperty = DependencyProperty.Register(
                "Text",
                typeof(string),
                typeof(CustomControl),
                new FrameworkPropertyMetadata(null, Callback));

            HeaderProperty = DependencyProperty.Register(
                "Header",
                typeof(object),
                typeof(CustomControl),
                new FrameworkPropertyMetadata(null));

            DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl), new FrameworkPropertyMetadata(typeof(CustomControl)));
        }

        static void Callback(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            obj.SetValue(HeaderProperty, e.NewValue);
        }

        public static DependencyProperty TextProperty;
        public static DependencyProperty HeaderProperty;

        public object Header
        {
            get
            {
                return this.GetValue(HeaderProperty);
            }

            set
            {
                SetValue(HeaderProperty, value);
            }
        }

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

Header is the property used in my TemplateBinding. When a value is provided to Text, the StringFormat is applied since the property is of type String, then the value is forwarded to the Header property using a callback. It works, but it's really dirty:

  • The Header and the Text property aren't in sync, as Text isn't updated when I update Header. I choose to provide no getter to the Text property to avoid some mistakes, but it can still happen if someone directly reads the value from the DependencyProperty (GetValue(TextProperty)).
  • Unpredictable behaviour may happen if someone provides a value to both Header and Text property as one of the values will be lost.

So overall, I wouldn't recommand using this hack. Do it only if you are really in control of your project. If the control has even the slightest chance of being used on another project, just give up on the StringFormat.

Quilting answered 27/12, 2011 at 12:59 Comment(0)
A
3

StringFormat is used when binding to a string property, while the Text property in your control is of type object, hence StringFormat is ignored.

Asceticism answered 27/12, 2011 at 13:59 Comment(2)
Incorrect. The runtime invokes object.ToString() for all objects before formatting the string.Voltameter
Yes, but it checks the underlying type of the dependency property before applying the StringFormat. It works if I change the type of my dp to String instead of Object.Quilting
V
6

You can't pass StringFormat or Converter when using TemplateBinding. Here are few workarounds.

Voltameter answered 27/12, 2011 at 13:5 Comment(0)
A
3

StringFormat is used when binding to a string property, while the Text property in your control is of type object, hence StringFormat is ignored.

Asceticism answered 27/12, 2011 at 13:59 Comment(2)
Incorrect. The runtime invokes object.ToString() for all objects before formatting the string.Voltameter
Yes, but it checks the underlying type of the dependency property before applying the StringFormat. It works if I change the type of my dp to String instead of Object.Quilting

© 2022 - 2024 — McMap. All rights reserved.