Data bound menus

I had a twitter conversation recently with Edward Tanguay about his StackOverflow question on binding menus to an Observable Collection. Naturally, I locked on to the ObservableCollection part and didn't get the true gist of his question. I took a step back, reread his question and the provided answers, and finally caught on to the problem. I wish I could upvote Kent Boogaart's answer more than once, because it set me on a trail of discovery.

This example uses Update Controls. Download the example source code: databoundmenus.zip.

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. Update Controls can help with all of these things.

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. Use Command Binding for all menu items.

<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>

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
    {
        // We can only save a file when one is open.
        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
    {
        // Create a RecentFileViewModel for each recent file.
        // The view model serves the menu item.
        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 to simple, you won't be able to handle the more interactive requirements. If you choose something too complex, you loose tool support and put too much of your design in code. With this combination of techniques, you can create interactive menus with ease.

One Response to “Data bound menus”

  1. XAML Templates Says:

    Hi, at http://www.xamltemplates.net/ you can see styles/themes for all the WPF and Silverlight controls, check it out.

Leave a Reply

You must be logged in to post a comment.