65.9K
CodeProject is changing. Read more.
Home

MenuItems: Collections within a MenuItem

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.14/5 (5 votes)

Jan 10, 2008

CPOL

3 min read

viewsIcon

56486

downloadIcon

738

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.

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

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

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

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.

StringBuilder sb = new StringBuilder();
sb.AppendLine(@"<DataTemplate 
    xmlns=%22%3C/span">http://schemas.microsoft.com/winfx/2006/xaml/presentation">http://schemas.microsoft.com/winfx/2006/xaml/presentation
    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.

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.

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.

<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