In this post, we will implement a collapsible menu, animated of course. It's amazing to see how easily any .NET educated programmer (even WinForms only) can add functionnalities to Silverlight applications.
Project / source code
Our collapsible menu is declared in a XML file (
Menu.xml, inserted as a resource) :
<?xml version="1.0" encoding="utf-8" ?>
<Items>
<Item Text="China" Id="10" />
<Item Text="USA" Id="30" >
<Item Text="Grand Canyon" Id="31" />
<Item Text="Yosemite" Id="32" />
.....
</Item>
<Item Text="Egypt" Id="40" />
.....
</Items>
To build the collapsible menu in our Silverlight application, we first add a Silverlight user control, named CollapsibleMenu (the CollapsibleMenu.xaml and CollapsibleMenu.xaml.cs files are then created by Visual Studio). We force the namespace named glMenu (gl for our own initials). In the CollapsibleMenu.xaml file, we suppress Width and Height properties. Business as usual for a user control.
Our collapsible menu is implemented as an horizontal StackPanel, with two elements : one (a vertical StackPanel with menu items) for the menu itself (horizontally collapsible) and a bordered TextBlock (22 pixels wide) for the tab (always visible, with "Menu" written vertically).
<StackPanel Orientation="Horizontal" >
<StackPanel x:Name="spMenu" Orientation="Vertical" ..... >
.....
</StackPanel>
<Border CornerRadius="0,15,15,0" Width="22" VerticalAlignment="Top" ..... >
<TextBlock x:Name="tab" Text="Menu" TextWrapping="Wrap" ...... />
</Border>
</StackPanel>
For the TextBlock (named tab), we also specify some font properties (FontFamily, FontSize, etc.) and handle three events : MouseEnter and MouseLeave but also MouseMove (leaving the tab thru the left or right sides must have a different effect : keeping the menu as it is or collapsing it).
The menu itself is a vertical StackPanel (with an initial width of 0 pixel) that will be populated by program in the function that handles the Loaded event :
<StackPanel x:Name="spMenu" Orientation="Vertical" Width="0" Loaded="spMenu_Loaded"
MouseLeave="spMenu_MouseLeave" >
.....
</StackPanel>
We handle the Loaded event (to initialize the menu from the XML file) and the MouseLeave event (to progressively reduce its width to 0). Inside this StackPanel, we define an animation (to horizontally and progressively expand or collapse the menu in one second) :
<StackPanel x:Name="spMenu" ..... >
<StackPanel.Resources>
<Storyboard x:Name="stb" >
<DoubleAnimation x:Name="da" Storyboard.TargetName="spMenu"
Storyboard.TargetProperty="Width" Duration="0:0:1" />
</Storyboard>
</StackPanel.Resources>
</StackPanel> Q
In the Loaded function, we read the XML file and prepare the menu (first as a list of MenuItem objects) and then (in the PrepareMenu function) as a set of XAML tags :
List<MenuItem> lstMenuItems;
private void spMenu_Loaded(object sender, RoutedEventArgs e)
{
lstMenuItems = new List<MenuItem>();
XElement xmlRoot = XElement.Load("Menu.xml");
getMenuItems(xmlRoot);
PrepareMenu();
}
The MenuItem class (also created in the glMenu namespace) is
public class MenuItem
{
public MenuItem(string aText, int aId, int aLevel, int acountSubItems)
{
Text = aText; Id = aId; Level = aLevel; countSubItems = acountSubItems;
}
public string Text {get; set;}
public int Id {get; set;}
public int Level { get; set; }
public int countSubItems {get; set;}
}
Level is 0 for a main menu item (for instance USA) and 1 for a submenu item (for instance Yosemite). The field countSubItems contains the number of sub-items for a main menu item (5 for Italy).
Linq provides an incomparable help in the task of analyzing the XML file (do not forget to add a reference to System.Xml.Linq) :
void getMenuItems(XElement xml)
{
var lstItems = from p in xml.Elements("Item") select p;
foreach (var el in lstItems)
{
string Text = el.Attribute("Text").Value;
int Id; Int32.TryParse(el.Attribute("Id").Value, out Id);
if (el.HasElements == false)
lstMenuItems.Add(new MenuItem(Text, Id, 0, 0));
else
{
// this main menu item has one or several sub-menu items
lstMenuItems.Add(new MenuItem(Text, Id, 0, el.Elements("Item").Count()));
var subItems = from pSub in el.Elements("Item") select pSub;
foreach (var sub in subItems)
{
Text = sub.Attribute("Text").Value;
Int32.TryParse(sub.Attribute("Id").Value, out Id);
lstMenuItems.Add(new MenuItem(Text, Id, 1, 0));
}
}
}
}
The menu is a vertical StackPanel. Each main menu item is a Grid. Each sub-menu item is also a Grid but included in a vertical StackPanel containing all sub-menu items :
void PrepareMenu()
{
int N = lstMenuItems.Count;
for (int n=0; n<N; n++)
{
MenuItem mi = lstMenuItems[n];
PrepareMainMenuItem(mi);
if (mi.countSubItems > 0)
{
PrepareSubMenuItems(mi);
n += mi.countSubItems;
}
}
spMenu.Width = 0; // menu initially fully collapsed
..... // add storyboard to main items with submenu
}
At the end of the PrepareMenu function, we also need to add a Storyboard (to vertically and progressively expand or callapse a sub-menu). This is only necessary for main menu items that have sub-menu items.
In the PrepareMainMenuItem function, we prepare the XAML for a specific main menu item : a Grid (with a linear gradient brush for Background) plus a TextBlock for the text. We handle the MouseLeftButtonDown (to open the associated sub-menu but it could be to activate a main menu item without sub-menu).
In the PrepareSubMenuItems function, we prepare the XAML for the sub-menu of a specific main menu item : a vertical StackPanel containing as many Grid as they are sub-menu items in this main menu item). This Grid is made of a Rectangle (for Background and Stroke) and a TextBlock.
As we did in previous posts, our collapsible menu is able to generate the Click event (the MenuEventArgs is just a class derived from EventArgs, plus the Id field as complementary information) :
public delegate void MenuEventHandler(object sender, MenuEventArgs e);
public event MenuEventHandler Click;
....
MenuEventArgs args = new MenuEventArgs(); args.itemId = Id;
if (Click != null) // if Click event handled by the user
Click(this, args);
In Page.xaml, the user specifies the collapsible menu with (do not forget to put it on top) :
<UserControl .....
xmlns:gl="clr-namespace:glMenu"
..... >
<Grid x:Name="LayoutRoot" Background="White">
<Grid x:Name="menuGrid" Canvas.ZIndex="99" >
<gl:CollapsibleMenu x:Name="menu" VerticalAlignment="Top" Margin="0, 20, 0, 0"
Click="myMenu_Click" />
</Grid>
<Grid ..... > here the main grid for the Silverlight application
.....
</Grid>
</Grid>
That's all, folks !