Recently I needed a Silverlight menu. I found the following implementations:
But none behave like an ordinary
HeaderedItemsControl
and all did impose some constraint which make using databinding, templating, or MVVM more awkward than it should be. So here is my take on this control.
Implementation
GMenu Source Code (VS2012) (.zip)
Below I’m going to quickly explain the salient point of the implementation and then show some usage samples.
Implementation
Basic ItemsControl
The main class is MenuItem
. Inspired by WinForm, WPF, I did create a Menu class but it’s just an optional container with a different layout (and it also hide the arrow on the side).
The UI, at its most basic is defined
as follows (non necessary styling element removed)
<Style TargetType="local:MenuItem">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:MenuItem">
<Grid Background="Transparent">
<Border x:Name="PART_HighlightBg" Margin="2" Opacity="0"
Background="{Binding HighlightBrush, Source={StaticResource SystemBrushes}}"
/>
<ContentPresenter
Margin="4"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
/>
<Popup x:Name="PART_Popup" IsOpen="False">
<ItemsPresenter />
</Popup>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
There is a Transparent Grid background grid for hit testing, a border at lowest Z order for highlight, the content presenter for the header and an
ItemsPresenter
in a popup for children.
Now the most basic implementation of an
ItemsControl
just setup children. I also copy the style in PrepareContainerForItemOverride
, as MenuItem
’s children item are whole new
MenuItem
with the default style. This code make sure all children and children’s children and so on looks the same as the top level
MenuItem
, in case it has been styled.
public class MenuItem : HeaderedItemsControl
{
public MenuItem()
{
this.DefaultStyleKey = typeof(MenuItem);
((INotifyCollectionChanged)Items).CollectionChanged +=
delegate { HasChildren = Items.Count > 0; };
}
#region ItemsControl override
protected override DependencyObject GetContainerForItemOverride()
{
return new MenuItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is MenuItem;
}
protected override void ClearContainerForItemOverride(DependencyObject element, object item)
{
base.ClearContainerForItemOverride(element, item);
}
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
var mi = (MenuItem)element;
mi.Style = Style;
mi.Background = Background;
mi.Foreground = Foreground;
mi.BorderBrush = BorderBrush;
mi.BorderThickness = BorderThickness;
base.PrepareContainerForItemOverride(element, item);
}
#endregion
}
To manage the popup by code I added an IsOpen
property.
#region IsOpen
internal Popup GetPopup()
{
return this.GetTemplateChild("PART_Popup") as Popup;
}
public bool IsOpen
{
get
{
var p = GetPopup();
return p != null && p.IsOpen;
}
set
{
value = value && !IsSeparator;
var p = GetPopup();
if (p == null)
return;
if (p.IsOpen == value)
return;
p.IsOpen = value && Items.Count() > 0;
MenuPopupManager.OnOpen(this, value);
}
}
#endregion
Mouse Handling
Now all is needed is to overrides OnMouseEnter
,
OnMouseLeave
, OnMouseLeftButtonDown
, OnMouseLeftButtonUp
.
Most of them do little and delegate the thinking to the class
MenuPopupManager
. This class maintains a list of all open popup, position the Popup appropriately, hide those Popup that need be hidden and close all
MenuItem
after a timeout.
Ideally it should close all menu when the user click outside the
MenuItem
but I could not get it to work reliably.
A skeleton implementation looks like that:
public static class MenuPopupManager
{
#region PopupData, CurrentPopupData
internal class PopupData
{
public PopupData()
{
}
public List<MenuItem> Items = new List<MenuItem>();
public Timer Timer;
public Dictionary<MenuItem, PlacementMode> Placements = new Dictionary<MenuItem, PlacementMode>();
public MenuItem Hovering;
}
static PopupData CurrentPopupData;
#endregion
#region PlacePopup
internal static void PlacePopup(MenuItem mi, Popup p)
{
Action place = delegate
{
};
if (mi.IsOpen)
{
place();
}
else
{
EventHandler onOpen = null;
onOpen = delegate
{
place();
p.Opened -= onOpen;
};
p.Opened += onOpen;
}
}
#endregion
#region OnMouseEnter(), OnMouseLeave(), OnOpen(), OnFinish(),
internal static void OnMouseEnter(MenuItem item)
{
var pd = CurrentPopupData;
if (pd == null)
return;
pd.Hovering = item;
}
internal static void OnMouseLeave(MenuItem item)
{
var pd = CurrentPopupData;
if (pd == null)
return;
pd.Hovering = null;
}
internal static void OnOpen(MenuItem item, bool open)
{
var pd = CurrentPopupData;
var pi = item.GetParentItem();
}
internal static void OnFinish(MenuItem item)
{
}
#endregion
}
Obviously MenuItem
implement Click and triggering an ICommand
and hiding itself after that. If no Click event handler or Command is defined it will do nothing (including not closing the menu), making it easy to embed advanced control in popup menu.
#region Command
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(ICommand), typeof(MenuItem), new PropertyMetadata(null));
#endregion
#region CommandParameter
public object CommandParameter
{
get { return (object)GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register("CommandParameter", typeof(object), typeof(MenuItem), new PropertyMetadata(null));
#endregion
#region Click, OnClick()
public event RoutedEventHandler Click;
protected virtual void OnClick(RoutedEventArgs e)
{
if (IsSeparator)
return;
var c = Command;
var p = CommandParameter;
if (c != null && c.CanExecute(p))
{
c.Execute(p);
MenuPopupManager.OnFinish(this);
}
var he = Click;
if (he != null)
{
he.Invoke(this, e);
MenuPopupManager.OnFinish(this);
}
}
#endregion
Styling
And lastly I added a bit of Styling. I downloaded the Silverlight toolkit and took the
SystemBrushes
class from the SystemColor
theme’s source code. Used various SystemBrushes
’s
Brush
for styling the Menu
and MenuItem
.
The nice silverish background brush of the Menu is
SystemBrushes.ButtonGradient
.
For the mouse hover effect where an highlight color (quickly but progressively) appears I finally took the time to use animation and view state. It proved to be real easy!
First I setup
two states. One which progressively increased the opacity of the background control (PART_HighlightBg
). The second state does nothing, in effect reverting the opacity change instantly. I could have used an animation too but I liked the shorter template
and result is good too.
<ControlTemplate TargetType="local:MenuItem">
<Grid Background="Transparent">
<vsm:VisualStateManager.VisualStateGroups>
<vsm:VisualStateGroup x:Name="Highlight">
<vsm:VisualState x:Name="HighlightOn">
<Storyboard>
<DoubleAnimation
Duration="0:0:0.3" To="0.4"
Storyboard.TargetName="PART_HighlightBg" Storyboard.TargetProperty="Opacity">
<DoubleAnimation.EasingFunction>
<ExponentialEase Exponent="3" EasingMode="EaseInOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</vsm:VisualState>
<vsm:VisualState x:Name="HighlightOff"/>
</vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>
<Border x:Name="PART_HighlightBg"
Margin="2" Opacity="0"
Background="{Binding HighlightBrush, Source={StaticResource SystemBrushes}}"
BorderBrush="{Binding ControlDarkBrush, Source={StaticResource SystemBrushes}}"
BorderThickness="{TemplateBinding BorderThickness}"
/>
-->
</Grid>
</ControlTemplate>
Then I simply setup the state in mouse handler
protected override void OnMouseEnter(MouseEventArgs e)
{
base.OnMouseEnter(e);
MenuPopupManager.OnMouseEnter(this);
VisualStateManager.GoToState(this, "HighlightOn", true);
}
protected override void OnMouseLeave(MouseEventArgs e)
{
base.OnMouseLeave(e);
MenuPopupManager.OnMouseLeave(this);
VisualStateManager.GoToState(this, "HighlightOff", true);
}
Examples
And now some example on how to use this control.
Inline definition
<Grid x:Name="LayoutRoot" Background="White">
<local:Menu Margin="10,0">
<local:MenuItem Header="File lala">
<local:MenuItem Header="Foo">
<local:MenuItem Header="tadat"/>
<local:MenuItem Header="Footaise">
<local:MenuItem Header="tadat"/>
<local:MenuItem Header="tadat"/>
<local:MenuItem Header="tadat"/>
</local:MenuItem>
<local:MenuItem Header="tadat"/>
<local:MenuItem Header="tadat"/>
</local:MenuItem>
<local:MenuItem IsSeparator="True"/>
<local:MenuItem Header="tadat"/>
<local:MenuItem Header="tadat"/>
</local:MenuItem>
</local:Menu>
</Grid>
MVVM - databinding
First I’ll define a data model used for my MVVM usage.
public class ModelCommand : ICommand
{
ModelItem item;
public ModelCommand(ModelItem item) { this.item = item; }
public bool CanExecute(object parameter) { return true; }
public event EventHandler CanExecuteChanged;
public void Execute(object parameter) { MessageBox.Show("Clicked: " + item.Name); }
}
public class ModelItem
{
public ModelItem()
{
Items = new List<ModelItem>();
Command = new ModelCommand(this);
}
public string Name { get; set; }
public List<ModelItem> Items { get; private set; }
public ICommand Command { get; private set; }
}
public class Model
{
public Model()
{
}
public List<ModelItem> Items { get; private set; }
}
Then I can do some simple binding
<local:Menu Margin="10,0" HorizontalAlignment="Right">
<local:MenuItem Header="Persons" ItemsSource="{Binding Items, Source={StaticResource MM}}">
<local:MenuItem.ItemContainerStyle>
<Style TargetType="local:MenuItem">
<Setter Property="Command" Value="{Binding Command}"/>
</Style>
</local:MenuItem.ItemContainerStyle>
<local:MenuItem.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</local:MenuItem.ItemTemplate>
</local:MenuItem>
</local:Menu>
Remark to setup the Command on the MenuItem
I should use the
ItemContainerStyle
property.
Hierarchical binding
<local:Menu
Margin="10,0" HorizontalAlignment="Right" VerticalAlignment="Bottom"
PopupPlacement="Top" CornerRadius="5,5,0,0" BorderThickness="1,1,1,0">
<local:MenuItem Header="Persons" Command="{Binding Command}"
ItemsSource="{Binding Items, Source={StaticResource MM}}">
<local:MenuItem.ItemContainerStyle>
<Style TargetType="local:MenuItem">
<Setter Property="Command" Value="{Binding Command}"/>
</Style>
</local:MenuItem.ItemContainerStyle>
<local:MenuItem.ItemTemplate>
<sdk:HierarchicalDataTemplate ItemsSource="{Binding Items}">
<TextBlock Text="{Binding Name}"/>
</sdk:HierarchicalDataTemplate>
</local:MenuItem.ItemTemplate>
</local:MenuItem>
</local:Menu>
Styling
I can also add a single parentless MenuItem
anywhere. And I can style it too, setup the background brush for instance (whether it’s parentless or not) and the value will flow down.
<local:MenuItem
Margin="100,0" HorizontalAlignment="Right" VerticalAlignment="Bottom"
PopupPlacement="Top" BorderThickness="1,1,1,0"
Background="Aquamarine" Header="Persons"
ItemsSource="{Binding Items, Source={StaticResource MM}}">
<local:MenuItem.ItemContainerStyle>
<Style TargetType="local:MenuItem">
<Setter Property="Command" Value="{Binding Command}"/>
</Style>
</local:MenuItem.ItemContainerStyle>
<local:MenuItem.ItemTemplate>
<sdk:HierarchicalDataTemplate ItemsSource="{Binding Items}">
<TextBlock Text="{Binding Name}"/>
</sdk:HierarchicalDataTemplate>
</local:MenuItem.ItemTemplate>
</local:MenuItem>