Introduction
There is not just one way to make a WPF menu work. There are several techniques that you have to combine. Much of an application's menu is static. Some of it is context sensitive, and only appears in certain conditions. And in a few places, the menu items are dynamic, like recently opened files or currently open windows. This article demonstrates techniques for each case.
The sample code uses and includes an open source library called Update Controls that helps with data binding. Although the sample code depends upon it, these techniques themselves will work without Update Controls, except where otherwise stated.
Declarative Menu Structure
For the static menus, you want to declare the structure entirely in XAML. This gives you the greatest design/code separation, and the best tool support. Bind each of the menu items to an ICommand
property in your view model.
<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>
You can use update controls MakeCommand
to create all of the bindable ICommand
properties. The When
clause will enable and disable the menu item.
public ICommand FileSaveCommand
{
get
{
return MakeCommand
.When(() => _dataModel.OpenFileName != null)
.Do(() => _dataModel.LastAction = "Save");
}
}
Some of the menu items are not application actions, but window actions. These can be handled in code-behind.
<Separator/>
<MenuItem Header="E_xit" Click="Exit_Click"/>
private void Exit_Click(object sender, RoutedEventArgs e)
{
Close();
}
Context Sensitive Menus
You want context sensitive menus to appear under certain conditions. WPF has a mechanism for that: the DataTrigger. A DataTrigger
sets a control property when a data property is equal to a specific value. In this case, we want to set a MenuItem
's Visibility
property to Hidden
when the data property IsFileOpen
is False
.
<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>
We apply this style to any menu that is sensitive to this context.
<MenuItem Header="_Edit" Style="{StaticResource VisibleWhenFileIsOpen}">
<MenuItem Header="Cu_t"/>
<MenuItem Header="_Copy"/>
<MenuItem Header="_Paste"/>
</MenuItem>
DataTriggers
automatically reset. When the IsFileOpen
data property is no longer False
, the Visibility
control property will go back to the default Visible
. There is no need to create another trigger for that rule.
Dynamic Menus
For recently opened files or currently open windows, you want each menu item to represent a data object. You want to bind the menu to a list.
If you bind to the raw data objects, you will have a hard time getting exactly the behavior that you want in the view. XAML is declarative, and is easiest to use when the data is already in the right format. That's where the View Model comes in.
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));
}
}
}
The recent file view model presents the file name in a format suitable for the menu item. It even adds the underscore to turn the 1-based index into a hot key.
The view model also provides the command to open the file. It doesn't actually perform the operation; it delegates to a file handler and provides the context.
We provide a list of these view models based on the list of recently opened files.
public IEnumerable<RecentFileViewModel> RecentFiles
{
get
{
return _dataModel.RecentFiles
.Select((fileName, index) =>
new RecentFileViewModel(index, fileName, this));
}
}
Please note that this pattern does not work with ObservableCollection
. Once you call .Select()
on an ObservableCollection
, it is no longer observable. This pattern only works with Update Controls.
Now we need to bind MenuItems
to this collection. My first instinct was to set the ItemTemplate
of the parent MenuItem
to a DataTemplate
containing a child MenuItem
. The problem with that is that a DataTemplate
controls the content of the child item, not the child item itself. So instead of setting ItemTemplate
, you need to set the ItemContainerStyle
.
<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>
A WPF menu should not be defined using just one technique. If you choose something too simple, you won't be able to handle the more interactive requirements. If you choose something too complex, you lose tool support and put too much of your design in code. With this combination of techniques, you can create interactive menus with ease.
History
- 3rd July, 2009: Initial post