Table of Contents
Introduction
Last year I introduced an article presenting Silverlight Menu,
which was an attempt to create a useful control for the community. Despite my efforts to enhance the control with nice features, the feedbacks
I received usually complained about the lack of more flexible customization, such as more robust templating, styling, and commanding.
Now the time for the new Silverlight Menu 4U has come. After browsing through the old code for some time (to evaluate if I could reuse it),
I realized how monolithic the code was. The menu levels were made of stack panels instead of list boxes, and it would require painful efforts to
make it flexible. I decided to rewrite it from scratch, and here are the results.
In this article, I will present you two main walkthroughs: first, and more important, is the guide for Silverlight Menu 4U users, where I'll
explain how to implement different kinds of menus, from basic to advanced usage. The second part will approach the making of the control, dissecting
its parts, and showing how and why they were made.
System Requirements
The following software are needed for running Silverlight Menu 4U provided with this article:
Also, you may like to download Microsoft Expression Blend
for styling and templating Silverlight Menu 4U. Please pay attention to the Blend requirements before installing it.
Menu 4U How-To
A Simple Example
The picture above shows the default look of Silverlight Menu 4U. Notice it's a drop down style, much like Visual Studio 2010.
Let's see how to implement it in your project:
First, add the following namespace to your XAML:
<navigation:Page x:Class="Menu4UDemo.Views.Basic"
...
xmlns:navigation="clr-namespace:System.Windows.Controls;
assembly=System.Windows.Controls.Navigation"
...
>
Next, add SLMenu
to your XAML. Please keep in mind that subscribing the MenuItemClick
is optional:
<ctrl:SLMenu x:Name="mnu" MenuItemClick="mnu_MenuItemClick"/>
Very simple, isn't it? Remember that such simplicity means that the menu is assuming the default settings.
Now we go to the code-behind and provide the DataSource
property, which is a hierarchical structure containing all of the menu items of Silverlight Menu 4U:
public Basic()
{
InitializeComponent();
List<object> menuItems = MenuHelper.CreateSimpleMenu();
mnu.DataSource = menuItems;
}
For the demo, I put the menu item creation itself in a separate helper class:
public static List<object> CreateSimpleMenu()
{
List<object> menuItems = new List<object>();
var i1 = new MenuItem("Item 1");
var i11 = new MenuItem("Item 1.1");
var i12 = new MenuItem("Item 1.2");
var i13 = new MenuItem("Item 1.3");
i1.Children.Add(i11);
i1.Children.Add(i12);
i1.Children.Add(i13);
var i131 = new MenuItem("Item 1.3.1");
var i132 = new MenuItem("Item 1.3.2");
i13.Children.Add(i131);
i13.Children.Add(i132);
var i2 = new MenuItem("Item 2");
var i21 = new MenuItem("Item 2.1");
var i22 = new MenuItem("Item 2.2");
var i23 = new MenuItem("Item 2.3");
i2.Children.Add(i21);
i2.Children.Add(i22);
i2.Children.Add(i23);
var i3 = new MenuItem("Item 3");
var i31 = new MenuItem("Item 3.1");
var i32 = new MenuItem("Item 3.2");
var i33 = new MenuItem("Item 3.3");
i3.Children.Add(i31);
i3.Children.Add(i32);
i3.Children.Add(i33);
menuItems.Add(i1);
menuItems.Add(i2);
menuItems.Add(i3);
return menuItems;
}
Or, if you prefer, you could add the items directly to the XAML, provided that you use a root item of the MenuItem
type to
contain the first level items (that is the only use of this root item).
<!---->
<ctrl:SLMenu x:Name="mnu2" Grid.Row="1" MenuItemClick="mnu_MenuItemClick">
<!---->
<ctrl:MenuItem>
<ctrl:MenuItem Text="File">
<ctrl:MenuItem Text="New File"/>
<ctrl:MenuItem Text="Open File">
<ctrl:MenuItem Text="1. Demo.txt"/>
<ctrl:MenuItem Text="2. App.config"/>
</ctrl:MenuItem>
<ctrl:MenuItem Text="Save File"/>
<ctrl:MenuItem Text="Exit"/>
</ctrl:MenuItem>
<ctrl:MenuItem Text="Edit">
<ctrl:MenuItem Text="Cut"/>
<ctrl:MenuItem Text="Copy"/>
<ctrl:MenuItem Text="Paste"/>
</ctrl:MenuItem>
<ctrl:MenuItem Text="View">
<ctrl:MenuItem Text="Zoom"/>
<ctrl:MenuItem Text="Full Screen"/>
</ctrl:MenuItem>
</ctrl:MenuItem>
</ctrl:SLMenu>
As you must be realizing now, the MenuItem
class contains the basic information for the menu item. We'll be dealing with the MenuItem
class internals
later on, in the "Under the Hood" section. For now, it's enough to point out that each menu item has two important properties: Text
and Children
, which is a list of MenuItem
instances. This describes a hierarchical structure that Silverlight Menu 4U
will later render as separate panels linked by each of the parent MenuItem
s.
Back to the code-behind class: now we wire up the MenuItemClick
event to the code below so that we know when a user clicks a MenuItem
:
private void mnu_MenuItemClick(object sender, MenuItemEventArgs args)
{
var item = (MenuItem)args.MenuItem;
if (item.Children.Count() == 0)
MessageBox.Show(string.Format("You cliked: {0}", item.Text));
}
Notice that the line if (item.Children.Count() == 0)
dismisses all the parent menu items. This is so because
you usually wouldn't care whether the users clicked a parent menu item. But you surely can remove this line if you wish.
Docking
For the Docking feature demonstration, we'll be using a different DataSource
resembling the Visual Studio 2010 menu. The entire listing for the menu items is quite large,
so we'll skip it here. That being said, we just assume that the CreateVSMenu
helper function will do this job for us.
The code below shows that four different instances of SilverlightMenu4U
are provided the same DataSource
.
This is so because we want to demonstrate how the same menu data behaves under different docking circumstances:
public partial class Docking : Page
{
public Docking()
{
InitializeComponent();
mnu1.DataSource =
mnu2.DataSource =
mnu3.DataSource =
mnu4.DataSource = MenuHelper.CreateVSMenu();
}
...
Now let's take a look at the menu docked at the top of the page. The four different instances of the menu are displayed in the same page of our demo
application, so there are some attached properties such as Grid.Row
that will be set just for allowing the four menus to be rendered together.
<ctrl:SLMenu x:Name="mnu1"
Grid.Column="1" Grid.ColumnSpan="1"
Grid.Row="0" Grid.RowSpan="3"
HorizontalAlignment="Stretch"
MenuItemClick="mnu_MenuItemClick"/>
The image above shows our already familiar menu. Since the Dock
property has the default value of Top
, in this case
it just doesn't need to be set.
Now let's take a look at the menu with the Dock
property set to Left
:
<ctrl:SLMenu x:Name="mnu2"
Grid.Column="0" Grid.ColumnSpan="3"
Grid.Row="1" Grid.RowSpan="1"
TabTemplate="{StaticResource secondLevelHeaderLeftTemplate}"
Dock="Left"
MenuItemClick="mnu_MenuItemClick"/>
Besides the Dock
property, there is another remarkable difference here: the TabTemplate
property.
TabTemplate
is the tab just above the second level menu. In this case, the "View" menu item. If you want a dock different from Top
, you must
adjust this property to work with your menu:
<DataTemplate x:Key="secondLevelHeaderLeftTemplate">
<Grid Margin="0,-2,0,-2" Height="25" Width="59">
<Grid.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#E8EBED" Offset="0"/>
<GradientStop Color="#E8EBED" Offset="1"/>
</LinearGradientBrush>
</Grid.Background>
<Path Data="M1,0 L0,0 L0,1 L1,1"
Stretch="Fill" Stroke="Gray"/>
<TextBlock Margin="3,0,3,0" VerticalAlignment="Center">
<ctrl:BindingHelper.Binding>
<ctrl:BindingProperties TargetProperty="Text"
SourceProperty="SecondLevelHeaderText"
RelativeSourceAncestorType="SLMenu"
RelativeSourceAncestorLevel="1"/>
</ctrl:BindingHelper.Binding>
</TextBlock>
</Grid>
</DataTemplate>
Or, if you don't care about using a tab at all, you may leave the template empty, like this (it really doesn't affect the menu behavior, it's just a question of appearance):
<DataTemplate x:Key="secondLevelHeaderLeftTemplate">
</DataTemplate>
The image above shows a menu with the Dock
property set to Right
.
As you can see, the tab fits perfectly at the right. As expected, this is another implementation of the TabTemplate
property. This is how we write the XAML:
<ctrl:SLMenu x:Name="mnu3"
Grid.Column="0" Grid.ColumnSpan="3"
Grid.Row="1" Grid.RowSpan="1"
TabTemplate="{StaticResource secondLevelHeaderRightTemplate}"
SecondLevelMenuItemTemplate="{StaticResource secondLevelMenuItemRTLTemplate}"
Dock="Right" MenuItemClick="mnu_MenuItemClick"/>
This new instance differs slightly from the menu at the Left. We switched the positions of some of the menu item components so that
the children indicator (the black little triangle) appeared at the left side:
<DataTemplate x:Key="secondLevelMenuItemRTLTemplate">
<Grid Width="300" Opacity="{Binding IsEnabled, Converter={StaticResource isEnabledToOpacity}}">
<ctrl:ItemWrapper Text="{Binding Text}" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="12"/>
<ColumnDefinition Width="28"/>
<ColumnDefinition/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Data="M0,1 L1,0 L1,2 L0,1"
Width="4" Height="6" Stretch="Fill" Fill="Black"
Visibility="{Binding Children, Converter={StaticResource hasItemsToVis}}"/>
<Image Grid.Column="1" HorizontalAlignment="Left"
Source="{Binding IconUrl}" Width="16" Margin="4,0,0,0"
Visibility="{Binding IsCheckable, Converter={StaticResource boolToCollapsed}}"/>
<Image Grid.Column="1" HorizontalAlignment="Left"
Source="{Binding IconUrl}" Width="16" Margin="4,0,0,0"
Visibility="{Binding IsChecked, Converter={StaticResource boolToVis}}"/>
<TextBlock Grid.Column="2" VerticalAlignment="Center"
Text="{Binding Text}"/>
<TextBlock Grid.Column="3" VerticalAlignment="Center"
HorizontalAlignment="Right" Text="{Binding ShortCut}"/>
</ctrl:ItemWrapper>
<ctrl:Separator Grid.Column="0" Grid.ColumnSpan="3" Text="{Binding Text}"/>
</Grid>
</DataTemplate>
The bottom menu shown above displays a menu very similar with the top menu. The difference is that the different menu levels are rendered bottom-up instead of top-down.
Styling
Important: If you feel uncomfortable directly writing the XAML code, I strongly recommend using
Microsoft Expression Blend.
Styling plays an important role in Silverlight Menu4U. If you don't like the default Visual Studio-like menu,
you can change it as you wish. We are walking through this change in the example below.
The image above shows how to set the FirstLevelItemsPanelBrush
and SecondLevelItemsPanelBrush
properties to give the menu a different look-and-feel:
<Style x:Key="slMenuStyle" TargetType="ctrl:SLMenu">
<Setter Property="FirstLevelItemsPanelBrush">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#00ffffff" Offset="0"/>
<GradientStop Color="#00ffffff" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="SecondLevelItemsPanelBrush">
<Setter.Value>
<RadialGradientBrush RadiusX="1.2"
RadiusY="1.2" Center="0,0"
Opacity="0.8">
<GradientStop Offset="0.0" Color="#00FFFFFA"/>
<GradientStop Offset="0.7" Color="#00D6EDFE"/>
<GradientStop Offset="0.8" Color="#806AC8F4"/>
<GradientStop Offset="0.9" Color="#802590E7"/>
<GradientStop Offset="1.3" Color="#801E41C3"/>
</RadialGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="MenuBorderStyle" Value="{StaticResource menuBorderStyle}"/>
<Setter Property="ItemSelectionStyle" Value="{StaticResource itemSelectionStyle}"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontFamily" Value="Arial Black"/>
</Style>
Notice that we have also overridden the MenuBorderStyle
, ItemSelectionStyle
, and FontFamily
properties. The menu now looks more "liquid"
and transparent. It may not be very appealing, but no doubt it shows an entirely different menu appearance.
Templating
If you are tired of traditional menus, then you will like the templating features of Silverlight Menu4U.
While you can use styles to set specific properties of the menu, by templating a menu, you can overcome the classic
icon/text/shortcut appearance. When you define the template for the menu items, you can entirely recreate the control
structure to recreate the menu content as you wish: Grids, StackPanels, Borders, Textboxes... Everything is allowed inside a template.
In this case, we'll be using templates to make a totally customized scroll-like menu for some Harry Potter-related website:
Looks cool, doesn't it? First, we provide the HeaderTemplate
and FooterTemplate
. These templates will define the top and the bottom of the paper scroll:
<DataTemplate x:Key="headerTemplate">
<Grid Width="380" Height="80">
<Image Source="/Images/ScrollTop.png" Stretch="UniformToFill"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="footerTemplate">
<Grid Width="380" Height="59">
<Image Source="/Images/ScrollBottom.png" Stretch="UniformToFill"/>
</Grid>
</DataTemplate>
Now we define the background for the menu panel itself (with the ScrollBody.png image). It must contain the stretched part
of the scroll paper, and will work as the background for the menu items:
<Setter Property="SecondLevelItemsPanelBrush">
<Setter.Value>
<ImageBrush ImageSource="/Images/ScrollBody.png"/>
</Setter.Value>
</Setter>
That was the easy part. Now we define the template for the menu items:
<DataTemplate x:Key="secondLevelMenuItemTemplate">
<Border BorderBrush="Transparent" BorderThickness="1"
CornerRadius="2" Width="300" Margin="32,0,32,0">
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"/>
<ColumnDefinition/>
<ColumnDefinition Width="30"/>
</Grid.ColumnDefinitions>
<Image Grid.Column="0" HorizontalAlignment="Left"
Source="{Binding IconUrl}" VerticalAlignment="Top"/>
<StackPanel Grid.Column="1">
<TextBlock Margin="8,0,0,0" Text="{Binding Text}"
FontSize="16" FontWeight="Bold" HorizontalAlignment="Left"/>
<TextBlock Margin="8,0,0,0" Text="{Binding Description}"
FontSize="11" TextWrapping="Wrap" HorizontalAlignment="Left" />
</StackPanel>
<Path Grid.Column="2" Data="M1,1 L0,0 L0,2 L1,1"
Width="10" Height="10" Stretch="Fill" Fill="Black"/>
</Grid>
</Border>
</DataTemplate>
RightToLeft
One of the suggestions I was given after the release of the old Silverlight menu was to create right-to-left support for alphabets such as Arabic and Hebrew. This time, fortunately I was able
to embrace this feature, and here is how to use it. You should override the template for both the first and second level menu items. This will do the trick:
<DataTemplate x:Key="firstLevelMenuItemTemplate">
<Grid Width="120"
Opacity="{Binding IsEnabled, Converter={StaticResource isEnabledToOpacity}}">
<ctrl:ItemWrapper Text="{Binding Text}" HorizontalAlignment="Stretch">
<TextBlock VerticalAlignment="Center"
Text="{Binding Text}" TextAlignment="Right"/>
</ctrl:ItemWrapper>
</Grid>
</DataTemplate>
<DataTemplate x:Key="secondLevelMenuItemTemplate">
<Grid Width="160" Opacity="{Binding IsEnabled,
Converter={StaticResource isEnabledToOpacity}}">
<ctrl:ItemWrapper Text="{Binding Text}" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="12"/>
<ColumnDefinition/>
<ColumnDefinition Width="28"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Data="M0,1 L1,0 L1,2 L0,1"
Width="4" Height="6" Stretch="Fill" Fill="Black"
Visibility="{Binding Children,
Converter={StaticResource hasItemsToVis}}"/>
<TextBlock Grid.Column="1" VerticalAlignment="Center"
Text="{Binding Text}" TextAlignment="Right"/>
<Image Grid.Column="2" HorizontalAlignment="Left"
Source="{Binding IconUrl}" Width="16" Margin="4,0,0,0"
Visibility="{Binding IsCheckable, Converter={StaticResource boolToCollapsed}}"/>
<Image Grid.Column="2" HorizontalAlignment="Left"
Source="{Binding IconUrl}" Width="16" Margin="4,0,0,0"
Visibility="{Binding IsChecked, Converter={StaticResource boolToVis}}"/>
</ctrl:ItemWrapper>
<ctrl:Separator Grid.Column="0" Grid.ColumnSpan="3" Text="{Binding Text}"/>
</Grid>
</DataTemplate>
Obviously, we must provide the DataSource
for the right-to-left menu. In this case, we have an Arabic menu:
public static List<object> CreateArabicMenu()
{
var menu = new List<object>();
var m1 = new MenuItem("إبحار");
var m11 = new MenuItem("المواضيع", @"/Images/mnuCut.png");
m1.Children.Add(m11);
var m12 = new MenuItem("أبجدي", @"/Images/mnuCopy.png");
m1.Children.Add(m12);
var m13 = new MenuItem("بوابات", @"/Images/mnuPaste.png");
m1.Children.Add(m13);
var m14 = new MenuItem("مقالة عشوائية", @"/Images/mnuDelete.png");
m1.Children.Add(m14);
var m2 = new MenuItem("المشاركة والمساعدة");
var m21 = new MenuItem("اتصل بنا", @"/Images/mnuAddClass.png");
m2.Children.Add(m21);
var m22 = new MenuItem("بوابة المجتمع", @"/Images/mnuAddExistingItem.png");
m2.Children.Add(m22);
...some more menu items...
menu.Add(m1);
menu.Add(m2);
menu.Add(m3);
return menu;
}
After some more styling (which I'm skipping for brevity), and... voilà! Here's our brand new beautiful Arabic menu:
Checkable Items
"Checkable items" is a common feature in menu controls. For example, you may have an application that shows several kinds of panels (such as Visual Studio 2010). You may also
provide your user the ability to toggle the visibility of each panel individually. For this, you might use the "checkable items" functionality provided by Silverlight Menu4U:
The following code shows how the MenuHelper
class on the client application side implements checkable menu items:
...
CreateCheckableMenuItem(mnuToolbars, "Build", true);
CreateCheckableMenuItem(mnuToolbars, "Data Design");
CreateCheckableMenuItem(mnuToolbars, "Database Diagram", true);
CreateCheckableMenuItem(mnuToolbars, "Debug");
CreateCheckableMenuItem(mnuToolbars, "Formatting", true);
CreateCheckableMenuItem(mnuToolbars, "HTML Source Editing", true);
CreateCheckableMenuItem(mnuToolbars, "Layout", true);
CreateCheckableMenuItem(mnuToolbars, "Query Designer");
CreateCheckableMenuItem(mnuToolbars, "Standard", true);
CreateCheckableMenuItem(mnuToolbars, "Style Sheet", true);
CreateCheckableMenuItem(mnuToolbars, "Table Designer");
CreateCheckableMenuItem(mnuToolbars, "Text Editor");
CreateCheckableMenuItem(mnuToolbars, "View Designer", true);
CreateCheckableMenuItem(mnuToolbars, "Web Browser");
CreateCheckableMenuItem(mnuToolbars, "Web One Click Publish");
var mnuSeparator32 = new MenuItem("-"); mnuToolbars.Children.Add(mnuSeparator32);
CreateCheckableMenuItem(mnuToolbars, "Customize");
...
And finally, here is the CreateCheckableMenuItem
function. Notice how the ExecuteDelegate
action
toggles the IsChecked
property on and off:
static void CreateCheckableMenuItem(MenuItem parent, string text, bool isChecked = false)
{
var item = new MenuItem(text);
item.Command = new SimpleCommand()
{
ExecuteDelegate = new Action<object>((o) => { item.IsChecked = !item.IsChecked; })
};
item.IsCheckable = true;
item.IsChecked = isChecked;
item.IconUrl = @"/Images/Checked.png";
parent.Children.Add(item);
}
Under the Hood
This section deals with the internals of Silverlight Menu4U. It might be a very extensive topic, but for the sake of simplicity,
not every little aspect is going to be covered here.
One of the interesting questions about SilverlightMenu4U is whether it is a User Control or a Custom Control. By definition, a user control is a composition of existing user controls,
much like creating a window in WPF or a page in Silverlight. Also by definition, a user control encapsulates all the look and feel necessary for the presentation
of the control (in the control's XAML file), and (usually) can't be styled. On the other hand, custom controls are extended from existing controls (by deriving from controls such
as a Button
or ItemsPanel
, for example) and the default look and feel must be provided in the generic.xaml of the assembly and retrieved by code.
It can be said that custom controls are more powerful and harder to develop than user controls.
In which category falls Silverlight Menu4U
? The answer is, it is a user control, because it is a composition of different and
preexisting Silverlight controls, but also provides the flexibiility for styling and templating, like a custom control.
Creating Levels
The first category of inner controls are the panels that make up the menu levels. Each menu level shows a different group of menu item siblings in the menu.
As expected, only the first level is always visible. The other levels are shown on demand, when the user moves the mouse over the upper menu levels.
The levels are not only shown on demand, they are also created on demand. They are created only once in the control's lifetime.
As we can see below, the first level is treated in a different manner, because it can be rendered horizontally or vertically, and aligned in different ways, depending on the docking
option chosen by the user. Each container is in fact a Grid
containing the listbox that will actually render the menu items:
private void CreateMenuLevel(int level)
{
if (level == 0)
{
switch (Dock)
{
case Dock.Top:
case Dock.Bottom:
firstLevelOrientation = Orientation.Horizontal;
break;
case Dock.Left:
case Dock.Right:
firstLevelOrientation = Orientation.Vertical;
break;
}
var levelContainer = new Grid();
levelContainer.Name =
string.Format("levelContainer{0}", levelContainers.Count());
levelContainer.SetValue(Canvas.ZIndexProperty, 100 + level);
LayoutRoot.Children.Add(levelContainer);
levelContainers.Add(levelContainer);
var listBox = CreateListBox(level, "levelListBoxes[0]",
"menuItemTemplate14U", "FirstLevelListBoxStyle4U",
"listBoxItemStyle4U", "FirstLevelItemsPanelBrush",
firstLevelOrientation, true);
levelListBoxes.Add(listBox);
var menuPanel = new Border();
menuPanel.Child = listBox;
menuPanels.Add(menuPanel);
levelContainer.Children.Insert(0, menuPanel);
levelListBoxes[level].CustomSelectionChanged +=
new CustomListBox.CustomSelectionChangedDelegate(
levelListBox_CustomSelectionChanged);
levelListBoxes[level].ItemMouseLeftButtonDown +=
new CustomListBox.ItemMouseLeftButtonDownDelegate(
levelListBox_ItemMouseLeftButtonDown);
switch (Dock)
{
case Dock.Top:
levelListBoxes[level].HorizontalAlignment =
HorizontalAlignment.Stretch;
levelContainer.SetValue(Grid.ColumnProperty, 0);
levelContainer.SetValue(Grid.ColumnSpanProperty, 3);
break;
case Dock.Left:
levelListBoxes[level].VerticalAlignment =
VerticalAlignment.Stretch;
levelContainer.SetValue(Grid.RowProperty, 0);
levelContainer.SetValue(Grid.RowSpanProperty, 3);
break;
case Dock.Bottom:
levelListBoxes[level].HorizontalAlignment =
HorizontalAlignment.Stretch;
levelContainer.SetValue(Grid.RowProperty, 2);
levelContainer.SetValue(Grid.ColumnProperty, 0);
levelContainer.SetValue(Grid.ColumnSpanProperty, 3);
levelContainer.VerticalAlignment = VerticalAlignment.Bottom;
break;
case Dock.Right:
levelListBoxes[level].VerticalAlignment = VerticalAlignment.Stretch;
levelContainer.SetValue(Grid.ColumnProperty, 2);
levelContainer.SetValue(Grid.RowProperty, 0);
levelContainer.SetValue(Grid.RowSpanProperty, 3);
break;
}
}
For the second and further levels, the container and listbox are always rendered vertically (though I may be changing this in the future):
else
{
switch (Dock)
{
case Dock.Top:
case Dock.Left:
case Dock.Bottom:
case Dock.Right:
secondLevelOrientation = Orientation.Vertical;
break;
}
var levelContainer = new Grid();
levelContainer.Name = string.Format("levelContainer{0}", levelContainers.Count());
levelContainer.SetValue(Canvas.ZIndexProperty, 100 + level);
LayoutRoot.Children.Add(levelContainer);
levelContainers.Add(levelContainer);
var secondLevel = new CustomListBox(level, secondLevelOrientation);
levelListBoxes.Add(secondLevel);
var listBox = CreateListBox(level, string.Format("levelListBoxes{0}", level),
"menuItemTemplate24U", "SecondLevelListBoxStyle4U",
"listBoxItemStyle4U", "SecondLevelItemsPanelBrush",
secondLevelOrientation, false);
levelListBoxes[level] = listBox;
var menuPanel = new Border();
menuPanel.Child = listBox;
menuPanels.Add(menuPanel);
levelContainer.Children.Insert(0, menuPanel);
levelListBoxes[level].CustomSelectionChanged +=
new CustomListBox.CustomSelectionChangedDelegate(levelListBox_CustomSelectionChanged);
levelListBoxes[level].ItemMouseLeftButtonDown +=
new CustomListBox.ItemMouseLeftButtonDownDelegate(levelListBox_ItemMouseLeftButtonDown);
if (level == 1)
{
secondLevelHeader.MouseLeftButtonDown +=
new MouseButtonEventHandler(levelHeader_MouseLeftButtonDown);
secondLevelHeader2.MouseLeftButtonDown +=
new MouseButtonEventHandler(levelHeader_MouseLeftButtonDown);
levelContainer.Visibility = System.Windows.Visibility.Collapsed;
}
switch (Dock)
{
case Dock.Top:
levelListBoxes[0].HorizontalAlignment = HorizontalAlignment.Stretch;
levelContainer.SetValue(Grid.RowProperty, 1);
levelContainer.SetValue(Grid.ColumnProperty, 0);
levelContainer.SetValue(Grid.ColumnSpanProperty, 3);
levelContainer.VerticalAlignment = VerticalAlignment.Top;
break;
case Dock.Left:
levelListBoxes[0].VerticalAlignment = VerticalAlignment.Stretch;
levelContainer.SetValue(Grid.ColumnProperty, 1);
levelContainer.SetValue(Grid.RowProperty, 0);
levelContainer.SetValue(Grid.RowSpanProperty, 3);
levelContainer.HorizontalAlignment = HorizontalAlignment.Left;
break;
case Dock.Bottom:
levelListBoxes[0].HorizontalAlignment = HorizontalAlignment.Stretch;
levelContainer.SetValue(Grid.RowProperty, 1);
levelContainer.SetValue(Grid.ColumnProperty, 0);
levelContainer.SetValue(Grid.ColumnSpanProperty, 3);
if (level == 1)
levelContainer.VerticalAlignment = VerticalAlignment.Bottom;
else
levelContainer.VerticalAlignment = VerticalAlignment.Bottom;
break;
case Dock.Right:
levelListBoxes[0].VerticalAlignment = VerticalAlignment.Stretch;
levelContainer.SetValue(Grid.ColumnProperty, 0);
levelContainer.SetValue(Grid.RowProperty, 0);
levelContainer.SetValue(Grid.RowSpanProperty, 3);
levelContainer.HorizontalAlignment = HorizontalAlignment.Right;
break;
}
}
Creating List Boxes
I consider the making of the listboxes the most important part in the control. First, we provide the templates, styles, and listbox orientation.
Then we create an instance of CustomListBox
and then apply these properties to the listbox. Then we make the listbox "inherit" some
of the user control properties. And in the end, we create the ItemsPanelTemplate
, for the ItemsPanel
. This part is the core of the control's templating:
CustomListBox CreateListBox(int level, string name, string
itemTemplate, string style, string itemContainerStyle,
string itemsPanelBrush, Orientation listBoxOrientation, bool stretch)
{
var listBox = new CustomListBox(level, listBoxOrientation);
listBox.Name = string.Format("listBox{0}", levelListBoxes.Count());
listBox.ItemTemplate = (DataTemplate)this.Resources[itemTemplate];
listBox.Style = (Style)this.Resources[style];
listBox.ItemContainerStyle = (Style)this.Resources[itemContainerStyle];
string orientation = listBoxOrientation.ToString();
var virtualizingStackPanel = new VirtualizingStackPanel()
{
Orientation = listBoxOrientation
};
switch (listBoxOrientation)
{
case Orientation.Horizontal:
listBox.VerticalAlignment = System.Windows.VerticalAlignment.Top;
if (stretch)
{
listBox.HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch;
}
else
{
listBox.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
}
break;
case Orientation.Vertical:
listBox.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
if (stretch)
{
listBox.VerticalAlignment = System.Windows.VerticalAlignment.Stretch;
}
else
{
listBox.VerticalAlignment = System.Windows.VerticalAlignment.Top;
}
break;
}
var properties = new BindingProperties()
{
SourceProperty = "FirstLevelItemsPanelBrush",
TargetProperty = "Background",
RelativeSourceAncestorType = "SLMenu",
RelativeSourceAncestorLevel = 1
};
listBox.Foreground = this.Foreground;
listBox.FontFamily = this.FontFamily;
listBox.FontSize = this.FontSize;
listBox.FontStyle = this.FontStyle;
listBox.FontWeight = this.FontWeight;
listBox.Background = this.Background;
listBox.BorderBrush = this.BorderBrush;
listBox.BorderThickness = this.BorderThickness;
listBox.Language = this.Language;
Controls.BindingHelper.SetBinding(virtualizingStackPanel, properties);
var strTemplate = new StringBuilder();
strTemplate.Append("<ItemsPanelTemplate");
strTemplate.Append(" xmlns=\"http://schemas." +
"microsoft.com/winfx/2006/xaml/presentation\"");
strTemplate.Append(" xmlns:controls=\"clr-namespace:" +
"Silverlight.Controls;assembly=Silverlight.Controls\">");
strTemplate.Append(" <VirtualizingStackPanel Orientation=\"" +
orientation + "\" Background=\"{Binding " +
itemsPanelBrush + "}\">");
strTemplate.Append(" <controls:BindingHelper.Binding>");
strTemplate.Append(" <controls:BindingProperties " +
"TargetProperty=\"Background\" SourceProperty=\"" +
itemsPanelBrush + "\"");
strTemplate.Append(" RelativeSourceAncestorType" +
"=\"SLMenu\" RelativeSourceAncestorLevel=\"1\"/>");
strTemplate.Append(" </controls:BindingHelper.Binding>");
strTemplate.Append(" </VirtualizingStackPanel>");
strTemplate.Append("</ItemsPanelTemplate>");
ItemsPanelTemplate itemsPanelTemplate = (ItemsPanelTemplate)XamlReader.Load(strTemplate.ToString());
listBox.ItemsPanel = itemsPanelTemplate;
return listBox;
}
Handling Mouse Events
Instead of the original ListBox
, I used the CustomListBox
I mentioned above to encapsulate a few functionalities, for example
the mouse event handling. Whenever the user moves the mouse over a menu item, that lisbox item must be selected automatically. This is not the default
functionality of the listbox, so we have to implement it by ourselves. The following code shows how we wire up the MouseEnter
and MouseMove
events of the item Container
for each item in the list:
void CustomListBox_MouseEnter(object sender, MouseEventArgs e)
{
for (var i = 0; i < this.Items.Count; i++)
{
var container = (ListBoxItem)this.ItemContainerGenerator.ContainerFromIndex(i);
if (container != null)
{
container.MouseEnter -= new MouseEventHandler(container_MouseEnter);
container.MouseEnter += new MouseEventHandler(container_MouseEnter);
container.MouseMove -= new MouseEventHandler(container_MouseMove);
container.MouseMove += new MouseEventHandler(container_MouseMove);
}
}
}
Then we select the item that is being entered:
void container_MouseMove(object sender, MouseEventArgs e)
{
SelectItem(sender);
}
void container_MouseEnter(object sender, MouseEventArgs e)
{
SelectItem(sender);
}
Then we select the item in the listbox, provided it is not disabled nor an item separator:
private void SelectItem(object sender)
{
var container = (ListBoxItem)sender;
dynamic item = this.ItemContainerGenerator.ItemFromContainer(container);
var isEnabled = item.IsEnabled == null ? true : item.IsEnabled;
var isSeparator = item.IsSeparator == null ? false : item.IsSeparator;
if (isEnabled && !isSeparator)
{
if (this.SelectedItem != item)
this.SelectedItem = item;
}
}
Positioning Levels
Finally, whenever the user moves the mouse over a specific menu item, we must show or hide the next menu level for the child menu items of the selected menu item.
Not only that, each child menu level must be positioned taking in consideration the docking mode, the parent item's top and left, and the menu orientation:
void levelListBox_CustomSelectionChanged(CustomListBox parentListBox,
IEnumerable itemsSource, double stackedPosition, double width, double height)
{
var parentLevel = parentListBox.Level;
var menuLevel = parentLevel + 1;
for (var i = levelContainers.Count() - 1; i > parentListBox.Level + 1; i--)
{
levelContainers[i].Visibility = Visibility.Collapsed;
}
if (levelListBoxes.Count() < menuLevel + 1)
{
CreateMenuLevel(menuLevel);
}
levelListBoxes[menuLevel].ItemsSource = itemsSource;
var parentMarginLeft = 0.0;
var parentMarginRight = 0.0;
var parentMarginTop = 0.0;
var parentHeight = 0.0;
var offsetX = 0.0;
var offsetY = 0.0;
if (firstLevelOrientation == Orientation.Horizontal)
{
if (parentLevel > 0)
{
parentMarginLeft = ((Thickness)levelContainers[parentLevel].GetValue(MarginProperty)).Left;
parentMarginTop = ((Thickness)levelListBoxes[parentLevel].GetValue(MarginProperty)).Top;
parentHeight = ((double)levelListBoxes[parentLevel].GetValue(ActualHeightProperty));
}
switch (Dock)
{
case Dock.Top:
if (menuLevel > 1)
{
offsetX = width;
offsetY = parentMarginTop + stackedPosition;
}
else if (menuLevel > 0)
{
offsetX = stackedPosition;
offsetY = parentMarginTop;
}
else
{
offsetX = stackedPosition;
offsetY = 0.0;
}
levelListBoxes[menuLevel].Margin = new Thickness(0, offsetY, 0, 0);
levelContainers[menuLevel].Margin = new Thickness(parentMarginLeft + offsetX, 0, 0, 0);
levelContainers[menuLevel].SetValue(Grid.RowProperty, 1);
if (parentLevel == 0)
{
secondLevelHeader.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
secondLevelHeader.Margin = new Thickness(offsetX, 0, 0, 0);
secondLevelHeader.VerticalAlignment = VerticalAlignment.Top;
SecondLevelHeaderText = levelListBoxes[parentLevel].SelectedItem.ToString();
}
break;
case Dock.Bottom:
if (menuLevel > 1)
{
offsetX = parentMarginLeft + width;
offsetY = parentHeight - stackedPosition - height;
}
else if (menuLevel > 0)
{
offsetX = stackedPosition;
offsetY = parentMarginTop;
}
else
{
offsetX = stackedPosition;
offsetY = 0.0;
}
levelListBoxes[menuLevel].Margin = new Thickness(0, 0, 0, offsetY);
levelContainers[menuLevel].Margin = new Thickness(offsetX, 0, 0, 0);
levelContainers[0].HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch;
if (parentLevel == 0)
{
secondLevelHeader.Margin = new Thickness(offsetX, 0, 0, 0);
secondLevelHeader.VerticalAlignment = VerticalAlignment.Bottom;
SecondLevelHeaderText = levelListBoxes[parentLevel].SelectedItem.ToString();
}
break;
}
}
else
{
var secondLevelContainerHeight =
(double)levelListBoxes[menuLevel].GetValue(ActualHeightProperty);
if (parentLevel > 0)
{
parentMarginLeft = ((Thickness)levelContainers[parentLevel].GetValue(MarginProperty)).Left;
parentMarginRight = ((Thickness)levelContainers[parentLevel].GetValue(MarginProperty)).Right;
parentMarginTop = ((Thickness)levelContainers[parentLevel].GetValue(MarginProperty)).Top;
}
switch (Dock)
{
case Dock.Left:
if (menuLevel > 1)
{
offsetX = parentMarginLeft + width;
offsetY = parentMarginTop + stackedPosition;
}
else if (menuLevel > 0)
{
offsetX = parentMarginLeft;
offsetY = stackedPosition;
}
else
{
offsetX = 0.0;
offsetY = stackedPosition;
}
levelContainers[menuLevel].SetValue(Grid.ColumnProperty, 1);
levelContainers[menuLevel].Margin = new Thickness(offsetX, offsetY, 0, 0);
levelContainers[menuLevel].HorizontalAlignment = HorizontalAlignment.Left;
if (parentLevel == 0)
{
secondLevelHeader2.Margin = new Thickness(0, offsetY, 0, 0);
secondLevelHeader2.VerticalAlignment = VerticalAlignment.Top;
secondLevelHeader2.HorizontalAlignment = HorizontalAlignment.Left;
SecondLevelHeaderText = levelListBoxes[parentLevel].SelectedItem.ToString();
}
break;
case Dock.Right:
if (menuLevel > 1)
{
offsetX = parentMarginRight + width;
offsetY = parentMarginTop + stackedPosition;
}
else if (menuLevel > 0)
{
offsetX = parentMarginLeft;
offsetY = stackedPosition;
}
else
{
offsetX = 0.0;
offsetY = stackedPosition;
}
levelContainers[menuLevel].SetValue(Grid.ColumnProperty, 1);
levelContainers[menuLevel].Margin = new Thickness(0, offsetY, offsetX, 0);
levelContainers[menuLevel].HorizontalAlignment = HorizontalAlignment.Right;
if (parentLevel == 0)
{
secondLevelHeader2.Margin = new Thickness(0, offsetY, 0, 0);
secondLevelHeader2.VerticalAlignment = VerticalAlignment.Top;
secondLevelHeader2.HorizontalAlignment = HorizontalAlignment.Right;
SecondLevelHeaderText = levelListBoxes[parentLevel].SelectedItem.ToString();
}
break;
}
}
if (menuLevel > 1)
{
levelContainers[menuLevel].Visibility = levelListBoxes[menuLevel].Items.Count() > 0 ?
Visibility.Visible : Visibility.Collapsed;
}
}
Acknowledgements
Many thanks to Colin Eberhardt for his article Implementing RelativeSource
binding in Silverlight. As you may have seen, I've used Colin's implementation all over the project. I did it because Silverlight 4 (the version I'm using here) doesn't support
relative source binding natively. Fortunately, as pointed out in this article by Kunal Chowdhury, the new Silverlight 5
now provides this great feature.
Final Considerations
I hope you liked the article and you found the article useful for you. If you have complaints, ideas, opinions, please let me know. I'm willing
to modify/enhance the code based on your feedback so that the project can evolve organically.
History
- 2011-07-31: Initial version.
- 2011-08-03: New "Under the Hood" section.
- 2011-08-05: New feature: item population directly via XAML.
- 2011-08-05: Checkable items explained.