1. The problem
ToolTip
binding is one of the common problems of WPF
applications. For example, the binding source has a Int32
property Number
, its value can be positive or negative, based on the scenario sometimes we want to display the values in different forms:
Value | Display |
---|---|
1000 | 1000 |
-1000 | (1000) |
0 | Zero |
Binding
has built-in support for StringFormat, along with Custom numeric format strings anyone can easily come up with this:
1
<TextBlock Text="{Binding Number, StringFormat={}{0:##;(##);Zero}}"/>
The problem is that the same piece of code does not work for ToolTip
. No errors show up at either compile time or run time, the only thing is based on the result we can tell StringFormat
is ignored somehow.
2. Analysis
It’s actually an old problem - the FrameworkElement.ToolTip property is of type object
, which allows developers put anything in it. When it comes to display, it’s the ToolTip control does the real job . The ToolTip
control is a ContentControl
, so we need to use ContentStringFormat to make it work:
1
2
3
4
5
<TextBlock Text="{Binding Number, StringFormat={}{0:##;(##);Zero}}">
<TextBlock.ToolTip>
<ToolTip Content="{Binding Number}" ContentStringFormat="{}{0:##;(##);Zero}"/>
</TextBlock.ToolTip>
</TextBlock>
ToolTip
is flexible enough to display complex user interface, but 99.99%
of the time we just want to display some strings. It seems to me not worth making XAML
complicated for such a tiny task. We know that sometimes Microsoft
does tricks to make developers’ life easier, in my pinion this could have been one of those tricks, but they didn’t do it.
Luckily that WPF
is super flexible, so let’s do it on our own.
3. Implementations
There’re more than one choices we can use, in this post we use custom binding approach to achieve the most intuitive usage, it’s again based on the BindingDecoratorBase used in my previous article EnabledStateBinding:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public sealed class ToolTipBinding : BindingDecoratorBase
{
public ToolTipBinding()
{
}
public ToolTipBinding(string path)
{
Path = new PropertyPath(path);
}
public override object ProvideValue(IServiceProvider provider)
{
if (StringFormat == null)
{
throw new ArgumentException("StringFormat is not set", nameof(StringFormat));
}
var converter = Binding.Converter;
var newConverter = new ToolTipConverter {Converter = converter, Format = StringFormat};
Binding.Converter = newConverter;
return Binding.ProvideValue(provider);
}
private class ToolTipConverter : IValueConverter
{
public IValueConverter Converter { get; set; }
public string Format { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var v = value;
var cvt = Converter;
if (cvt != null)
{
////If there's a Converter comes with the original binding, use it first
v = cvt.Convert(value, targetType, parameter, culture);
}
return string.Format(Format, v);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Here’s the usage:
1
2
<TextBlock Text="{Binding Number, StringFormat={}{0:##;(##);Zero}}"
ToolTip="{local:ToolTipBinding Number, StringFormat={}{0:##;(##);Zero}}"/>
We can further optimize the code based on specific cases. For example, the AutoBinding
(implementation is omitted) below can automatically generate binding for the ToolTip
property based on the binding of the Text
property.
1
<TextBlock Text="{local:AutoBinding Number, StringFormat={}{0:##;(##);Zero}}"/>
References
1. 问题
ToolTip
的绑定也是WPF
应用程序开发的常见问题之一。比如数据绑定的源对象有一个Int32
类型的属性Number
,数值可能为正数也可能为负数,根据不同场景我们有时候希望显示为不同的形式,比如:
数值 | 显示 |
---|---|
1000 | 1000 |
-1000 | (1000) |
0 | Zero |
借助Binding
对于StringFormat的内建支持,配合Custom numeric format strings可以轻松写出如下实现:
1
<TextBlock Text="{Binding Number, StringFormat={}{0:##;(##);Zero}}"/>
问题是把同样的代码放到ToolTip
行不通,不论是编译时还是运行时都不会报错,但从结果可以看出StringFormat
被忽略了。
2. 分析
这实际上是个老生常谈的问题——FrameworkElement.ToolTip类型是object
,可以放任何类型的数据,而真正显示是由ToolTip完成的。而ToolTip
是个ContentControl
,要用ContentStringFormat才能起作用,如下所示:
1
2
3
4
5
<TextBlock Text="{Binding Number, StringFormat={}{0:##;(##);Zero}}">
<TextBlock.ToolTip>
<ToolTip Content="{Binding Number}" ContentStringFormat="{}{0:##;(##);Zero}"/>
</TextBlock.ToolTip>
</TextBlock>
ToolTip
很强大可以显示复杂信息不假,但99.99%
的时候我们只是要显示一串字符而已,为了这芝麻大点事把XAML
搞复杂有点本末倒置。在WPF
中有时候微软会有一些小trick
为开发者提供便利,在绑定上为ToolTip
提供StringFormat
支持应该是可以提供的便利,然而并没有。
好在WPF
极度灵活,自己实现一下并非难事。
3. 实现
解决办法不少,本文采用自定义绑定的方法以达到最直观的使用形式,同样基于我之前的文章EnabledStateBinding中用到的BindingDecoratorBase。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public sealed class ToolTipBinding : BindingDecoratorBase
{
public ToolTipBinding()
{
}
public ToolTipBinding(string path)
{
Path = new PropertyPath(path);
}
public override object ProvideValue(IServiceProvider provider)
{
if (StringFormat == null)
{
throw new ArgumentException("StringFormat is not set", nameof(StringFormat));
}
var converter = Binding.Converter;
var newConverter = new ToolTipConverter {Converter = converter, Format = StringFormat};
Binding.Converter = newConverter;
return Binding.ProvideValue(provider);
}
private class ToolTipConverter : IValueConverter
{
public IValueConverter Converter { get; set; }
public string Format { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var v = value;
var cvt = Converter;
if (cvt != null)
{
////如果原先的绑定有Converter,先调用Converter把数据转换一下
v = cvt.Convert(value, targetType, parameter, culture);
}
return string.Format(Format, v);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
使用方式如下:
1
2
<TextBlock Text="{Binding Number, StringFormat={}{0:##;(##);Zero}}"
ToolTip="{local:ToolTipBinding Number, StringFormat={}{0:##;(##);Zero}}"/>
根据实际情况,还可以进一步简化。比如下面的AutoBinding
(实现略去)在为TextBlock
的Text
属性创建绑定的同时也自动为ToolTip
生成相关绑定。
1
<TextBlock Text="{local:AutoBinding Number, StringFormat={}{0:##;(##);Zero}}"/>