WPF:ContextMenu再次出击。DataContext未更新






4.90/5 (16投票s)
当父级的 DataContext 更改时,
简而言之
你可能已经知道,ContextMenu
类并非 WPF 视觉树的一部分(参见 这里 和 这里,了解由此引起的一些问题)。
尽管如此,ContextMenu
会获取其父控件的 DataContext
(自 .NET 3.5 起?参见 这里)。问题在于,这种获取只发生一次。如果父控件的数据上下文随后发生变化,ContextMenu
的数据上下文将不会更新。如果包含上下文菜单的视图绑定到可变内容(例如 listbox
的选定项目),这将导致问题。
解决方法
解决方法是显式地将菜单的数据上下文绑定到父控件的 datacontext
,如下所示
<ContextMenu DataContext="{Binding PlacementTarget.DataContext,
RelativeSource={RelativeSource Self}}" >
这个神奇的咒语告诉 WPF 在菜单的数据上下文和其“放置目标”(即父控件)的数据上下文之间创建永久绑定,即使父控件的数据上下文发生变化,该绑定仍然有效。只有当你预计父控件的数据上下文在其生命周期内会发生变化时,才需要这个咒语。
示例
- ContextMenuDataContext.zip 是一个 Visual Studio 2010 解决方案。

我创建了一个简单的示例来演示这个问题。它包含一个带有国家/地区名称的列表框和两个用户控件:“好”和“坏”。两个控件都显示国家/地区的首都。它们还具有右键单击菜单,以显示国家/地区的语言。“好”控件显示所选国家/地区的正确语言。“坏”控件在第一次调用菜单时显示正确的语言,然后即使所选国家/地区发生变化,也继续显示该语言。
这是因为菜单首先在右键单击时创建,然后从父控件获取(正确的)数据上下文。在后续的右键单击中,将重用相同的菜单对象(由“Same menu?”命令证明),并且其数据上下文不会改变,除非我们为其创建显式绑定。
以下是一些关键代码片段(为了清晰起见,省略了某些细节,包括“Same menu”命令)
class Country
{
public string Name { get; set; }
public string Capital { get; set; }
public string Language { get; set; }
}
static class Countries
{
public static readonly Country[] List = new[]
{
new Country { Name = "USA", Capital="Washington", Language="English"},
new Country { Name = "Spain", Capital="Madrid", Language="Spanish"},
new Country { Name = "France", Capital="Paris", Language="French"},
new Country { Name = "Brazil", Capital="Brasilia", Language="Portuguese"},
new Country { Name = "Thailand", Capital="Bangkok", Language="Thai"},
};
}
class LanguageCommand : ICommand
{
public void Execute(object parameter)
{
object safeParameter = parameter ?? "null";
MessageBox.Show(safeParameter.ToString());
}
}
<!-- MainWindow.xaml -->
<Window Title="MainWindow" Height="350" Width="525">
<DockPanel LastChildFill="True">
<ListBox Name="CountryList" ItemsSource="{x:Static local:Countries.List}" />
<UniformGrid Rows="2" Columns="1" DataContext="{Binding SelectedItem,
ElementName=CountryList}">
<local:GoodControl />
<local:BadControl />
</UniformGrid>
</DockPanel>
</Window>
<!-- GoodControl.xaml -->
<UserControla x:Class="ContextMenuDataContext.GoodControl">
<UserControl.Resources>
<local:LanguageCommand x:Key="LanguageCommand" />
</UserControl.Resources>
<UserControl.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget.DataContext,
RelativeSource={RelativeSource Self}}" >
<MenuItem Header="Language" Command="{StaticResource LanguageCommand}"
CommandParameter="{Binding Language}" />
</ContextMenu>
</UserControl.ContextMenu>
<TextBlock Text="{Binding Capital}" />
</UserControl>
<!-- BadControl.xaml -->
<UserControl x:Class="ContextMenuDataContext.BadControl">
<UserControl.Resources>
<local:LanguageCommand x:Key="LanguageCommand" />
</UserControl.Resources>
<UserControl.ContextMenu>
<ContextMenu>
<MenuItem Header="Language" Command="{StaticResource LanguageCommand}"
CommandParameter="{Binding Language}" />
</ContextMenu>
</UserControl.ContextMenu>
<TextBlock Text="{Binding Capital}" />
</UserControl />
结论
默认的数据上下文绑定机制在大多数情况下都能很好地工作,因为大多数视图在其生命周期内都不会改变数据上下文。问题出现在数据上下文发生变化时。最令人恼火的问题是,即使数据上下文发生变化,第一次调用菜单时一切都会正常工作。但是,后续调用将返回陈旧的数据。这可能会被忽略一段时间。显式地将上下文菜单的数据上下文绑定到 PlacementTarget.DataContext
以避免此错误。
历史
- 2011 年 2 月 27 日:初始发布