Menus, part II : multi-level drop-down menu

by gleblanc 26. October 2008 14:43

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.

Tags:

Menus. Part I

by gleblanc 12. October 2008 12:54

With this post, we are starting a serie of posts dedicated to menus. In this post, we present the basis with a basic menu, though XML driven and, eventually, linked to a status bar. We'll improve it in the forthcoming posts.


Project / source code

As usual, we create a Silverlight application that will host the menu we are building here. Let's assume the application is named Menu1. Visual Studio creates a namespace for the program and, by default, this namespace is named Menu1, as the application.

Our menu is described in a XML file (Menu.xml) and this XML file is inserted as resource into the Silverlight application (Solution Explorer, Add, New element, XML file) :
 <?xml version="1.0" encoding="utf-8" ?>
 <Menu>
  <Item Id="10" Text="Monet" Help="Claude Monet : Thames at Westminster" />
  <Item Id="20" Text="Turner" Help="Joseph Turner : Oxford High Street" />
  <Item Id="30" Text="Renoir" Help="Auguste Renoir : Pont des arts, Paris" />
  <Item Id="40" Text="Pissaro" Help="Camille Pissaro : L'Hermitage" Selected="True" />
  <Item Id="50" Text="Vermeer" Help="Johannes Vermeer : View of Delft" />
 </Menu>

Each menu item must have an Id and a Text attributes and, eventually, a Help sentence (a tooltip eventually displayed in the status bar) and a Selected attribute (the menu item with a Selected attribute is displayed in red and, at program startup, a request is made to the hosting application to respond to that item, as if the user had clicked on that menu item). This makes that menu item the currently selected menu item.

