Table of Contents
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
.
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=<a href="%22%3C/span">http://schemas.microsoft.com/winfx/2006/xaml/presentation">http:
xmlns:x=""http:
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;
}
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.
- Initial Version (January 2008)
- 01-14-2008 - Updated the section, The Problem, based on a posted question
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.