In this post, we will implement a multi-level drop down menu, ie eventually with any number of sub-menus. The drop-down menu presented here is XML driven, though without animation, as possible with Silverlight. We'll add animation effects in the forthcoming post.
Project / source code
The art-work initially displayed is "Tomorrow" from Jean-Yves Tanguy, a French painter who became an American citizen.
Our menu is XML-driven, as a set of
Item tags. An
Item tag can itself contain any number of
Item tags (corresponding to any number of sub-menus and any number of sub-menu items) :
<?xml version="1.0" encoding="utf-8" ?>
<Menu>
<Item Id="10" Text="Antarctica" />
<Item Id="20" Text="Europe" >
<Item Id="201" Text="Italy" >
<Item Id="2011" Text="Florence" />
.....
</Item>
<Item Id="202" Text="France" >
<Item Id="2021" Text="Paris" />
<Item Id="2022" Text="French riviera" >
<Item Id="20221" Text="Nice" />
<Item Id="20222" Text="Monaco" >
<Item Id="202221" Text="Monaco" />
<Item Id="202222" Text="Monte-Carlo" />
</Item>
<Item Id="20223" Text="Saint-Jean Cap Ferrat" />
</Item>
<Item Id="2023" Text="Provence" />
</Item>
.....
<Item Id="30" Text="America" >
.....
</Item>
.....
<Item Id="50" Text="Oceania" />
</Menu>
For each menu item, you need to provide an Id value as well as a Text. A Click event is raised whenever the user clicks on a menu item (the Id can then be retrieved from the second parameter of the event handling function).
As usual, we create a Silverlight application that will host the drop-down menu we are building here. Let's assume the application is named UseDropDownMenu. Visual Studio creates a namespace for the program and, by default, this namespace is named UseDropDownMenu, as the application. We first create a DropDownMenu XAML file and we specify glMenu as namespace (thus, as seen in the previous post, one change in the xaml file and another one in the xaml.cs file, these two files being created by Visual Studio).
In the hosting Silverlight application, we need to reserve space for the main menu (top horizontal bar, whose height is 30 pixels). The menu part must remain on top (this is why the Canvas.ZIndex attribute, applicable to a Grid container, is given a large value) :
<UserControl x:Class="UseDropDownMenu.Page"
.....
xmlns:mnu="clr-namespace:glMenu"
..... >
<Grid x:Name="LayoutRoot" Background="White">
<Grid x:Name="menuGrid" Canvas.ZIndex="99" >
<mnu:DropDownMenu x:Name="myMenu" Click="myMenu_Click" />
</Grid>
<Grid x:Name="g">
..... user grid here
</Grid>
</Grid>
</UserControl>
In the DropDownMenu.xaml file, we give the XAML description of the main menu bar (implemented as an horizontal StackPanel). Sub-menus (that are vertical StackPanel) will be dynamically added, as needed (and later on removed) :
<UserControl x:Class="glMenu.DropDownMenu"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<Grid x:Name="menuGrid" HorizontalAlignment="Stretch" VerticalAlignment="Top" >
<StackPanel x:Name="spMainMenu" Loaded="spMainMenu_Loaded" Orientation="Horizontal"
Height="30" >
<StackPanel.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="#FEF4C1" />
<GradientStop Offset="1" Color="#F4E186" />
</LinearGradientBrush>
</StackPanel.Background>
</StackPanel>
</Grid>
</UserControl>
Thanks to Linq, it's easy to retrieve the
Item children (just the children, not the grand-children) of a specific
Item (an
XElement within the XML file) :
List<MenuItem> getMenuItems(XElement xml)
{
var lstItems = from p in xml.Elements("Item")
select new MenuItem
{
Text = (string)p.Attribute("Text").Value,
Id = (int)p.Attribute("Id"),
hasSubMenu = p.HasElements,
XElem = p
};
return lstItems.ToList();
}
MenuItem is a class created to hold attributes of a menu item.
Menu is another class created to hold attributes of a menu (main menu or sub-menu). Menus displayed at one time are kept in a list of menus :
List<Menu> lstMenus = new List<Menu>();
The main menu is created at
Loaded time while sub-menus are created (and removed) as needed. A menu is first created in memory (each menu item is implemented as a Grid containing a rectangle - to mark MouseOver - and a TextBlock for the Text) :
void PrepareMenuPresentation(Menu mnu)
{
.....
int N = mnu.lstMenuItems.Count;
for (int n = 0; n < N; n++)
{
TextBlock txtItem = new TextBlock();
.....
Rectangle rcItem = new Rectangle();
......
Grid gridItem = new Grid();
.....
gridItem.MouseLeave += new MouseEventHandler(item_MouseLeave);
gridItem.MouseEnter += new MouseEventHandler(item_MouseEnter);
gridItem.MouseLeftButtonDown += new MouseButtonEventHandler(item_MouseLeftButtonDown);
gridItem.MouseMove += new MouseEventHandler(item_MouseMove);
gridItem.Children.Add(rcItem); gridItem.Children.Add(txtItem);
mnu.StackPan.Children.Add(gridItem);
}
.....
}
The mouse events are handled to control navigation from menu items to menu items (and, remember, sub-menus are dynamically added or removed).
Our code (300 lines) is not that long and complex but it would be too annoying - and useless - to explained it here line by line. Moreover, I tried to document the code as well as I could.