ContextMenu 本身只有 Opened (开启后) 和 Closed (关闭后) 事件,那要如何处理 【开启前】与【关闭前】?
前情提要
写习惯 .NET 的人应该都知道框架对于事件命名的其中一个规则是用 ed 字尾表示「某个动作发生以后」的事件,而用 ing 字尾表示「某个动作发生之前」的事件。但是在 ContextMenu class 身上左翻右找也就只有 Opened 和 Closed,难道没法处理 Opening 和 Closing 吗?
首先要知道的是当一个元素需要 ContextMenu 的时候,其实是设定一个 ContextMenu 的执行个体给 FrameworkElement.ContextMenu 属性,微软把处理开启前和关闭前的事件摆在 FrameworkElement 身上,也就是:
- FrameworkElement.ContextMenuOpening 事件
- FrameworkElement.ContextMenuClosing 事件
江湖一点诀,说完没半撇,唯一的问题只是知道这玩意在哪而已。
来个範例
不免俗总要有点範例来玩玩,假设的情境是画面上有两个 Border,在不同 Border 开启的时候会有不同的选项被启用或禁用。
在 MainViewModel 中,先处理储存两个 Border 资讯。
public class MainViewModel : NotifyPropertyBase
{
private ICommand _borderLoadedCommand;
public ICommand BorderLoadedCommand
{
get
{
if (_borderLoadedCommand == null)
{
_borderLoadedCommand = new RelayCommand((x) =>
{
var border = x as Border;
if (border != null)
{
if (!BorderDictionary.ContainsKey(border.Name))
{
BorderDictionary.Add(border.Name, border);
}
}
});
}
return _borderLoadedCommand;
}
}
private Dictionary<string, Border> BorderDictionary { get; set; }
public MainViewModel()
{
BorderDictionary = new Dictionary<string, Border>();
}
}
XAML 的部分则是让两个 Border 的 Loaded 事件繫结到 BorderLoadedCommand。(需安装 Microsoft.Xaml.Behaviors.Wpf 套件)
<Grid>
<Grid.RowDefinitions >
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Background="LightSteelBlue" x:Name="lightSteelBlueBorder" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding BorderLoadedCommand}" CommandParameter="{Binding ElementName=lightSteelBlueBorder}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Border>
<Border Background="SpringGreen" Grid.Row="1" x:Name="springGreenBorder">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding BorderLoadedCommand}" CommandParameter="{Binding ElementName=springGreenBorder}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Border>
</Grid>
设计 MenuItem 使用的 ViewModel,其中IsEnabled 属性会决定这个选项是否要启用:
public class MenuItemViewModel : NotifyPropertyBase
{
private string _header;
public string Header
{
get => _header;
set => SetProperty(ref _header, value);
}
private bool _isEnabled;
public bool IsEnabled
{
get => _isEnabled;
set => SetProperty(ref _isEnabled, value);
}
private RelayCommand _command;
public RelayCommand Command
{
get => _command;
set => SetProperty(ref _command, value);
}
private ObservableCollection<MenuItemViewModel> _menuItems;
public ObservableCollection<MenuItemViewModel> MenuItems
{
get => _menuItems;
set => SetProperty(ref _menuItems, value);
}
}
完成 MainViewModel 中整个 Menu 内容的资料:
public class MainViewModel : NotifyPropertyBase
{
private ObservableCollection<MenuItemViewModel> _menuItems;
public ObservableCollection<MenuItemViewModel> MenuItems
{
get => _menuItems;
set => SetProperty(ref _menuItems, value);
}
private ICommand _borderLoadedCommand;
public ICommand BorderLoadedCommand
{
get
{
if (_borderLoadedCommand == null)
{
_borderLoadedCommand = new RelayCommand((x) =>
{
var border = x as Border;
if (border != null)
{
if (!BorderDictionary.ContainsKey(border.Name))
{
BorderDictionary.Add(border.Name, border);
}
}
});
}
return _borderLoadedCommand;
}
}
private Dictionary<string, Border> BorderDictionary { get; set; }
private void InitialMenuItems()
{
MenuItems = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel
{
Header = "File",
IsEnabled = false,
MenuItems = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel
{
Header = "New",
Command = new RelayCommand((x) => { Console.WriteLine("New"); }),
IsEnabled = true
},
new MenuItemViewModel
{
Header = "Open",
Command = new RelayCommand((x) => { Console.WriteLine("Open"); }),
IsEnabled = true
},
new MenuItemViewModel
{
Header = "Save",
Command = new RelayCommand((x) => { Console.WriteLine("Save"); }),
IsEnabled = true
},
new MenuItemViewModel
{
Header = "Exit",
Command = new RelayCommand((x) => { Console.WriteLine("Exit"); }),
IsEnabled = true
}
}
},
new MenuItemViewModel
{
Header = "Edit",
IsEnabled = false,
MenuItems = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel
{
Header = "Copy",
Command = new RelayCommand((x) => { Console.WriteLine("Copy"); }),
IsEnabled = true
},
new MenuItemViewModel
{
Header = "Paste",
Command = new RelayCommand((x) => { Console.WriteLine("Paste"); }),
IsEnabled = true
}
}
}
};
}
public MainViewModel()
{
BorderDictionary = new Dictionary<string, Border>();
InitialMenuItems();
}
}
MainViewModel 加入 Opening 与 Closing 的相对应命令:
private ICommand _menuOpeningCommand;
public ICommand MenuOpeningCommand
{
get
{
if (_menuOpeningCommand == null)
{
_menuOpeningCommand = new RelayCommand((x) =>
{
foreach (var item in MenuItems)
{
if (BorderDictionary["lightSteelBlueBorder"].IsMouseOver)
{
if (item.Header == "File")
{
item.IsEnabled = true;
}
else
{
item.IsEnabled = false;
}
}
else if (BorderDictionary["springGreenBorder"].IsMouseOver)
{
if (item.Header == "Edit")
{
item.IsEnabled = true;
}
else
{
item.IsEnabled = false;
}
}
}
});
}
return _menuOpeningCommand;
}
}
private ICommand _menuClosingCommand;
public ICommand MenuClosingCommand
{
get
{
if (_menuClosingCommand == null)
{
_menuClosingCommand = new RelayCommand((x) =>
{
Debug.WriteLine("MenuClosingCommand");
});
}
return _menuClosingCommand;
}
}
因为结构简单,所以没考虑要把程式码写得太高大上,意思到就好。这样就完成整个 ViewModel 。
XAML 中主要就是把 MenuOpeningCommand 繫结到 Window.ContextMenuOpening 事件;MenuClosingCommand 则繫结到 Window.ContextMenuClosing 事件。相关部分如以下:
<Window x:Class="WpfMenuItemStorySample003.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:local="clr-namespace:WpfMenuItemStorySample003"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<i:Interaction.Triggers>
<i:EventTrigger EventName="ContextMenuOpening">
<i:InvokeCommandAction Command="{Binding MenuOpeningCommand}"/>
</i:EventTrigger>
<i:EventTrigger EventName="ContextMenuClosing">
<i:InvokeCommandAction Command="{Binding MenuClosingCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<Window.ContextMenu>
<ContextMenu ItemsSource="{Binding MenuItems}" x:Name="contextMenu">
<ContextMenu.Resources >
<HierarchicalDataTemplate ItemsSource="{Binding MenuItems}" DataType="{x:Type local:MenuItemViewModel}">
<TextBlock Text="{Binding Header}"/>
</HierarchicalDataTemplate>
</ContextMenu.Resources>
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Window.ContextMenu>
如此就完成了。Github 上的相关範例在此连结。