开发者

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

开发者 https://www.devze.com 2023-03-21 22:52 出处:网络
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.

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.

Now, I'd like to have the tooltip placed relative to the listbox itself. It's fairly large and gets in the way when casually mousing over the listbox. So I figured I'd do something like this in the DataTemplate:

<Grid ...>
    <TextBlock x:Na开发者_如何学JAVAme="ObjectText"
        ToolTipService.Placement="Left"
        ToolTip="{StaticResource ItemToolTip}"
        ToolTipService.PlacementTarget="{Binding RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}">
    </TextBlock>
...

...with the static resource...

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

Here's my problem. When I use that PlacementTarget I get a binding error that the DisplayName.Name and Details are not binding. The object it's trying to bind to is not the item vm but the overall Listbox vm.

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


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!


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


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

精彩评论

暂无评论...
验证码 换一张
取 消