WPF工具提示不更新

14
假设我有一个简单的代表员工的类。
class Staff
{
    public string FirstName { get; set; }
    public string FamilyName { get; set; }
    public int SecondsAlive { get; set; }        
}

我有一个针对员工的数据模板

<DataTemplate DataType={x:Type Staff}>
    <StackPanel Orientation="Horizontal">
        <TextBlock Text={Binding FirstName}/>
        <TextBlock Text=" ">
        <TextBlock Text={Binding FamilyName}/>
        <StackPanel.ToolTip>
            <TextBlock Text={Binding SecondsAlive}/>
        </StackPanel.ToolTip>
     </StackPanel>
</DataTemplate>

我随后在一个列表框中展示了一大堆的内容

myListBox.ItemsSource = GetAllStaff();

很标准的内容。我的问题是,当有人存活的秒数发生变化时,显示存活时间的工具提示不会更新。当您第一次将鼠标悬停在员工上方时,它可以正常工作,但此后它将永远保留该值。我可以实现INotifyPropertyChanged来解决这个问题,但每当SecondsAlive更改时为每个员工都执行此操作似乎有点过度设计。如果列表中有400名员工,那么我必须引发400个事件,即使用户可能永远不再查看另一个工具提示。我希望能够让工具提示在每次显示时请求SecondsAlive属性。是否可能做到这一点?

请注意,这只是一个示例,我不需要知道我的员工已经存活了多少秒 :-) 但我的问题是,仅为一个可能永远没有人看的工具提示而引发400次事件。


你是否真正测试了这种情况(触发400个事件),并且在实现INotifyPropertyChanged时事件是否触发了400次?如果是这样,你可以使用虚拟化堆栈面板,这样就不会实例化(因此绑定)所有的Staff对象。 - Emond
在我的情况下,每个员工都由屏幕上的一个小框表示,并且所有员工始终可见。 - MikeKulls
目前我正在触发这400个事件。由于我们无法调试绑定(下一个版本可能会支持),所以我无法确定结果是什么。 - MikeKulls
速度并不一定是问题。请记住,我的SecondsAlive只是一个例子,我谈论的是工具提示的一般情况。如果有大量更新,每个更新都会引发400个事件,那么这可能是一个问题。问题的一部分是我需要编写的代码量。例如,在我的应用程序中的某个地方,我有一个员工对象,大约有30个属性,我添加了一个基于这些30个属性的工具提示属性。对于员工的每个属性,我都需要为工具提示引发一个propertychanged事件。这对我来说似乎很麻烦。 - MikeKulls
如果我能让工具提示每次调用绑定,那么在应用程序的很多地方都会变得更简单。我进行了一些测试,发现在某些情况下会创建一个新的工具提示对象,但在其他情况下会重复使用。奇怪的是,它必须在工具提示对象之外缓存结果,因为即使创建了新的工具提示对象,它仍然保留旧值(如果未引发属性更改)。我只需要让工具提示丢弃此缓存即可。 - MikeKulls
6个回答

42

天哪!我终于找到了这个问题的解决方案!!!这个问题困扰我好几个月了。我不奇怪为什么没有人回答,因为我在顶部输入的代码实际上并没有展示我试图重现的问题,事实上它展示了解决方案。答案是,如果你像这样定义你的工具提示

    <StackPanel.ToolTip>
        <TextBlock Text="{Binding SecondsAlive}"/>
    </StackPanel.ToolTip>

那么一切都很顺利,不需要在"SecondsAlive"上触发propertyChanged事件。每次显示工具提示时,框架都会调用SecondsAlive属性。问题出现在您像这样定义工具提示时:

    <StackPanel.ToolTip>
        <ToolTip>
            <TextBlock Text="{Binding SecondsAlive}"/>
        </ToolTip>
    </StackPanel.ToolTip>