Let's now create the menu, by adding a Silverlight User Control (Solution Explorer, Add, Add new element, Silverlight User Control), that we name Menu.xaml. By default, it's created in the Menu1 namespace (as the hosting program). Since we intend to create a menu that will be easily used in another hosting application, we prefer a specific namespace (here Menu) for the menu (we'll do the same later on for the status bar). This requires two changes : one in the Menu.xaml file (value of x:Class attribute changed to Menu.Menu instead of Menu1.Menu) and one in the Menu.xaml.cs file (namespace Menu instead of namespace Menu1).

In the Menu.xaml file, we delete the Width and Height attributes and force an horizontal StackPanel as container (instead of a Grid). We alse force a Background color with a gradient (it's generally considered nicer) :
 <UserControl x:Class="Menu.Menu"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    >
  <StackPanel x:Name="spMenu" HorizontalAlignment="Stretch" Height="40"
              Loaded="spMenu_Loaded" Orientation="Horizontal" >
   <StackPanel.Background>
    <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
     <GradientStop Offset="0" Color="#FEF4C1" />
     <GradientStop Offset="1" Color="#F4E186" />
    </LinearGradientBrush>
   </StackPanel.Background>
  </StackPanel>
 </UserControl>

We create a class (named MenuItem) for a menu item (Solution Explorer, Add, New element, Class), again in the Menu namespace (one line to change in the MenuItem.cs file) :
 namespace Menu
 {
  public class MenuItem
  {
   public int Id { get; set; }
   public string Text { get; set; }
   public string Help { get; set; }
   public bool Selected { get; set; }
  }
 }

In the spMenu_Loaded function, we'll open the Menu.xml file and analyze it using the Linq technology. To do so, need to add a reference (Solution Explorer, Add reference, .NET) to System.Xml.Linq. We also need to add using System.Xml.Linq; to the Menu.xaml.cs file. To analyze our XML file and create a list of MenuItem, we write in Menu.xaml.cs (remember, Help and Selected attributes could be absent : easy for Help, thanks to the null value for a string but a check is necessary for Selected - whatever the value, we consider here, for the sake of simplicity, that the item is selected if the Selected attribute is present) :
 List<MenuItem> tabItems;
 .....
 private void spMenu_Loaded(object sender, RoutedEventArgs e)
 {
  XElement xml = XElement.Load("Menu.xml");
  var listItems = from p in xml.Elements("Item")
                  select new MenuItem
                  {
                   Text = (string)p.Attribute("Text").Value,
                   Id = (int)p.Attribute("Id"),
                   Help = (string)p.Attribute("Help"),
                   Selected=p.Attribute("Selected")!=null ? true : false
                  };
  tabItems = listItems.ToList();
  .....
 }

In the menu bar, each menu item is implemented as a single-cell Grid containing a rectangle (for the color change on MouseOver), a TextBlock (for menu item text) and a vertical Line as separator. We also handle mouse events for each menu item :
 private void spMenu_Loaded(object sender, RoutedEventArgs e)
 {
  .....
  int N = tabItems.Count;
  for (int n=0; n<N; n++)
  {
   TextBlock txtItem = new TextBlock();
   txtItem.Text = tabItems[n].Text;
   txtItem.VerticalAlignment = VerticalAlignment.Center;
   txtItem.HorizontalAlignment = HorizontalAlignment.Center;
   switch (tabItems[n].Selected)
   {
    case true : txtItem.Foreground = new SolidColorBrush(Colors.Red); break;
    case false : txtItem.Foreground = new SolidColorBrush(Color.FromArgb(255, 37, 36, 85));
                 break;
   }
   Rectangle rcItem = new Rectangle(); rcItem.Fill = new SolidColorBrush(Colors.Transparent);
   rcItem.Fill = new SolidColorBrush(Colors.Transparent);
   rcItem.Width = txtItem.ActualWidth +6;
   Grid gridItem = new Grid();
   gridItem.Width = txtItem.ActualWidth+55;
   gridItem.Tag = n;

   Line liSep = new Line();
   liSep.X1 = gridItem.ActualWidth-5; liSep.Y1 = 5;
   liSep.X2 = gridItem.ActualWidth-5; liSep.Y2 = 35; liSep.StrokeThickness = 5;
   liSep.Stroke = new SolidColorBrush(Color.FromArgb(255, 255, 245, 190));

   gridItem.MouseLeave += new MouseEventHandler(c_MouseLeave);
   gridItem.MouseEnter += new MouseEventHandler(c_MouseEnter);
   gridItem.MouseLeftButtonDown += new MouseButtonEventHandler(c_MouseLeftButtonDown);

   gridItem.Children.Add(rcItem);
   gridItem.Children.Add(txtItem); gridItem.Children.Add(liSep);
   spMenu.Children.Add(gridItem);
   .....
  }
At the end of the spMenu_Loaded function, we need to warn the hosting application that the Selected menuItem (here the item with Id 40) has to be activated. To do so, we first need a class (here named MenuEventArgs) derived from EventArgs : Solution Explorer, Add, New element, Class, again in the Menu namespace. Visual Studio creates the MenuEventArgs.cs file, that contains (forgetting a few using at the beginning) :
 namespace Menu
 {
  public class MenuEventArgs : EventArgs
  {
   public int ItemId { get; set; }
  }
 }
In the Menu.xaml.cs file, we need to add a event (here named Click) to warn the hosting application that a menu item has been selected (nSelected containing the ordinal position of the menu item selected in the XML file) :
 public delegate void MenuEventHandler(object sender, MenuEventArgs e);
 public event MenuEventHandler Click;
 .....
 private void spMenu_Loaded(object sender, RoutedEventArgs e)
 {
  .....
  if (nItemSelected!=-1)
  {
   MenuEventArgs args = new MenuEventArgs(); args.ItemId = tabItems[nItemSelected].Id;
   if (Click != null) Click(this, args);
  }
 }
In the mouse event functions, we mark the MouseOver selection or inform the hosting application that a menu item was clicked :
 void c_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
 {
  Grid gridItem = sender as Grid;
  int nItem = (int)gridItem.Tag;
  MenuEventArgs args = new MenuEventArgs(); args.ItemId = tabItems[nItem].Id;
  TextBlock txtItem = gridItem.Children[1] as TextBlock;
  txtItem.Foreground = new SolidColorBrush(Colors.Red);
  if (nItemSelected!=-1) 
  {
   Grid gSel = spMenu.Children[nItemSelected] as Grid;
   TextBlock txtSelItem = gSel.Children[1] as TextBlock;
   txtSelItem.Foreground = new SolidColorBrush(Color.FromArgb(255, 37, 36, 85));
  }
  nItemSelected=nItem;
  if (Click != null) Click(this, args); 
  } 

  void c_MouseEnter(object sender, MouseEventArgs e)
  {
   Grid gridItem = sender as Grid;
   Rectangle rcItem = gridItem.Children[0] as Rectangle;
   rcItem.Fill = new SolidColorBrush(Color.FromArgb(255, 203, 203, 239));
   int nItem = (int)gridItem.Tag;
   .....
  }

  void c_MouseLeave(object sender, MouseEventArgs e)
  {
   Grid gridItem = sender as Grid;
   Rectangle rcItem = gridItem.Children[0] as Rectangle;
   rcItem.Fill = new SolidColorBrush(Colors.Transparent);
   .....
  }
Now, let's add the menu in the hosting application. In Page.xaml, we inform Visual Studio that we'll use Menu (one line to add) :
 <UserControl x:Class="Menu1.Page"
   .....
   xmlns:mnu="clr-namespace:Menu"
   .....
   >
mnu is now the prefix we decide to give to reference our menu. The user inserts for instance the menu in a grid row (in Page.xaml)  :
 <mnu:Menu x:Name="myMenu" Grid.Row="0" Click="myMenu_Click" />
And the myMenu_Click function (in Page.xaml.cs) is something like :
 private void myMenu_Click(object sender, MenuEventArgs e)
 {
  ..... // menu item clicked in e.Id
 }
And now the status bar : we create a Silverlight User Control (StatusBar.xaml), again in the Menu namespace (do not forget the namespace change in StatusBar.xaml.cs). The StatusBar.xaml file becomes :
 <UserControl x:Class="Menu.StatusBar"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      >
  <Grid x:Name="LayoutRoot" Background="White">
   <Border Background="#F4E186" >
    <TextBlock x:Name="txtToolTip" FontSize="10" Foreground="Blue"
               VerticalAlignment="Center"  />
   </Border>
  </Grid>
 </UserControl>
To make the link between the menu and the status bar, we add a property in the Menu class :
 public StatusBar SB { get; set; }
The user inserts a status bar (in Page.xaml) :
 <mnu:StatusBar x:Name="myStatusBar" Grid.Row="2" />
and force the link between the menu and the status bar (for instance in the Loaded function in Page.xaml.cs) :
 myMenu.SB = myStatusBar;

Nothing Silverlight-ish in this menu but we are now ready for more Silverlight-like menus. See you in next posts for that. 

Tags:

Powered by BlogEngine.NET 1.5.0.7
Theme by Mads Kristensen

About the author

Gerard Leblanc is the author of several books (in french) on C++, C#, .NET and Silverlight (Eyrolles, Paris as publisher). See www.gleblanc.eu as the companion web site for these books (included sample programs).
He is Microsoft MVP for Silverlight.

MVP logo