WPF MenuItem 小傳 (2) -- 資料繫結

  • 58
  • 0

有時候會需要採用比較靈活的功能選單,希望能靠著資料繫結來完成,比較普遍的做法就是採用階層式資料繫結,讓我們一步步來完成這個需求。

建立 view model

功能選單的 view model 基本要素有三個:(1) 要顯示的功能名稱 (2) 功能的命令 (3) 下一層功能選單,其中 (2) 和 (3) 是互斥的。

依照上面的說明建立一個簡單的 view model:

 public class MenuItemViewModel : NotifyPropertyBase
 {
     private string _header;

     public string Header
     {
         get => _header;
         set => SetProperty(ref _header, value);
     }

     private RelayCommand _command;
     public RelayCommand Command
     {
         get => _command;
         set => SetProperty(ref _command, value);
     }

     private ObservableCollection<MenuItemViewModel> _items;
     public ObservableCollection<MenuItemViewModel> Items
     {
         get => _items;
         set => SetProperty(ref _items, value);
     }       
 }

接著我們建立視窗的 view model :

 public class MainViewModel
 {
     public MainViewModel()
     {
         InitialMenuItems();
     }

     private ObservableCollection<MenuItemViewModel> _items;
     public ObservableCollection<MenuItemViewModel> Items
     {
         get => _items;
         set => _items = value;
     }


     private void InitialMenuItems()
     {
         Items = new ObservableCollection<MenuItemViewModel>
         {
             new MenuItemViewModel
             {
                 Header = "File",
                 Items = new ObservableCollection<MenuItemViewModel>
                 {
                     new MenuItemViewModel
                     {
                         Header = "New",
                         Command = new RelayCommand((x) => { MessageBox.Show("New"); })
                     },
                     new MenuItemViewModel
                     {
                         Header = "Open",
                         Command = new RelayCommand((x) => { MessageBox.Show("Open"); })
                     },
                     new MenuItemViewModel
                     {
                         Header = "Save",
                         Command = new RelayCommand((x) => { MessageBox.Show("Save"); })
                     },
                     new MenuItemViewModel
                     {
                         Header = "Exit",
                         Command = new RelayCommand((x) => { MessageBox.Show("Exit"); })
                     }
                 }
             },
             new MenuItemViewModel
             {
                 Header = "Edit",
                 Items = new ObservableCollection<MenuItemViewModel>
                 {
                     new MenuItemViewModel
                     {
                         Header = "Copy",
                         Command = new RelayCommand((x) => { MessageBox.Show("Copy"); })
                     },
                     new MenuItemViewModel
                     {
                         Header = "Cut",
                         Command = new RelayCommand((x) => { MessageBox.Show("Cut"); })
                     },
                     new MenuItemViewModel
                     {
                         Header = "Paste",
                         Command = new RelayCommand((x) => { MessageBox.Show("Paste"); })
                     }
                 }
             }
         };
     }
 }
資料樣板

對於這種階層式的資料,資料樣板就不是一般的 DataTemplate,而是 HierarchicalDataTemplate;這是一種階層式的資料樣板,它具有 ItemsSource 屬性可以指定下一層的資料,在 WPF 原生的控制項中應該只有 Menu 系列和 Treeview 支援這種階層式資料樣板。也就是說在設計 Menu 資料繫結的時候,普遍的作法都是採用這種階層式樣板,除非你的功能選單只有一層。

所以整個 Window 的 xaml 如下所示:

<Window x:Class="WpfMenuItemStorySample002.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:local="clr-namespace:WpfMenuItemStorySample002"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions >
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>        
        <Menu ItemsSource="{Binding Items}">
            <Menu.Resources >
                <HierarchicalDataTemplate ItemsSource="{Binding Items}" DataType="{x:Type local:MenuItemViewModel}">
                    <TextBlock Text="{Binding Header}"/>
                </HierarchicalDataTemplate>               
            </Menu.Resources>
        </Menu>
    </Grid>
</Window>

可以看到 Menu 本身的 ItemsSource 繫結到 MainViewModel的 Items,在 Menu.Resources 裡設計了一個 HierarchicalDataTemplate,而這邊的 ItemsSource 繫結的則是下一層的 Items,所謂的下一層可以一直往下延伸;這邊用上了一個屬性 – DataType,這個屬性指明了所有符合 local:MenuItemViewModel 型別的資料都會套用這個樣板,你可以想像它是一種簡便的 TemplateSelector,只是無需自己親自寫個樣板選擇器。

命令繫結

回頭看看樣板:

 <HierarchicalDataTemplate ItemsSource="{Binding Items}" DataType="{x:Type local:MenuItemViewModel}">
     <TextBlock Text="{Binding Header}"/>                    
 </HierarchicalDataTemplate>

這時會發現要繫結命令根本無從下手,這時會出現一個誤區,我們可能會想把 TextBlock 改成有 Command 屬性的 MenuItem,畢竟看起來 Menu 的每個 Item 是個 MenuItem 好像很正常?

其實不然,當我們在設計資料樣板的時候,其實樣板套用的對象是 MenuItem 裡面的 ContentPresenter 的樣板,所以當你把 TextBlock 換成 MenuItem 的時候會變成 MenuItem 的內部又包了一個 MenuItem,這會讓功能表的外觀和行為都變得有點詭異。

另外一種方式,把 TextBlock 換成 Button 呢 ? 比起用 MenuItem 好一點,但是你可能需要花上一些時間來重新設計那個 Button 的 Style 甚至是 ControlTemplate。

我則習慣用 ItemContainerStyle 來解決這個問題,這個屬性所設定的 Style 套用的對象就是 MenuItem 本身,讓我們把它擺上去:

 <Menu ItemsSource="{Binding Items}">
     <Menu.Resources >
         <HierarchicalDataTemplate ItemsSource="{Binding Items}" DataType="{x:Type local:MenuItemViewModel}">
             <MenuItem Header="{Binding Header}" Command="{Binding Command}"/>                    
         </HierarchicalDataTemplate>               
     </Menu.Resources>
     <Menu.ItemContainerStyle>
         <Style TargetType="MenuItem">
             <Setter Property="Command" Value="{Binding Command}"/>
         </Style>
     </Menu.ItemContainerStyle>
 </Menu>

如此就簡單解決這個問題,範例請參考這邊