在这里使用额外的tooltip标签是有道理的,你需要创建一个tooltip对象来分配给tooltip属性,但这是不正确的。你实际上分配给tooltip属性的是tooltip的内容。我以为你需要控制它,如文本块和图像来显示,但你可以传入任何东西,它将像内容控件一样显示内容。看到它从内容控件继承,这是有道理的 :-) 一旦你知道了,所有这些都变得很明显 :-)

感谢大家关注这个问题。

PS. 我发现另一个问题,即简化代码的下一个合理步骤就是像这样直接将文本分配给tooltip(假设你的tooltip是纯文本):

 <TextBlock Text="{Binding Path=StaffName}" ToolTip="{Binding Path=StaffToolTip}"/>

这也导致了我之前遇到的问题。这是有道理的,因为 StaffToolTip 属性的结果被分配给了 tooltip 属性,并且永远不会再次调用。然而,为什么将 TextBlock 赋值给 tooltip 属性实际上解决了问题并不太合理。


我刚发现去掉<ToolTip>标签会导致一个错误,可以在这两个链接中看到。所以看起来我又回到了最初的方法,必须在所有地方引发属性更改。很遗憾。http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/07d2a0bb-af63-44d0-be05-cd31aefa8110?prof=required和https://connect.microsoft.com/VisualStudio/feedback/details/496959/specified-element-is-already-the-logical-child-of-another-element-disconnect-it-first - MikeKulls
那很有道理。我也遇到了相同的问题。我以为我的ViewModel有问题。谢谢。 - Lance
我为Slider.ToolTip元素做了这个,看起来它工作得很好,我没有看到你提到的错误。我正在使用.NET 4,但链接说该错误在那里没有被修复,所以我不知道为什么我没有看到它。 - gakera
移除<ToolTip Placement="Bottom">后,我的工具提示得以更新。现在我正在寻找设置位置的方法。(我相信我会在常规搜索中找到它。) - Doug Ferguson
这很有效,我正在使用Visual Studio 2015 + Update 3。没有这个提示,我永远不会发现解决方案! - Contango

3
尽管这是一个老问题。
在这种情况下:
    <StackPanel.ToolTip>
        <ToolTip>
            <TextBlock Text="{Binding SecondsAlive}"/>
        </ToolTip>
    </StackPanel.ToolTip>
ToolTip控件由一个独立的HWND(即本地窗口)托管。它应该有自己的DataContext,正确的绑定表达式应该如下:
    <StackPanel.ToolTip>
        <ToolTip 
            DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
            <TextBlock Text="{Binding SecondsAlive}"/>
        </ToolTip>
    </StackPanel.ToolTip>

1

在这种情况下,您可以使用一个很酷的技巧

现在存活的秒数=最初存活的秒数+经过的时间

您可以绑定到经过的时间属性,并指定一个转换器将初始值添加到其中。这样,您只需要引发1个事件,所有工具提示都会更新。

编辑:您可以将ElapsedTime属性(带有INotifyPropertyChanged)添加到许多地方--一个逻辑位置可能是存储Staff对象的集合

编辑:您还需要将每个工具提示绑定到共享的ElapsedTime属性,而不是SecondsAlive属性


嗨,Pickles,感谢你的答复。不幸的是,这只是我用来举例的一个例子,我实际上并不需要知道某个人已经活了多少秒。我只是想为一般情况下使用。尽管......如果您的示例代码有效,那么它一定会欺骗工具提示程序执行我想要做的事情(每次显示时调用属性)。也许,如果我只有一个什么也不做的转换器就可以解决问题。稍后尝试一下。 - MikeKulls
我的想法没有奏效。同样的问题,你仍然需要一个PropertyChanged事件。 - MikeKulls
我不太明白你的示例中ElapsedTime是从哪里来的。是从窗口上托管的控件吗? - MikeKulls
谢谢回复,但不幸的是这并没有真正回答我的问题。SecondsAlive只是一个例子,所以为了让它工作,我需要创建一个虚拟属性,所有工具提示都基于此属性,并在该属性上引发PropertyChanged事件。对我来说,这似乎有点像黑客行为。我所要做的就是避免编写重复的代码,每当应用程序中的任何对象的任何属性更新时,都会调用PropertyChanged("ToolTip")。如果我有50个对象,每个对象有30个属性,那么我需要在1500个位置引发此事件。对我来说,这似乎是不必要的。 - MikeKulls

