ToolTipBinding

ToolTip Binding with StringFormat

Posted by eagleboost on July 7, 2020

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(实现略去)在为TextBlockText属性创建绑定的同时也自动为ToolTip生成相关绑定。

1
<TextBlock Text="{local:AutoBinding Number, StringFormat={}{0:##;(##);Zero}}"/>

参考资料