Click here to Skip to main content
15,896,329 members
Articles / Desktop Programming / WPF
Article

MenuItems: Collections within a MenuItem

Rate me:
Please Sign up or sign in to vote.
4.14/5 (5 votes)
10 Jan 2008CPOL3 min read 55.9K   735   16   3
An article on a custom control used to populate a list of values in a MenuItem.

Table of Contents

The Problem

The problem arose when I wanted to add a list of items to a MenuItem. I wanted the items to be displayed as a list, displaying the item's title. Normally you can add an ItemsSource to a MenuItem. However, once you do so, children cannot be directly added to the MenuItem. Also, it must be a child of Menu or the items in the list will be displayed as part of a sub item. So I set about creating a control, that when added to a MenuItem would display the bound ItemsSource as a list of MenuItems.

The Solution

I started out by creating a new class which inherits from MenuItem. I did this so that the control would display correctly (as a child of Menu or MenuItem). If the class did not inherit from MenuItem then a container would be displayed even if the ItemsSource was empty. I then overrode the control's Style and ControlTemplate. First I added a ListView to the ControlTemplate.

C#
<ListView x:Name="MyListView"                    
             Style="{StaticResource ListViewStyle}" >

    ...
</ListView>

This was good start. When the ItemsSource was not empty, I now had a container visible in my MenuItem. Next, I added a Style to the ListView.

C#
<Style TargetType="ListView" x:Key="ListViewStyle" >
    <Setter Property="Template" >
        <Setter.Value>
            <ControlTemplate TargetType="ListView" >
                <Grid Margin="-2,0,0,0" >              
                    <ItemsPresenter />

                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

I didn't need all the visual elements of a ListView so I overrode its ControlTemplate. I started by adding a Grid to the template. The positioning of the Grid was slightly off from other MenuItem objects. So I added a Margin with a negative offset to the left. Inside of the Grid I added an ItemsPresenter to display the items contained in the ItemsSource (the ItemsSource is set in the code-behind and is discussed below).

To make sure the ListView items are aligned correctly I added an ItemsContainerStyle.

C#
<ListView.ItemContainerStyle>
    <Style TargetType="ListViewItem" >
        <Setter Property="VerticalContentAlignment" Value="Stretch" />

        <Setter Property="HorizontalContentAlignment" Value="Stretch" />
    </Style>
</ListView.ItemContainerStyle>

Each item in the ListView needed its own template (one that would return a MenuItem with a Header appropriate to the item). Normally I would create a DataTemplate in XAML for the ListView. However, I wanted the ability to set the Header via a property in the control. In order to do this I needed to dynamically set the binding path for the Header property. Since I could not do this successfully in XAML I turned to the code-behind.

In the Loaded event of my control I used the VisualTreeHelper to find the template generated ListView.

C#
if ((VisualTreeHelper.GetChildrenCount(this) > 0) && (mListView == null))
{
    mListView = (ListView)VisualTreeHelper.GetChild(VisualTreeHelper.GetChild(this,
        0), 1);

Once I had the ListView I created a String representation of the DataTemplate I needed for the items. While constructing the String I inserted Header and IsChecked binding paths based on the properties HeaderBindingPath and IsCheckedBindingPath respectively.

C#
StringBuilder sb = new StringBuilder();
sb.AppendLine(@"<DataTemplate 
    xmlns=<a href="%22%3C/span">http://schemas.microsoft.com/winfx/2006/xaml/presentation">http://schemas.microsoft.com/winfx/2006/xaml/presentation</a>
    xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"">");

sb.AppendLine(@"<MenuItem");
if (!String.IsNullOrEmpty(HeaderBindingPath))
    sb.AppendLine(@"Header=""{Binding Path=" + HeaderBindingPath + @"}""");
sb.AppendLine(@"Background=""{Binding RelativeSource={RelativeSource Mode=FindAncestor,
    AncestorType={x:Type MenuItem}}, Path=Background}""");
sb.AppendLine(@"IsCheckable=""{Binding RelativeSource={RelativeSource Mode=FindAncestor,
    AncestorType={x:Type MenuItem}}, Path=IsCheckable}""");

if (!String.IsNullOrEmpty(IsCheckedBindingPath))
    sb.AppendLine(@"IsChecked=""{Binding Path=" + IsCheckedBindingPath + @"}""");
sb.AppendLine(@"/>");
sb.AppendLine(@"</DataTemplate>");

object o = XamlReader .Load(new XmlTextReader(new StringReader(sb.ToString())));

I used a XamlReader object to generate the DataTemplate based on the String. Then I set the ItemTemplate and finally the ItemsSource of the ListView.

C#
if (o.GetType().Equals(typeof(DataTemplate)))
{
    mListView.ItemTemplate = (DataTemplate)o;
    mListView.ItemsSource = this.ItemsSource; // add the itemtemplate first, otherwise
                                              //there will be a visual child error
}

Using the Code

In the accompanying demo I created a class named Widget. Then I created an ObservableCollection containing Widget objects, named Widgets. I added a few Widget objects to the collection.

C#
for (int i = 0; i < 4; i++)
{
    Widgets.Add(new Widget(String.Format("Widget {0}", i)));
}

In Window1.xaml I added a MenuItem with one MenuItem and two MenuItems. Both MenuItems receive the collection Widgets as an ItemsSource.

C#
<Menu DockPanel.Dock="Top" >
    <MenuItem Header="Items" >
        <MenuItem Header="Single Item" />

        <con:MenuItems ItemsSource="{Binding Widgets}"
                          HeaderBindingPath="Title"
                          TopSeparatorVisibility="Visible"
                          BottomSeparatorVisibility="Visible" />
        <con:MenuItems ItemsSource="{Binding Widgets}"
                          HeaderBindingPath="Title"
                          IsCheckable="True" 
                          IsCheckedBindingPath="IsActive" />
    </MenuItem>
</Menu>

In the first MenuItems control I set the TopSeparatorVisibility and BottomSeparatorVisibility to Visible. This, as the names describe, display a separator at the top and bottom of the MenuItems control. In the second MenuItems control I set the IsCheckable and IsCheckableBindingPath. IsCheckable makes all the displayed MenuItem objects within the MenuItems control checkable. The IsCheckableBindingPath property sets the binding path for the IsChecked property. In the example above the IsActive property of a Widget is bound to the IsChecked property of the template generated MenuItem. Since a Widget has a default IsActive equal to true, all of the items in the MenuItems will be displayed initially with a checkmark.

History

  • Initial Version (January 2008)
  • 01-14-2008 - Updated the section, The Problem, based on a posted question

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Unknown
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralArrow keys do not navigate menu items. Pin
Peter Rilling21-Apr-09 12:22
Peter Rilling21-Apr-09 12:22 
QuestionHowever, the MenuItem cannot have any children. Pin
User 27100912-Jan-08 3:14
User 27100912-Jan-08 3:14 
AnswerRe: However, the MenuItem cannot have any children. Pin
Tony Gordon14-Jan-08 10:32
Tony Gordon14-Jan-08 10:32 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.