How to set a PlacementTarget for a WPF tooltip without messing up the DataContext?

前端 未结 3 2195
离开以前
离开以前 2021-02-10 18:19

I have a typical MVVM setup of Listbox and vm + DataTemplate and item vm\'s. The data templates have tooltips, which have elements bound to the item vm\'s. All works great.

3条回答
  •  逝去的感伤
    2021-02-10 18:56

    Ok, a friend at work mostly figured it out for me. This way is super clean, doesn't feel hacky.

    Here's the basic problem: as user164184 mentioned, tooltips are popups and therefore not part of the visual tree. So there's some magic that WPF does. The DataContext for the popup comes from the PlacementTarget, which is how the bindings work most of the time, despite the popup not being part of the tree. But when you change the PlacementTarget this overrides the default, and now the DataContext is coming from the new PlacementTarget, whatever it may be.

    Totally not intuitive. It would be nice if MSDN had, instead of spending hours building all those pretty graphs of where the different tooltips appear, said one sentence about what happens with the DataContext.

    Anyway, on to the SOLUTION! As with all fun WPF tricks, attached properties come to the rescue. We're going to add two attached properties so we can directly set the DataContext of the tooltip when it's generated.

    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 the XAML:

    
        
            
                
                    
                        
                        
                        ...
                    
                
            
        
    ...
    

    Just switch the ToolTip over to BindableToolTip.ToolTip instead, then add a new BindableToolTip.DataContext that points at whatever you want. I'm just setting it to the current DataContext, so it ends up inheriting the viewmodel bound to the DataTemplate.

    Note that I embedded the ToolTip instead of using a StaticResource. That was a bug in my original question. Obviously has to be generated unique per item. Another option would be to use a ControlTemplate Style trigger thingy.

    One improvement could be to have BindableToolTip.DataContext register for notifications on the ToolTip changing, then I could get rid of BindableToolTip.ToolTip. A task for another day!

提交回复
热议问题