Part 1: Inheriting from ItemsControl
An ItemContainerGenerator is a class that generates the containers for each of the items in an ItemsControl. At least, that is how things are in WPF. In Silverlight, this class doesn’t exist. That’s too bad because there are some methods that we really need. Most importantly, we need to be able to capture a container for any given item index via the ContainerFromIndex and IndexFromContainer methods.
I did some research and found that the Silverlight Toolkit team has already encountered the same issue in their TreeView control. The Silverlight Toolkit team solved the problem by building a ItemContainerGenerator implementation. I am assuming this class will eventually make it into the core product, but until then, we can either use this implementation or build our own.
I really want to avoid adding the Silverlight Toolkit as a dependency to Silverlight Contrib, so it looks like we will need to craft our own implementation of the ItemContainerGenerator for now. Maybe some day in the future we can refactor the CoolMenu control to use a native implementation. Using the Silverlight Toolkit ItemContainerGenerator as a guide, I built a custom generator for the CoolMenu control. For simplicity, I only added the methods I needed to make the control work. The entire generator can be found below:
public class CoolMenuItemContainerGenerator
{
private Panel m_itemsHost;
private readonly IDictionary<DependencyObject, object> m_itemContainer;
public CoolMenuItemContainerGenerator()
{
m_itemContainer = new Dictionary<DependencyObject, object>();
}
internal void ClearContainerForItemOverride(DependencyObject element, object item)
{
m_itemContainer.Remove(element);
}
internal void PrepareContainerForItemOverride(DependencyObject element, object item)
{
m_itemContainer[element] = item;
}
internal Panel ItemsHost
{
get
{
if(m_itemsHost == null)
{
if(m_itemContainer.Count <= 0)
return null;
DependencyObject container = m_itemContainer.First().Key;
m_itemsHost = VisualTreeHelper.GetParent(container) as Panel;
}
return m_itemsHost;
}
}
public DependencyObject ContainerFromIndex(int index)
{
Panel host = ItemsHost;
if(host == null || host.Children == null || index < 0 || index >= host.Children.Count)
return null;
return host.Children[index];
}
public int IndexFromContainer(DependencyObject container)
{
if(container == null)
throw new ArgumentException("container");
UIElement element = container as UIElement;
if (element == null)
return -1;
Panel host = ItemsHost;
if(host == null || host.Children == null)
return -1;
return host.Children.IndexOf(element);
}
public ReadOnlyCollection<DependencyObject> GetContainerList()
{
List<DependencyObject> containers = new List<DependencyObject>(m_itemContainer.Keys);
return new ReadOnlyCollection<DependencyObject>(containers);
}
}
As I mentioned earlier, this class is used by the ItemsControl to get the index of a particular item. This is important because for any selected item, we need to change the size of its immediate neighbors.
As the mouse enters each item, we obtain the index of the selected item using the helper methods in the ContainerGenerator. This index is then passed back to the menu control which in turn applies the appropriate behaviors to each item. This is really just the mediator pattern applied.
// Inside the CoolMenuItem class
private void CoolMenuItem_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
{
CoolMenu cm = this.ParentItemsControl as CoolMenu;
int index = CoolMenu.GetGenerator(cm).IndexFromContainer(this);
cm.OnItemHover(index);
}
// Inside the CoolMenu class
internal void OnItemHover(int selectedIndex)
{
for (int i = 0; i < this.Items.Count; ++i)
{
var content = m_generator.ContainerFromIndex(i) as CoolMenuItem;
// No items selected.
if (selectedIndex == -1)
{
this.CoolMenuBehavior.ApplyHoverBehavior(-1, content);
continue;
}
this.CoolMenuBehavior.ApplyHoverBehavior(Math.Abs(i - selectedIndex), content);
}
}
In the next part of this series, I will explain how the CoolMenu behaviors work. These behaviors allow you to change how the menu functions when a user hovers their mouse over an item or clicks on an item. With very little additional effort, one could even write a new custom behavior. See you next time!