ContextMenu 本身只有 Opened (開啟後) 和 Closed (關閉後) 事件,那要如何處理 【開啟前】與【關閉前】?
前情提要
寫習慣 .NET 的人應該都知道框架對於事件命名的其中一個規則是用 ed 字尾表示「某個動作發生以後」的事件,而用 ing 字尾表示「某個動作發生之前」的事件。但是在 ContextMenu class 身上左翻右找也就只有 Opened 和 Closed,難道沒法處理 Opening 和 Closing 嗎?
首先要知道的是當一個元素需要 ContextMenu 的時候,其實是設定一個 ContextMenu 的執行個體給 FrameworkElement.ContextMenu 屬性,微軟把處理開啟前和關閉前的事件擺在 FrameworkElement 身上,也就是:
江湖一點訣,說完沒半撇,唯一的問題只是知道這玩意在哪而已。
來個範例
不免俗總要有點範例來玩玩,假設的情境是畫面上有兩個 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 上的相關範例在此連結。