Introduction
After receiving some very constructive feedback from Justin Angel on my CoolMenu (View Demo Here) control, I decided to refactor the control to allow for a more flexible developer experience. I wanted to document my experience with this so that others might benefit. This series will walk through the various refactorings that I have been working on to make this control better.
Inheriting from ItemsControl
One of the biggest issues with the control was the fact that it did not inherit from an ItemsControl. The ItemsControl, according to the MSDN documentation, is used to present a collection of items. That’s a pretty powerful statement. It is fairly common that we need to display a collection of items, so why not take advantage of what this class has to offer? When I first set out to build this control, I naïvely inherited from Control and consequently ended up writing a lot of extra code to handle my items collection. This worked, but the syntax was ugly and furthermore, the control did not support DataTemplates. Before refactoring, the XAML syntax for adding items to was similar to the following:
<sc:CoolMenu Height="130">
<sc:CoolMenu.Items>
<sc:CoolMenuItemCollection>
<sc:CoolMenuItem>
<sc:CoolMenuItem.Content>
<Image Source="/Images/box.png" Margin="5" />
</sc:CoolMenuItem.Content>
</sc:CoolMenuItem>
<sc:CoolMenuItem>
<sc:CoolMenuItem.Content>
<Image Source="/Images/calc.png" Margin="5" />
</sc:CoolMenuItem.Content>
</sc:CoolMenuItem>
<sc:CoolMenuItem>
<sc:CoolMenuItem.Content>
<Image Source="/Images/coffee.png" Margin="5" />
</sc:CoolMenuItem.Content>
</sc:CoolMenuItem>
</sc:CoolMenuItemCollection>
</sc:CoolMenu.Items>
</sc:CoolMenu>
This is terrible and it hurts my eyes just looking at it.
Now, it is worth mentioning that the CoolMenuItem control serves an important purpose. At the very least, this control allows us to achieve some level of consistency when someone goes an adds items to our control. As a control developer, I have no idea what you intend on cramming into my collection and frankly, I shouldn’t have to care. So we have conflicting goals of making sure that each of our items are wrapped inside of a CoolMenuItem at the same time, we want cleaner syntax.
Luckily for us, the ItemsControl has a mechanism for this! The Item Container will automatically generate a CoolMenuItem for each the items defined in the template (Dr. WPF has a great article on this topic called ItemsControl: ‘I’ is for Item Container). The only thing we have to do is provide the base ItemsControl with an instance of our Item Container. In the CoolMenu class I added syntax that looks like the following:
protected override DependencyObject GetContainerForItemOverride()
{
return new CoolMenuItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return (item is CoolMenuItem);
}
The first override returns a new instance of our Item Container. This method is called when the base ItemsControl needs a new Item Container for an item. The second override is called when the base ItemsControl is inspecting an item to determine if the item is already a CoolMenuItem. This is possible if the user of my control decides to be explicit. With those small changes, we can now declare our items as follows:
<sc:CoolMenu Height="130">
<Image Source="/Images/box.png" Margin="5" />
<Image Source="/Images/calc.png" Margin="5" />
<Image Source="/Images/coffee.png" Margin="5" />
</sc:CoolMenu>
This is beautiful!
Note: I believe that the Item Container control must derive from ContentControl. In my experience, simply deriving from Control will prevent the ItemsControl from displaying correctly.
One other benefit from this particular refactoring is that we can now use data templates:
<sc:CoolMenu x:Name="CoolMenu2">
<sc:CoolMenu.ItemTemplate>
<DataTemplate>
<Image Source="{Binding Img}" Margin="5" Height="30" Width="30" />
</DataTemplate>
</sc:CoolMenu.ItemTemplate>
</sc:CoolMenu>
CoolMenu2.ItemsSource = new List<ImageContainer>
{
new ImageContainer { Img = "/Images/box.png"},
new ImageContainer { Img = "/Images/calc.png"},
new ImageContainer { Img = "/Images/coffee.png"},
};
Cool huh?
By default, the ItemsControl uses a StackPanel with vertical orientation to display the items. The CoolMenu control uses a StackPanel with horizontal orientation to display items. This means that we have to override the ItemsPanel property. So, in the control’s default template, you will find the following:
<Style TargetType="sc:CoolMenu">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="sc:CoolMenu">
<StackPanel x:Name="RootElement" Orientation="Horizontal" Background="Transparent" HorizontalAlignment="Center">
<ItemsPresenter />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The above template displays each of our items using a StackPanel with Horizontal orientation. Easy stuff.
After refactoring the CoolMenu to inherit from ItemsControl, I encountered a few other issues. One particular issue I ran into was keeping up with the actual items after they were added or removed. The next article in this series will explain how I approached this.
I haven’t checked any of this code in yet, but as soon as it is ready, you will be able to find it in the Silverlight Contrib project on CodePlex.