WPF MenuItem 小傳 (3) -- ContextMenu 開啟前與關閉前的處理

ContextMenu 本身只有 Opened (開啟後) 和 Closed (關閉後) 事件,那要如何處理 【開啟前】與【關閉前】?

前情提要

寫習慣 .NET 的人應該都知道框架對於事件命名的其中一個規則是用 ed 字尾表示「某個動作發生以後」的事件,而用 ing 字尾表示「某個動作發生之前」的事件。但是在 ContextMenu class 身上左翻右找也就只有 Opened 和 Closed,難道沒法處理 Opening 和 Closing 嗎?

首先要知道的是當一個元素需要 ContextMenu 的時候,其實是設定一個 ContextMenu 的執行個體給 FrameworkElement.ContextMenu 屬性,微軟把處理開啟前和關閉前的事件擺在 FrameworkElement 身上,也就是:

  1. FrameworkElement.ContextMenuOpening 事件
  2. 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 上的相關範例在此連結