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 theText
property aren't in sync, asText
isn't updated when I updateHeader
. I choose to provide no getter to theText
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
andText
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.
object.ToString()
for all objects before formatting the string. – Voltameter