Data Binding and Tooltips in Silverlight

Have you ever wanted to databind a tooltip in Silverlight (or WPF for that matter), and found that the DataContext isn’t available for tooltips (the datacontext is null)? It’s very annoying. Tooltips, unfortunately, aren’t connected to their parents in anyway when they’re created, so they loose the ability to connect to the proper data context that is being provided to their parent. Listboxes, textboxes, all suffer from this problem.

In order to combat that problem, I’ve come up with a simple workaround for this data context problem.

I’ve created a simple attached property which augments the standard tooltip property and intervenes before it is created so that the proper data context can be established. This code hasn’t been tested thoroughly, but it works in my limited testing. It could be extended to handle more edge cases, but if you’re creating advanced tooltips outside of XAML, there’s little reason to use this code.

public class DataBindingTooltip {
     public static DependencyProperty TooltipProperty;

     static DataBindingTooltip()
     {
         TooltipProperty = DependencyProperty.RegisterAttached
             ("Tooltip", typeof(object), 
             typeof(DataBindingTooltip), 
             new PropertyMetadata(TooltipChanged));
     }

     public static void SetTooltip(DependencyObject d, object value)
     {
         d.SetValue(DataBindingTooltip.TooltipProperty, value);
     }

     public static object GetToolTip(DependencyObject d)
     {
         return d.GetValue(DataBindingTooltip.TooltipProperty);
     }

     private static void TooltipChanged(DependencyObject sender, 
         DependencyPropertyChangedEventArgs e)
     {
         if (sender is FrameworkElement)
         {
             FrameworkElement owner = sender as FrameworkElement;
             // wait for it to be in the visual tree so that // context can be established owner.Loaded += new RoutedEventHandler(owner_Loaded);
         }

     }

     static void owner_Loaded(object sender, RoutedEventArgs e)
     {
         if (sender is FrameworkElement)
         {
             FrameworkElement owner = sender as FrameworkElement;
             // remove the event handler owner.Loaded -= new RoutedEventHandler(owner_Loaded);

             DependencyObject tooltip = 
                 owner.GetValue(DataBindingTooltip.TooltipProperty) as DependencyObject;
             if (tooltip != null)
             {
                 // assign the data context of the current owner control to the tooltip's datacontext tooltip.SetValue(FrameworkElement.DataContextProperty, 
                     owner.GetValue(FrameworkElement.DataContextProperty));
             }
             ToolTipService.SetToolTip(owner, tooltip);
         }            
     }
 }

 

Here’s how to use it:

<Grid x:Name="LayoutRoot" Background="White"> <ListBox x:Name="lb" ItemsSource="{Binding Mode=OneWay}"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <local:DataBindingTooltip.Tooltip> <StackPanel Orientation="Vertical" > <TextBlock Text="Tooltip!" FontWeight="Bold" /> <TextBlock Text="{Binding Name}" /> <TextBlock Text="{Binding Age}" /> </StackPanel> </local:DataBindingTooltip.Tooltip> <TextBlock Text="{Binding Name}" Margin="4" /> <TextBlock Text="{Binding Age}" Margin="4"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid>

 

And, a small test sample:

public partial class Page : UserControl {
    public Page()
    {
        InitializeComponent();

        lb.DataContext = new List<Person>() { 
            new Person(){Name="John", Age=23},
            new Person(){Name="Hank", Age=37},
            new Person(){Name="Sally", Age=31}};
    }
}

public class Person {
    public string Name { get; set; }
    public int Age { get; set; }
}

Which, when you put it all together results in:

image

Hopefully, you can spot the trick — as soon as the host control has been created and assigned a datacontext (in the Loaded event for the element), I set the datacontext onto the tooltip and assign it to the REAL tooltip property (via the TooltipService.Tooltip attached property).

Hope you find this useful.

14 Comments

  1. hey aaron, i like the solution. there are a couple cases where unhooking the Loaded handler inside the Loaded handler will bite you.

    1. if you are assigning the tooltip in a trigger, the owner will already be loaded and you won’t be able to update the data context. you could get around this by checking if the owner is already loaded or not at the time the data context is applied.

    2. there are times where, if you are in a data bound ItemsControl and you Move() data items within the data collection, the container items will actually get Unloaded and then Reloaded (i guess the generator is smart enough to reuse the container). in this case, you’ll get bit by unhooking the Loaded handler. [in this case you have to go so far as to set a timer and check IsLoaded at that time so that you are *sure* the item has really been unloaded.

    case 1 is way more common than case 2, but case 2 is really hard to debug. ;)

  2. Thanks for pointing out a few of the cases that I suggested exist, but didn’t handle.

    Are you saying that you actually do a trigger where the TooltipServer.Tooltip property is set to a new tooltip? I just want to make sure I understand what you’re saying.

    For the second case — the visuals are destroyed and recreated for a moved item aren’t they? Since the tooltip is attached to the root datatemplate visual, it would be newly created (and hence the load would work).

    The extension the code I wanted to write this morning (but had to leave for work), was that it would double check that the control needed the loaded event handler method — if the “owner” FrameworkElement already had a datacontext (or a visual parent), then it’s probably not necessary to use the loaded event technique at all.

  3. Nice work! Compared to all the hastle of building rich tooltips in ASP.Net (even using the AJAXControlToolkit), this is a breeze. Thanks!

  4. Thank you for posting this wonderful solution. I have tried it and it works well except for when there is scrolling region. I have a datagrid and and when there are large number of rows, when one scrolls down the tips seem to stuck to the original visible area. This may be related to eburke’s and your comments. Do you have any ideas/solutions for this issue?
    Thank you.

  5. I also thank you. This is the first approach that worked for me. I have a tree view with images at each node. I wanted a tooltip to appear when hovering over those images. I couldn’t get a tooltip to bind dynamically (unless simply a property of the image control itself which prevented proper styling), until I tried your solution.

    Many thanks!

  6. Hello everybody!
    I create a new app and copy the code above to run ,
    but can’t compile pass successfully. Please help me to check ,thanks !
    Error info:
    The property ‘Tooltip’ does not exist on the type ‘StackPanel’ in the XML namespace ‘clr-namespace:SilverlightApplication2’.

Comments are closed.