【问题标题】:How to set a PlacementTarget for a WPF tooltip without messing up the DataContext?如何在不弄乱 DataContext 的情况下为 WPF 工具提示设置 PlacementTarget?
【发布时间】:2011-10-10 15:40:42
【问题描述】:

我有一个典型的 Listbox 和 vm + DataTemplate 以及项目 vm 的 MVVM 设置。数据模板具有工具提示,其中包含绑定到项目 vm 的元素。一切都很好。

现在,我想让工具提示相对于列表框本身放置。它相当大,并且在随意将鼠标悬停在列表框上时会妨碍您。所以我想我会在 DataTemplate 中做这样的事情:

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

...使用静态资源...

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

这是我的问题。当我使用该 PlacementTarget 时,我收到 DisplayName.Name 和 Details 未绑定的绑定错误。它试图绑定的对象不是项目 vm,而是整个 Listbox vm。

所以我的问题是:如何为工具提示设置 ToolTipService.PlacementTarget,同时保持从其所有者那里继承的 DataContext?

【问题讨论】:

    标签: c# wpf xaml


    【解决方案1】:

    据我了解 [但我可能错了(尝试没有害处)],您可以参考在祖先 DataContext 中使用的对象来初始化您的项目,即

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

    【讨论】:

    • 我的 VM 树确实有对所有者的父引用。问题是我有相反的问题 - 它被绑定到列表框的虚拟机,我需要的是项目虚拟机中的数据。
    【解决方案2】:

    工具提示不是可视化树的一部分,因为它们是基于弹出窗口的。因此,您的展示位置目标出价(使用可视树搜索)来获取相对祖先将不起作用。为什么不使用 ContentHacking 呢?这样一来,人们就可以从 ContextMenu、Popups、ToolTip 等逻辑元素侵入可视化树...

    1. 声明一个 StaticResource,它是任何 FrameworkElement(我们需要支持数据上下文)。

      <UserControl.Resources ...>
              <TextBlock x:Key="ProxyElement" DataContext="{Binding}" />
      </UserControl.Resources>
      
    2. 在可视树中提供一个内容控件,并将此静态资源“ProxyElement”设置为其内容。

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

    上述步骤所做的是,“ProxyElement”已连接到 ItemsControl(用作 DataContext),并且可以作为 SaticResource 在任何地方使用。

    1. 现在将此 StaticResource 用作工具提示中失败的任何绑定的源...

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

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

    如果这有帮助,请告诉我...

    【讨论】:

    • 这很有趣,我玩了一段时间,但无法让它在列表框中的实际项目上工作。我认为问题不在于视觉树不可用。使用示例问题中的代码,我从绑定引擎得到的错误是它试图绑定到列表框的虚拟机,而不是项目的虚拟机。如果工具提示不是可视树的一部分是问题所在,那么我希望错误是关于尝试绑定到 null 或者可能是主窗口的 vm。但它成功找到了列表框的虚拟机。我想要的是让它使用项目的虚拟机。
    • 我尝试的另一件事是将 ContentControl 与 Content 设置为项 DataTemplate 内的代理静态资源。该绑定成功但不会重新绑定,因为我将工具提示悬停在每个数据项上(它一直绑定到第一个)。
    • 那么 Scott,您是想说 DisplayName.Name 和 Details 是单个项目的属性还是列表框的虚拟机的属性?
    • 它们是单个项目的属性。但是没关系,我得到了一个我很满意的答案并发布了它。看看,你喜欢吗?
    • 感谢斯科特的赏金! :-)
    【解决方案3】:

    好的,工作中的一位朋友主要为我解决了这个问题。这种方式超级干净,不会觉得hacky。

    这是基本问题:正如 user164184 所提到的,工具提示是弹出窗口,因此不是可视化树的一部分。因此,WPF 有一些魔力。弹出窗口的 DataContext 来自 PlacementTarget,这是绑定大部分时间的工作方式,尽管弹出窗口不是树的一部分。但是,当您更改 PlacementTarget 时,它会覆盖默认值,现在 DataContext 来自新的 PlacementTarget,无论它是什么。

    完全不直观。如果 MSDN 有,而不是花费数小时构建所有那些显示不同工具提示出现位置的漂亮图表,而是说出关于 DataContext 发生了什么的一句话,那就太好了。

    不管怎样,继续解决!与所有有趣的 WPF 技巧一样,附加属性可以提供帮助。我们将添加两个附加属性,以便在生成工具提示时直接设置工具提示的 DataContext。

    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;
            }
        }
    }
    

    然后在 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>
    ...
    

    只需将ToolTip 切换到BindableToolTip.ToolTip,然后添加一个新的BindableToolTip.DataContext,它指向你想要的任何东西。我只是将它设置为当前的 DataContext,所以它最终继承了绑定到 DataTemplate 的视图模型。

    请注意,我嵌入了工具提示,而不是使用静态资源。这是我原来的问题中的一个错误。显然必须为每个项目生成唯一的。另一种选择是使用 ControlTemplate Style 触发器。

    一项改进可能是让 BindableToolTip.DataContext 注册以获取有关 ToolTip 更改的通知,然后我可以摆脱 BindableToolTip.ToolTip。改天的任务!

    【讨论】:

    • 太棒了。谢谢!我同意这方面的文档很垃圾。
    • 我挖掘了所有 .NET Framework 源代码,试图理解 ContextMenus 和 ToolTips。两者都是弹出窗口。很好的答案。
    • 也许我做错了什么,但它说“BindableTooltip 必须从 DependencyObject 派生”。但它不能继承 DependencyObject,因为它是一个静态类。我在VB.net,也许是因为这个?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-08
    • 2011-01-03
    • 1970-01-01
    • 1970-01-01
    • 2012-01-26
    相关资源
    最近更新 更多