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!