在WPF应用程序中,菜单设计是一个重要的组成部分,它需要结合多种技术来实现。菜单通常包括静态部分、上下文敏感部分以及动态变化的部分,如最近打开的文件或当前打开的窗口。本文将展示如何针对不同情况设计菜单。
对于静态菜单,希望在XAML中声明整个结构。这样做的好处是设计和代码分离,同时获得最佳的开发工具支持。将每个菜单项绑定到视图模型中的ICommand属性。
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="_New" Command="{Binding FileNewCommand}"/>
<MenuItem Header="_Open" Command="{Binding FileOpenCommand}"/>
<MenuItem Header="_Save" Command="{Binding FileSaveCommand}"/>
<MenuItem Header="_Close" Command="{Binding FileCloseCommand}"/>
</MenuItem>
</Menu>
可以使用Update Controls库中的MakeCommand方法来创建所有可绑定的ICommand属性。When子句将用于启用和禁用菜单项。
public ICommand FileSaveCommand
{
get
{
// 只有当文件打开时才能保存。
return MakeCommand.When(() => _dataModel.OpenFileName != null)
.Do(() => _dataModel.LastAction = "Save");
}
}
有些菜单项不是应用程序操作,而是窗口操作。这些可以在代码后台处理。
<Separator/>
<MenuItem Header="E_xit" Click="Exit_Click"/>
private void Exit_Click(object sender, RoutedEventArgs e)
{
Close();
}
上下文敏感菜单在特定条件下出现。WPF提供了DataTrigger机制来实现这一点。DataTrigger在数据属性等于特定值时设置控件属性。在这种情况下,希望当数据属性IsFileOpen为False时,将MenuItem的Visibility属性设置为Hidden。
<Window.Resources>
<Style x:Key="VisibleWhenFileIsOpen" TargetType="MenuItem">
<Style.Triggers>
<DataTrigger Binding="{Binding IsFileOpen}" Value="False">
<Setter Property="Visibility" Value="Hidden"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
将这种样式应用到任何对这种上下文敏感的菜单上。
<MenuItem Header="_Edit" Style="{StaticResource VisibleWhenFileIsOpen}">
<MenuItem Header="Cu_t"/>
<MenuItem Header="_Copy"/>
<MenuItem Header="_Paste"/>
</MenuItem>
DataTriggers会自动重置。当IsFileOpen数据属性不再是False时,Visibility控制属性将恢复为默认的Visible。不需要为此规则创建另一个触发器。
对于最近打开的文件或当前打开的窗口,希望每个菜单项代表一个数据对象。希望将菜单绑定到一个列表。
如果直接绑定到原始数据对象,将很难在视图中获得所需的确切行为。XAML是声明式的,当数据已经处于正确的格式时,使用起来最简单。这就是视图模型的用武之地。
public class RecentFileViewModel
{
private int _index;
private string _fileName;
private IFileHandler _fileHandler;
public RecentFileViewModel(int index, string fileName, IFileHandler fileHandler)
{
_index = index;
_fileName = fileName;
_fileHandler = fileHandler;
}
public string FileName
{
get
{
return string.Format("{0} - {1}", _index + 1, _fileName);
}
}
public ICommand Open
{
get
{
return MakeCommand.Do(() => _fileHandler.Open(_fileName));
}
}
}
最近文件视图模型以适合菜单项的格式呈现文件名。它甚至添加了下划线,将基于1的索引转换为热键。
视图模型还提供了打开文件的命令。它实际上并不执行操作;它委托给文件处理程序并提供上下文。
根据最近打开的文件列表提供这些视图模型的列表。
public IEnumerable<RecentFileViewModel> RecentFiles
{
get
{
// 为每个最近打开的文件创建RecentFileViewModel。
// 视图模型服务于菜单项。
return _dataModel.RecentFiles.Select((fileName, index) =>
new RecentFileViewModel(index, fileName, this));
}
}
请注意,这种模式不适用于ObservableCollection。一旦在ObservableCollection上调用.Select(),它就不再是可观察的。这种模式只适用于Update Controls。
现在需要将MenuItems绑定到这个集合。第一反应是将父MenuItem的ItemTemplate设置为包含子MenuItem的DataTemplate。问题是DataTemplate控制的是子项的内容,而不是子项本身。因此,需要设置ItemContainerStyle,而不是ItemTemplate。
<MenuItem Header="_Recent Files" ItemsSource="{Binding RecentFiles}">
<MenuItem.ItemContainerStyle>
<Style>
<Setter Property="MenuItem.Header" Value="{Binding FileName}"/>
<Setter Property="MenuItem.Command" Value="{Binding Open}"/>
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>