0
值得注意的是,ToolTip 在重新加载新数据之前会检查其绑定的对象的相等性。
在我的情况下,我进行了一个重写。
public override bool Equals(object obj) 

并且

public override int GetHashCode()

在一个具有属性的类中

public class MultipleNameObject { string Name, string[] OtherNames};

不幸的是,我只对MultipleNameObject的Name属性进行了string.Compare()以进行相等性比较。 工具提示应该在ItemsControl中显示OtherNames,但如果鼠标悬停在网格上之前的MultipleNameObject上,即使OtherNames不同,也不会更新。

[编辑]启用调试后运行可以确认ToolTip正在使用我的对象的GetHashCode()重写来决定是否获取新数据。 修复将string[] OtherNames考虑在内的问题即可解决该问题。


就原始问题而言,我猜这可能意味着当对象更改时,工具提示可能不会更新,因为它将认为它等于自身,即使一些内部已更改,除非OP正确地覆盖了绑定对象的compareTo和Equals。这是推测,但对我有效。 - Samuel
我尝试过这个,但没有成功。@MikeKulls的答案起作用了。关键是要覆盖你正在处理的任何内容的模板,然后仔细使用正确的XAML,以避免复制一次性内容。 - Contango

0

这个示例展示了如何向网格添加一个工具提示,该工具提示在用户悬停在网格中的单元格上时按需重新计算。

如果您有一个包含10,000个项目的巨大网格,并且希望持续更新网格 - 但不希望持续更新工具提示,因为这需要时间,那么这将非常有用。

此示例适用于Infragistics,但原则同样适用于其他优秀的库,如DevExpress。

Xaml

<ig:TemplateColumn Key="ColumnKey">
    <ig:TemplateColumn.HeaderTemplate>
        <DataTemplate>
            <TextBlock Text="Header"></TextBlock>
        </DataTemplate>
    </ig:TemplateColumn.HeaderTemplate>
    <ig:TemplateColumn.ItemTemplate>
        <DataTemplate>
            <StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                <StackPanel.ToolTip>
                    <TextBlock Text="{Binding ToolTip}"/>
                </StackPanel.ToolTip>
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="MouseEnter" >
                        <i:InvokeCommandAction Command="{Binding ToolTipHoverRefreshCommand}" />
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </StackPanel>
        </DataTemplate>
    </ig:TemplateColumn.ItemTemplate>
</ig:TemplateColumn>

视图模型

public ICommand ToolTipHoverRefreshCommand => new DelegateCommand(() =>
{
    this.OnPropertyChanged(this.ToolTip);
});  

public string ToolTip
{
    get
    {
        return DateTime.Now.ToString();
    }
    set
    {
        this.OnPropertyChanged(nameof(this.ToolTip));
    }
}

0

我曾遇到同样的问题,无法进行更新。我找到了解决方法,就是将控件添加到工具提示模板中:

<DataTemplate DataType={x:Type Staff}>
    <StackPanel Orientation="Horizontal">
        <TextBlock Text={Binding FirstName}/>
        <TextBlock Text=" ">
        <TextBlock Text={Binding FamilyName}/>
        <StackPanel.ToolTip>
            <Tooltip>
                <ToolTip.Template>
                    <ControlTemlate>
                        <TextBlock Text={Binding SecondsAlive}/>
                    </ControlTemplate>
                </ToolTip.Template>
            </Tooltip> 
        </StackPanel.ToolTip>
     </StackPanel>
</DataTemplate>

我不太明白为什么需要这样做,或者为什么这会使它的行为不同,但它解决了问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接