WPF中ListView的ContextMenu绑定ViewModel的GetContextMenu失败求助
想要将ListView的ContextMenu的ItemsSource绑定到UserControl的DataContext对应的ViewModel中的GetContextMenu属性,但尝试RelativeSource、ElementName绑定到ClientsListView或ClientsTab均失败,报错如下:
System.Windows.Data Error: 4 : Cannot find source for binding with reference 'ElementName=ClientsListView'. BindingExpression:Path=DataContext.GetContextMenu; DataItem=null; target element is 'ContextMenu' (Name=''); target property is 'ItemsSource' (type 'IEnumerable')
注:ListView的SelectionChanged事件绑定工作正常。
附XAML代码
<UserControl x:Class="ServerUI.Views.MainWindowViews.ClientsTabView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:i="http://schemas.microsoft.com/xaml/behaviors" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" Name="ClientsTab"> <Grid> <ListView x:Name="ClientsListView" ItemsSource="{Binding ClientViewModels}"> <ListView.ContextMenu> <ContextMenu ItemsSource="{Binding ElementName=ClientsListView, Path=DataContext.GetContextMenu}"/> </ListView.ContextMenu> <ListView.View> <GridView> <GridViewColumn x:Name="OnlineStatusHeader" Header="Online Status" DisplayMemberBinding="{Binding Online}" /> <GridViewColumn x:Name="NameHeader" Header="Computer/User" DisplayMemberBinding="{Binding ComputerUser}" /> </GridView> </ListView.View> <i:Interaction.Triggers> <i:EventTrigger EventName="SelectionChanged"> <i:InvokeCommandAction Command="{Binding UpdateSelectedClientsCommand}" CommandParameter="{Binding ElementName=ClientsListView, Path=SelectedItems}"/> </i:EventTrigger> </i:Interaction.Triggers> </ListView> </Grid> </UserControl>
附ViewModel代码
namespace ServerUI.ViewModels.MainWindowViewModels { public class ClientsTabViewModel : ObservableObject { public ObservableCollection<ClientViewModel> ClientViewModels { get; } = new(); public IEnumerable<ClientViewModel> SelectedClients { get; set; } public ICommand CloseConnectionCommand => new RelayCommand(() => ExecuteCloseConnectionCommand()); public ICommand ConfigureClientCommand => new RelayCommand(() => ExecuteConfigureClientCommand()); public ICommand UpdateSelectedClientsCommand => new RelayCommand<object>(e => ExecuteUpdateSelectedClientsCommand(e)); public ICommand OpenServerModuleCommand => new RelayCommand<object>(e => ExecuteOpenServerModuleCommand(e)); public List<FrameworkElement> GetContextMenu { get { List<FrameworkElement> menuItems = new(); menuItems.Add(new MenuItem() { Header = "Configure Client", Command = ConfigureClientCommand }); menuItems.Add(new MenuItem() { Header = "Close Connection", Command = CloseConnectionCommand }); menuItems.Add(new Separator()); foreach(var client in SelectedClients) { foreach(var module in client.Client.LoadedServerModules) { menuItems.Add(new MenuItem() { Header = module.ModuleName, Command = OpenServerModuleCommand, CommandParameter = module }); } } return menuItems; } } void ExecuteUpdateSelectedClientsCommand(object param) { System.Collections.IList items = (System.Collections.IList)param; SelectedClients = items.Cast<ClientViewModel>(); } } }
问题根源
ContextMenu属于弹出元素,它不在主视觉树中,而是独立于弹出窗口的视觉树,因此ElementName和RelativeSource(基于视觉树查找上级)的绑定方式会失效——无法定位到目标元素。而SelectionChanged的绑定能正常工作,是因为Interaction.Triggers依附在ListView本身,属于主视觉树范畴。
解决步骤
1. 改用PlacementTarget绑定
ContextMenu的PlacementTarget属性指向它附着的控件(即当前的ListView),通过这个属性可以间接绑定到ViewModel:
修改ContextMenu的绑定代码为:
<ContextMenu ItemsSource="{Binding PlacementTarget.DataContext.GetContextMenu, RelativeSource={RelativeSource Self}}"/>
2. 修复ViewModel的属性通知问题
当前GetContextMenu是普通属性,当SelectedClients变化时,UI不会自动更新菜单内容,需要补充属性变更通知:
- 将
SelectedClients改为带通知的属性 - 当
SelectedClients变化时,触发GetContextMenu的属性变更通知
修改后的ViewModel代码片段:
private IEnumerable<ClientViewModel> _selectedClients; public IEnumerable<ClientViewModel> SelectedClients { get => _selectedClients; set { _selectedClients = value; OnPropertyChanged(); OnPropertyChanged(nameof(GetContextMenu)); // 通知UI菜单内容已更新 } } void ExecuteUpdateSelectedClientsCommand(object param) { System.Collections.IList items = (System.Collections.IList)param; SelectedClients = items.Cast<ClientViewModel>(); }
3. (可选)优化菜单属性类型(符合MVVM规范)
建议将GetContextMenu的类型从List<FrameworkElement>改为ObservableCollection<object>或IEnumerable<object>,避免在ViewModel中直接创建UI元素,用数据模板定义菜单样式:
ViewModel中定义菜单数据:
public ObservableCollection<object> ContextMenuItems { get { var items = new ObservableCollection<object>(); items.Add(new { Header = "Configure Client", Command = ConfigureClientCommand }); items.Add(new { Header = "Close Connection", Command = CloseConnectionCommand }); items.Add(new Separator()); foreach(var client in SelectedClients ?? Enumerable.Empty<ClientViewModel>()) { foreach(var module in client.Client.LoadedServerModules) { items.Add(new { Header = module.ModuleName, Command = OpenServerModuleCommand, CommandParameter = module }); } } return items; } }
XAML中使用数据模板:
<ContextMenu ItemsSource="{Binding PlacementTarget.DataContext.ContextMenuItems, RelativeSource={RelativeSource Self}}"> <ContextMenu.ItemTemplate> <DataTemplate> <MenuItem Header="{Binding Header}" Command="{Binding Command}" CommandParameter="{Binding CommandParameter}"/> </DataTemplate> </ContextMenu.ItemTemplate> </ContextMenu>
内容的提问来源于stack exchange,提问作者Meme Machine

