WPF MVVM 在视图模型中使用代码 C# 为 TreeView 添加动态上下文菜单
WPF MVVM Add dynamic context Menu for TreeView using code C# in View Model
在这个著名的 article 的帮助下,我使用 HierarchicalDataTemplate 创建了一个 TreeView。
我的树视图中的每个节点都有不同的上下文菜单。所以我为 treeView 创建了一个 属性,return 为我选择了每个节点的对象。然后我使用下面的代码来显示我的上下文菜单。但是上下文菜单总是空的。
<view:MyTreeView ItemsSource="{Binding MyNode}"
SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}" >
<TreeView.Resources>
<ContextMenu x:Key="MyContextMenu" ItemsSource="{Binding ContextMenuItem}"/>
<DataTemplate DataType="{x:Type local:ChildViewModel}">
<StackPanel Orientation="Horizontal" ContextMenu="{StaticResource MyContextMenu}">
//...
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</view:MyTreeView>
PrincipalViewModel:(与 ChildViewModel 无关)
private ICommand _editMapCommand;
public ICommand EditMapCommand
{
get
{
return _editMapCommand;
}
set
{
SetProperty(ref _editMapCommand, value, () => EditMapCommand);
OnPropertyChanged("EditMapCommand");
}
}
private ICommand _removeMapCommand;
public ICommand RemoveMapCommand
{
get
{
return _removeMapCommand;
}
set
{
SetProperty(ref _removeMapCommand, value, () => RemoveMapCommand);
OnPropertyChanged("RemoveMapCommand");
}
}
private ObservableCollection<MenuItem> _contextMenuMap;
public ObservableCollection<MenuItem> ContextMenuMap
{
get
{
return _contextMenuMap;
}
set
{
SetProperty(ref _contextMenuMap, value, () => ContextMenuMap);
OnPropertyChanged("ContextMenuMap");
}
}
private object _selectedItem;
public object SelectedItem
{
get
{
return _selectedItem;
}
set
{
SetProperty(ref _selectedItem, value, () => SelectedItem);
OnPropertyChanged("SelectedItem");
Fill(_selectedItem);
}
}
private void FillPropertyCard(object obj)
{
PcEditable = false;
if (obj is MyObject)
{
ContextMenuMap = new ObservableCollection<MenuItem>();
EditMapCommand = new DelegateCommand<CancelEventArgs>(OnEditMapCommandExecute, OnEditMapCommandCanExecute);
RemoveMapCommand = new DelegateCommand<CancelEventArgs>(OnRemoveMapCommandExecute, OnRemoveMapCommandCanExecute);
ContextMenuMap.Add(new MenuItem() { Header = "editHeader", Command = EditMapCommand });
ContextMenuMap.Add(new MenuItem() { Header = "removeHeader", Command = RemoveMapCommand });
}
我想我遗漏了一些与绑定相关的东西。
注意:在调试的时候,我在xaml中发现,ContextMenuMap的值按预期改变了,但总是没有显示。
您必须代理绑定。 ContextMenus 是弹出窗口,因此它们不是同一可视化树的一部分,因此不会继承 DataContext。您可以在 Thomas Levesque's article 'How to bind to data when the DataContext is not inherited' 上阅读更多相关信息,他还提供了 BindingProxy class 的源代码。将其添加到您的项目,然后修改您的 ContextMenu 以使用它:
<local:BindingProxy x:Key="MyBindingProxy" Data="{Binding}" />
<ContextMenu x:Key="MyContextMenu" DataContext="{Binding Source={StaticResource MyBindingProxy}, Path=Data}" ItemsSource="{Binding ContextMenuMap}" />
不过,您粘贴的代码还有很多其他问题,对于初学者来说,您将上下文菜单的项目绑定到 ContextMenuItem
,而我确定您的意思是 ContextMenuMap
。另外 ContextMenuMap
不应该是 MenuItem
的 collection,你不应该在你的视图模型中声明视图控件。将 ContextMenuMap
改为 collection 字符串;上下文菜单 MenuItems
将自动创建。
编辑:抱歉 Sadok,我并不是认真地建议您在应用程序中使用 collection 字符串,我只是用它来说明在这种情况下数据绑定如何工作的总体要点.在 real-world 应用程序中,您可以为菜单项创建视图模型,就像为其他类型的视图一样。一个简单的可能只需要 header 文本、ICommands(您当前将其设置为单独的属性)并且可能支持 CanExecute 处理程序:
public class MenuItemViewModel
{
public string Header { get; private set; }
public ICommand Command { get; private set; }
public MenuItemViewModel(string header, Action execute, Func<bool> canExecute = null)
{
this.Header = header;
this.Command = new RelayCommand(execute, canExecute);
}
}
菜单将在您的代码中设置如下:
// set up the menu
this.ContextMenuMap = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel("New", OnNew),
new MenuItemViewModel("Open", OnOpen),
new MenuItemViewModel("Save", OnSave, CanSave)
};
// menu command handlers
private void OnNew() { /* ... */ }
private void OnOpen() { /* ... */ }
private void OnSave() { /* ... */ }
private bool CanSave() { /* ... */ return false; }
或者,如果您愿意,可以在适当的地方使用匿名函数:
this.ContextMenuMap = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel("Cut", () => { /* cut code here */ }),
new MenuItemViewModel("Copy", () => { /* copy code here */ }),
new MenuItemViewModel("Paste", () => { /* paste code here */ }, () => false)
};
唯一的其他变化是让您 XAML 知道如何使用此视图模型。正如我在下面提到的,您可以设置 DisplayMemberPath
来指定用于文本的字段,您可以使用样式 setter 来指定命令字段:
<ContextMenu x:Key="MyContextMenu" DataContext="{Binding Source={StaticResource MyBindingProxy}, Path=Data}" ItemsSource="{Binding ContextMenuMap}" DisplayMemberPath="Header">
<ContextMenu.Resources>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding Command}" />
</Style>
</ContextMenu.Resources>
</ContextMenu>
在这个著名的 article 的帮助下,我使用 HierarchicalDataTemplate 创建了一个 TreeView。
我的树视图中的每个节点都有不同的上下文菜单。所以我为 treeView 创建了一个 属性,return 为我选择了每个节点的对象。然后我使用下面的代码来显示我的上下文菜单。但是上下文菜单总是空的。
<view:MyTreeView ItemsSource="{Binding MyNode}"
SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}" >
<TreeView.Resources>
<ContextMenu x:Key="MyContextMenu" ItemsSource="{Binding ContextMenuItem}"/>
<DataTemplate DataType="{x:Type local:ChildViewModel}">
<StackPanel Orientation="Horizontal" ContextMenu="{StaticResource MyContextMenu}">
//...
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</view:MyTreeView>
PrincipalViewModel:(与 ChildViewModel 无关)
private ICommand _editMapCommand;
public ICommand EditMapCommand
{
get
{
return _editMapCommand;
}
set
{
SetProperty(ref _editMapCommand, value, () => EditMapCommand);
OnPropertyChanged("EditMapCommand");
}
}
private ICommand _removeMapCommand;
public ICommand RemoveMapCommand
{
get
{
return _removeMapCommand;
}
set
{
SetProperty(ref _removeMapCommand, value, () => RemoveMapCommand);
OnPropertyChanged("RemoveMapCommand");
}
}
private ObservableCollection<MenuItem> _contextMenuMap;
public ObservableCollection<MenuItem> ContextMenuMap
{
get
{
return _contextMenuMap;
}
set
{
SetProperty(ref _contextMenuMap, value, () => ContextMenuMap);
OnPropertyChanged("ContextMenuMap");
}
}
private object _selectedItem;
public object SelectedItem
{
get
{
return _selectedItem;
}
set
{
SetProperty(ref _selectedItem, value, () => SelectedItem);
OnPropertyChanged("SelectedItem");
Fill(_selectedItem);
}
}
private void FillPropertyCard(object obj)
{
PcEditable = false;
if (obj is MyObject)
{
ContextMenuMap = new ObservableCollection<MenuItem>();
EditMapCommand = new DelegateCommand<CancelEventArgs>(OnEditMapCommandExecute, OnEditMapCommandCanExecute);
RemoveMapCommand = new DelegateCommand<CancelEventArgs>(OnRemoveMapCommandExecute, OnRemoveMapCommandCanExecute);
ContextMenuMap.Add(new MenuItem() { Header = "editHeader", Command = EditMapCommand });
ContextMenuMap.Add(new MenuItem() { Header = "removeHeader", Command = RemoveMapCommand });
}
我想我遗漏了一些与绑定相关的东西。
注意:在调试的时候,我在xaml中发现,ContextMenuMap的值按预期改变了,但总是没有显示。
您必须代理绑定。 ContextMenus 是弹出窗口,因此它们不是同一可视化树的一部分,因此不会继承 DataContext。您可以在 Thomas Levesque's article 'How to bind to data when the DataContext is not inherited' 上阅读更多相关信息,他还提供了 BindingProxy class 的源代码。将其添加到您的项目,然后修改您的 ContextMenu 以使用它:
<local:BindingProxy x:Key="MyBindingProxy" Data="{Binding}" />
<ContextMenu x:Key="MyContextMenu" DataContext="{Binding Source={StaticResource MyBindingProxy}, Path=Data}" ItemsSource="{Binding ContextMenuMap}" />
不过,您粘贴的代码还有很多其他问题,对于初学者来说,您将上下文菜单的项目绑定到 ContextMenuItem
,而我确定您的意思是 ContextMenuMap
。另外 ContextMenuMap
不应该是 MenuItem
的 collection,你不应该在你的视图模型中声明视图控件。将 ContextMenuMap
改为 collection 字符串;上下文菜单 MenuItems
将自动创建。
编辑:抱歉 Sadok,我并不是认真地建议您在应用程序中使用 collection 字符串,我只是用它来说明在这种情况下数据绑定如何工作的总体要点.在 real-world 应用程序中,您可以为菜单项创建视图模型,就像为其他类型的视图一样。一个简单的可能只需要 header 文本、ICommands(您当前将其设置为单独的属性)并且可能支持 CanExecute 处理程序:
public class MenuItemViewModel
{
public string Header { get; private set; }
public ICommand Command { get; private set; }
public MenuItemViewModel(string header, Action execute, Func<bool> canExecute = null)
{
this.Header = header;
this.Command = new RelayCommand(execute, canExecute);
}
}
菜单将在您的代码中设置如下:
// set up the menu
this.ContextMenuMap = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel("New", OnNew),
new MenuItemViewModel("Open", OnOpen),
new MenuItemViewModel("Save", OnSave, CanSave)
};
// menu command handlers
private void OnNew() { /* ... */ }
private void OnOpen() { /* ... */ }
private void OnSave() { /* ... */ }
private bool CanSave() { /* ... */ return false; }
或者,如果您愿意,可以在适当的地方使用匿名函数:
this.ContextMenuMap = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel("Cut", () => { /* cut code here */ }),
new MenuItemViewModel("Copy", () => { /* copy code here */ }),
new MenuItemViewModel("Paste", () => { /* paste code here */ }, () => false)
};
唯一的其他变化是让您 XAML 知道如何使用此视图模型。正如我在下面提到的,您可以设置 DisplayMemberPath
来指定用于文本的字段,您可以使用样式 setter 来指定命令字段:
<ContextMenu x:Key="MyContextMenu" DataContext="{Binding Source={StaticResource MyBindingProxy}, Path=Data}" ItemsSource="{Binding ContextMenuMap}" DisplayMemberPath="Header">
<ContextMenu.Resources>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding Command}" />
</Style>
</ContextMenu.Resources>
</ContextMenu>