Click here to Skip to main content
15,883,859 members
Articles / Programming Languages / C#
Technical Blog

Silverlight Menu

Rate me:
Please Sign up or sign in to vote.
4.00/5 (1 vote)
26 Nov 2012CPOL3 min read 12.6K   6   3
My take on this control.

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.

image

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)

C#
<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.

C#
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)
    {
        // copy import UI value to child element automatically...
        // copy first in case something is set in the template (would be set in base.XXX())
        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.

C#
#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
    {
        // separator don't open
        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:

C#
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
        {
            // placement logi
        };

        // placement needs size calculation, need be done after being shown
        // otherwise DesiredSize will always be {0,0}, despite calls to Measure() !!??
        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();
        // update CurrentPopupData and close popup that should be closed
    }
    internal static void OnFinish(MenuItem item)
    {
        // close everything and update CurrentPopupData
    }

    #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.

C#
#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); }
}

// Using a DependencyProperty as the backing store for CommandParameter.  This enables animation, styling, binding, etc...
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.

image

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.

XML
<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}"
            />
        <!-- other controls -->
    </Grid>
</ControlTemplate>

Then I simply setup the state in mouse handler

C#
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

image

XML
<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.

C#
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()
    {
        // initialize a recursive list of Items and their children
        // and their children and their children's children and so on...
    }
    public List<ModelItem> Items { get; private set; }
}

Then I can do some simple binding

image

XML
<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

image

XML
<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.

image

XML
<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>

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) http://www.ansibleww.com.au
Australia Australia
The Australia born French man who went back to Australia later in life...
Finally got over life long (and mostly hopeless usually, yay!) chronic sicknesses.
Worked in Sydney, Brisbane, Darwin, Billinudgel, Darwin and Melbourne.

Comments and Discussions

 
QuestionHow to Design for ItemsPresenterContainer Pin
BuiLuongTruong25-Dec-14 14:31
BuiLuongTruong25-Dec-14 14:31 
SuggestionLiquid Controls Pin
Mesrop Davoyan26-Nov-12 21:44
professionalMesrop Davoyan26-Nov-12 21:44 
I use this library and very satisfied
http://www.vectorlight.net/silverlight/controls/main_menu.aspx[^]
GeneralRe: Liquid Controls Pin
Super Lloyd27-Nov-12 8:43
Super Lloyd27-Nov-12 8:43 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.