WPF:如何将ItemsControl内ContextMenu命令绑定至ViewModel命令
需求:
- 有一个绑定到
List<Person>集合的ItemsControl,其内部包含ContextMenu - 需要将
MainWindowViewModel中的DisplayNameCommand和DisplaySurnameCommand分别绑定到对应的ContextMenu菜单项,禁止绑定到Person对象 - 同时ContextMenu要保留ItemsSource的数据上下文,以便在命令参数中访问当前Person对象的属性
相关代码
ItemsControl XAML代码
<ItemsControl ItemsSource="{Binding PeopleList}"> <ItemsControl.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}"/> <TextBlock Text="{Binding Surname}"/> <Image> <Image.ContextMenu> <ContextMenu> <MenuItem Header="Display Name" Command="{Binding DisplaySurnameCommand}" CommandParameter="{Binding Name}"> </MenuItem> <MenuItem Header="Display Surname" Command="{Binding DisplaySurnameCommand}" CommandParameter="{Binding Surname}"> </MenuItem> </ContextMenu> </Image.ContextMenu> </Image> </StackPanel> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>
ViewModel和实体类代码
public class MainWindowViewModel { public MainWindowViewModel() { DisplayNameCommand = new RelayCommand(DisplayName); DisplaySurnameCommand = new RelayCommand(DisplaySurname); PeopleList = new List<Person>(); PeopleList.Add(new Person("Julie", "Adams")); PeopleList.Add(new Person("Mack", "McMack")); PeopleList.Add(new Person("Josh", "Broccoli")); } public List<Person> PeopleList { get; set; } public void DisplayName(object message) { MessageBox.Show("Name: " + (string)message); } public void DisplaySurname(object message) { MessageBox.Show("Surname: "+ (string)message); } public RelayCommand DisplayNameCommand { get; } public RelayCommand DisplaySurnameCommand { get; } } public class Person { public Person(string name, string surname) { Name = name; Surname = surname; } public string Name { get; set; } public string Surname { get; set; } }
已尝试的方案
- 使用ItemContainerStyle:能触发命令,但只能为所有菜单项设置同一个命令,无法分别绑定不同命令
- 直接设置相对绑定:出现绑定错误
Cannot find source: RelativeSource FindAncestor, AncestorType='System.Windows.Window', AncestorLevel='1' - 参考StackOverflow方案:同样出现上述绑定错误
解决方案
ContextMenu在WPF中不属于可视化树,直接用RelativeSource查找Window会失败。以下两种方案可满足需求,且无需修改Person类:
方案1:利用PlacementTarget和Tag传递ViewModel
修改Image和ContextMenu的绑定逻辑,通过Image的Tag承载ViewModel,再通过ContextMenu的PlacementTarget访问:
<Image Tag="{Binding DataContext, RelativeSource={RelativeSource AncestorType=Window}}"> <Image.ContextMenu> <ContextMenu> <MenuItem Header="Display Name" Command="{Binding PlacementTarget.Tag.DisplayNameCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" CommandParameter="{Binding Name}"> </MenuItem> <MenuItem Header="Display Surname" Command="{Binding PlacementTarget.Tag.DisplaySurnameCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" CommandParameter="{Binding Surname}"> </MenuItem> </ContextMenu> </Image.ContextMenu> </Image>
原理:ContextMenu的PlacementTarget指向触发它的Image控件,Image的Tag绑定到Window的ViewModel,既可以访问ViewModel中的命令,又能保持CommandParameter对Person属性的绑定。
方案2:使用Freezable辅助类传递数据上下文
- 创建Freezable类型的辅助类:
public class BindingProxy : Freezable { protected override Freezable CreateInstanceCore() { return new BindingProxy(); } public object Data { get { return (object)GetValue(DataProperty); } set { SetValue(DataProperty, value); } } public static readonly DependencyProperty DataProperty = DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null)); }
- 在Window的Resources中声明该代理:
<Window.Resources> <local:BindingProxy x:Key="ViewModelProxy" Data="{Binding}" /> </Window.Resources>
- 修改ContextMenu的命令绑定:
<Image.ContextMenu> <ContextMenu> <MenuItem Header="Display Name" Command="{Binding Data.DisplayNameCommand, Source={StaticResource ViewModelProxy}}" CommandParameter="{Binding Name}"> </MenuItem> <MenuItem Header="Display Surname" Command="{Binding Data.DisplaySurnameCommand, Source={StaticResource ViewModelProxy}}" CommandParameter="{Binding Surname}"> </MenuItem> </ContextMenu> </Image.ContextMenu>
原理:Freezable对象不会脱离可视化树的数据上下文,可通过它间接访问ViewModel,同时保留对Person属性的参数绑定。
内容的提问来源于stack exchange,提问作者mooshmore
相关产品推荐
相关产品推荐

