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

前端 未结 3 2193
离开以前
离开以前 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:46

    As I understand [but I probably wrong (no harm in trying)], you can initialize your items with reference to objects which were used in ancestor DataContext, i.e.

    public class ItemsVM<T> : VMBase
    {
         public T parentElement;
         public ItemsVM (T _parentElement)
         {
             this.parentElement = _parentElement;
         }
    
         ...
    
    }
    
    0 讨论(0)
  • 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:

    <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 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!

    0 讨论(0)
  • 2021-02-10 19:05

    ToolTips are not part of the visual tree as they are popup based. So your placement target biding (which uses Visual Tree search) to get the relative ancestor wont work. Why not use ContentHacking instead? This way one hacks into the visual tree from the logical elements such as ContextMenu, Popups, ToolTip etc...

    1. Declare a StaticResource which is any FrameworkElement (we need support for data context).

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

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

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

    1. Now use this StaticResource as a source for any bindings which are failing 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...

    0 讨论(0)
提交回复
热议问题