How to set PlacementTarget for WPF tooltip without spoiling DataContext?

I have a typical MVVM setup for Listbox and vm + DataTemplate and item vm. Data templates have tooltips that have elements associated with the vm element. Everything works great.

Now I would like the tooltip to be placed relative to the list itself. It is quite large and interferes, casually glancing at the list. So I decided that I would do something similar in the DataTemplate:

<Grid ...> <TextBlock x:Name="ObjectText" ToolTipService.Placement="Left" ToolTip="{StaticResource ItemToolTip}" ToolTipService.PlacementTarget="{Binding RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"> </TextBlock> ... 

... with a static resource ...

 <ToolTip x:Key="ItemToolTip"> <StackPanel> <TextBlock Text="{Binding DisplayName.Name}"/> <TextBlock Text="{Binding Details}" FontStyle="Italic"/> ... </StackPanel> </ToolTip> 

Here is my problem. When I use this PlacementTarget, I get a binding error that DisplayName.Name and Details are optional. The object that he is trying to bind is not a vm element, but a general vm list.

So my question is: how can I set ToolTipService.PlacementTarget for a tooltip, but keep the DataContext inherited from its owner?

+4
source share
3 answers

Well, a friend at work basically figured it out for me. This method is super clean, does not feel hacked.

Here is the main problem: as indicated in user164184, tooltips and therefore are not part of the visual tree. So there is the magic that WPF does. The DataContext for the popup comes from a PlacementTarget, since bindings work most of the time, even though popup is not part of the tree. But when you change the PlacementTarget, it overrides the default value, and now the DataContext comes from the new PlacementTarget, whatever it is.

Totally unintuitive. It would be nice if MSDN, instead of spending hours building all these good graphs, where various tooltips appear, said one sentence about what happens to the DataContext.

In any case, the decision! Like all WPF fun tricks, the attached properties come to the rescue. We are going to add two attached properties so that we can directly set the DataContext of the tooltip when creating it.

 public static class BindableToolTip { public static readonly DependencyProperty ToolTipProperty = DependencyProperty.RegisterAttached( "ToolTip", typeof(FrameworkElement), typeof(BindableToolTip), new PropertyMetadata(null, OnToolTipChanged)); public static void SetToolTip(DependencyObject element, FrameworkElement value) { element.SetValue(ToolTipProperty, value); } public static FrameworkElement GetToolTip(DependencyObject element) { return (FrameworkElement)element.GetValue(ToolTipProperty); } static void OnToolTipChanged(DependencyObject element, DependencyPropertyChangedEventArgs e) { ToolTipService.SetToolTip(element, e.NewValue); if (e.NewValue != null) { ((ToolTip)e.NewValue).DataContext = GetDataContext(element); } } public static readonly DependencyProperty DataContextProperty = DependencyProperty.RegisterAttached( "DataContext", typeof(object), typeof(BindableToolTip), new PropertyMetadata(null, OnDataContextChanged)); public static void SetDataContext(DependencyObject element, object value) { element.SetValue(DataContextProperty, value); } public static object GetDataContext(DependencyObject element) { return element.GetValue(DataContextProperty); } static void OnDataContextChanged(DependencyObject element, DependencyPropertyChangedEventArgs e) { var toolTip = GetToolTip(element); if (toolTip != null) { toolTip.DataContext = e.NewValue; } } } 

And then in XAML:

 <Grid ...> <TextBlock x:Name="ObjectText" ToolTipService.Placement="Left" ToolTipService.PlacementTarget="{Binding RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" mystuff:BindableToolTip.DataContext="{Binding}"> <mystuff:BindableToolTip.ToolTip> <ToolTip> <StackPanel> <TextBlock Text="{Binding DisplayName.Name}"/> <TextBlock Text="{Binding Details}" FontStyle="Italic"/> ... </StackPanel> </ToolTip> </mystuff:BindableToolTip.ToolTip> </TextBlock> ... 

Just switch ToolTip to BindableToolTip.ToolTip , and then add a new BindableToolTip.DataContext that indicates what you want. I just set it to the current DataContext, so it inherits the view model bound to the DataTemplate.

Note that I introduced ToolTip instead of using StaticResource. This was a mistake in my original question. Obviously, a unique one should be created for each element. Another option is to use a ControlTemplate style trigger.

One improvement could be the presence of the BindableToolTip.DataContext registry for ToolTip change notifications, and then I could get rid of BindableToolTip.ToolTip. The task for the next day!

+7
source

Hints are not part of the visual tree as they pop up. Thus, your placement target (which uses a search in the Visual Tree) to make the sibling ancestor not work. Why not use ContentHacking instead? Thus, you crack the visual tree from such logical elements as ContextMenu, Popups, ToolTip, etc.

  • Declare StaticResource, which is any element of FrameworkElement (we need data context support).

     <UserControl.Resources ...> <TextBlock x:Key="ProxyElement" DataContext="{Binding}" /> </UserControl.Resources> 
  • Put the content control in the Visual Tree and set this static "ProxyElement" resource as its content.

     <UserControl ...> <Grid ...> <ItemsControl x:Name="MyItemsControl" ItemsTemplate="{StaticResource blahblah}" .../> <ContentControl Content="{StaticResource ProxyElement}" DataContext="{Binding ElementName=MyItemsControl}" Visibility="Collapsed"/> 

What has been done above is that the "ProxyElement" was connected to the ItemsControl (which serves as the DataContext), and it is available as a SaticResource to be used anywhere.

  • Now use this StaticResource as the source for any bindings that do not appear in your tooltip ...

     <Grid ...> <TextBlock x:Name="ObjectText" ToolTipService.Placement="Left" ToolTip="{StaticResource ItemToolTip}" PlacementTarget="{Binding Source={StaticResource ProxyElement}, Path=DataContext}" ... /> <!-- This sets the target as the items control --> 

and

  <ToolTip x:Key="ItemToolTip"> <StackPanel DataContext="{Binding Source={StaticResource ProxyElement}, Path=DataContext.DataContext}"><!-- sets data context of the items control --> <TextBlock Text="{Binding DisplayName.Name}"/> <TextBlock Text="{Binding Details}" FontStyle="Italic"/> ... </StackPanel> </ToolTip> 

Let me know if this helps ...

+5
source

As I understand it [but I'm probably mistaken (there is no harm in trying)], you can initialize your objects with a reference to the objects that were used in the ancestor of the DataContext, i.e.

 public class ItemsVM<T> : VMBase { public T parentElement; public ItemsVM (T _parentElement) { this.parentElement = _parentElement; } ... } 
0
source

All Articles