有時候會需要採用比較靈活的功能選單,希望能靠著資料繫結來完成,比較普遍的做法就是採用階層式資料繫結,讓我們一步步來完成這個需求。
建立 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>
如此就簡單解決這個問題,範例請參考這邊。