Implementing a ContextMenu in Silverlight 4 Beta

November 18, 2009

The Silverlight team receives a lot of feedback about features that users would love to see, and right click support was quite high on the list (currently #2).    So, I’m pleased to say that right click events are fully supported in the new Silverlight 4 beta release recently announced at PDC!

It’s clear there are a number of useful applications for this feature aside from context menus, but menus are definitely one of the major uses cases – so, I’d like to go through a sample implementation of a reusable end-to-end context menu implementation that can be used in any Silverlight application.

Highlights of the features the custom menu should have:

  • An attached property that lets us apply a working ContextMenu to an element in XAML without writing code.
  • ContextMenu, MenuItem, MenuItemSeparator controls that can be retemplated to suit your application’s look and feel.
  • Click events for MenuItems, plus Command and CommandParameter support for MVVM applications.
  • Support interaction with both mouse and keyboard.  For example:
    • Moving between menu items: mouse, tab key, up/down arrow keys
    • Selecting a menu item: clicking, space key, enter key
    • Closing menu: click (right/left) anywhere else in app, escape key
    • MenuItemSeparators shouldn’t be selectable
  • For familiarity’s sake, an API similar to the WPF ContextMenu.

Sample look and feel:

image

1.  The Events

The two new events that are raised for all UIElements are MouseRightButtonDown and MouseRightMouseButtonUp.  These are routed events, similar to the well-known left mouse button events.  The first step to implementing a context menu will be to hook up handlers for these events, and do something with them.

MouseRightButtonDown

  • In order to suppress the normal “Silverlight” context menu that will still pop up by default, we need to mark this event as handled to stop the routed event from bubbling up the visual tree.  We can do this by setting the Handled property to true in event args:
private static void element_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
    e.Handled = true;
}

You might notice this differs slightly from WPF, which requires you to handle the Up event – the difference here is to support platforms where context menus are opened on button down instead of up.

MouseRightButtonUp

  • This is when we’ll open the context menu.  Read on for details..

2.  Class Overview

Here’s a simplified class diagram showing the main classes we’ll create:

image

  •  ContextMenu
    • This is the main class that will contain and display the menu items.  Notice the ContextMenu attached property – this will allow us to attach a menu to any element and automatically hook up mouse event listeners as needed to show and hide the menu without writing any code in the app.  This class will have a custom template that can be modified/overriden to suit any application.
  • MenuItemBase
    • This is the base class for any item added to a ContextMenu.
  • MenuItemSeparator
    • By default this element should draw a single horizontal line to separate menu items (customizable by changing the template).  It shouldn’t be selectable.
  • MenuItem
    • This class represents a selectable option in a ContextMenu.  Its content can be set using the Text property, and you can either listen to the Click event or supply a Command (and CommandParameter) which will automatically be executed.

3.  Sample Usage

Once you include the ContextMenu assembly in your application, all you have to do to hook up a context menu is set the ContextMenu.ContextMenu attached property:

   1: <TextBox Text="hi there, world!">
   2:    <menu:ContextMenu.ContextMenu>
   3:        <menu:ContextMenu DataContext="{StaticResource myViewModel}">
   4:            <menu:MenuItem Text="option 1" Command="{Binding MyCommand1}" />
   5:            <menu:MenuItem Text="option 2" Command="{Binding MyCommand2}" />
   6:            <menu:MenuItemSeparator />
   7:            <menu:MenuItem Text="option 3" Click="MenuItem_Click" />
   8:        </menu:ContextMenu>
   9:    </menu:ContextMenu.ContextMenu>
  10: </TextBox>

Pretty clean – no app code needed!  Notice items can either specify the Command property (in this case bound to commands in my sample ViewModel), or handle the Click event (don’t worry, keyboard selection counts as clicking).  Don’t forget to declare the prefix for the ContextMenu control assembly:

<UserControl x:Class="ContextMenuExample.MainPage"
    xmlns:menu="clr-namespace:ContextMenuControls;assembly=ContextMenuControls"
    ...
>

Since ContextMenu, MenuItem, and MenuItemSeparator are custom controls, you can also retemplate them in XAML to change the look and feel.

4.  Drilldown: ContextMenu Class

Here’s a simplified listing of the main members of the ContextMenu class (some  omitted for brevity):

   1: [ContentProperty("Items")]
   2: public class ContextMenu : ToolTip
   3: {
   4:     #region ContextMenu attached property
   5:     public static readonly DependencyProperty ContextMenuProperty =
   6:         DependencyProperty.RegisterAttached("ContextMenu",
   7:                                             typeof(ContextMenu),
   8:                                             typeof(ContextMenu),
   9:                                             new PropertyMetadata(null, OnContextMenuChanged));
  10: 
  11:     private static void OnContextMenuChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e);
  12:     #endregion
  13: 
  14:     private ObservableCollection<MenuItemBase> _items;
  15:     private ItemsControl _itemContainerList;
  16:
  17:     private void OnLoaded(object sender, RoutedEventArgs e);
  18:     private void OnOpened(object sender, RoutedEventArgs e);
  19:     private void OnClosed(object sender, RoutedEventArgs e);
  20: 
  21:     public ObservableCollection<MenuItemBase> Items { get; }
  22:     private void _items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e);
  23: 
  24:     private void MenuItem_Selected(object sender, EventArgs e);
  25:     private static void element_MouseRightButtonDown(object sender, MouseButtonEventArgs e);
  26:     private static void element_MouseRightButtonUp(object sender, MouseButtonEventArgs e);
  27: 
  28:     private void OnKeyDown(object sender, KeyEventArgs e);
  29:     protected override void OnMouseRightButtonDown(MouseButtonEventArgs e);
  30:     private void OnMouseDownElsewhere(object sender, MouseButtonEventArgs e);
  31: }

Starting off: ContextMenu inherits from ToolTip.  That gives us a good starting point, since the positioning logic and “popup” behavior are already done (thanks to Dave Relyea for some pointers on ToolTip use here).  Also note the Items property is attributed as the ContentProperty, so that adding items to the ContextMenu in XAML is supported without needing to add the <ContextMenu.Items> tags around them every time.

ContextMenu attached DependencyProperty, OnContextMenuChanged

This allows us to attach a ContextMenu to any given FrameworkElement.  Thanks to the OnContextMenuChanged method, which is invoked whenever this property is set or unset, we can automatically start or stop handling the right mouse button events whenever this property is changed on a given element.

Items, ItemContainerList

The Items collection, which contains zero or more MenuItemBase objects to be displayed, is implemented as an ObservableCollection so that we can receive notifications when items are added or removed from the collection and update the displayed items accordingly.  ItemContainerList is a reference to an ItemsControl in the ContextMenu’s template, which will be used to display the items inside the menu. 

We could have made this a bit easier by using a ListBox to get the selection and update notification behavior for free, but implementing our own collection change handling with a generic ItemsControl gives us better control over styling, selection behavior, and API exposure.

OnLoaded

One quirk that results from using a ToolTip is that the menu will default the IsHitTestVisible property to false every time it is loaded.  Since we obviously want to be able to click on the menu, we set IsHitTestVisible to true here.

OnOpened, OnClosed

The main work to be done here is to attach and detach event handlers to the plugin’s root element when the menu opens and closes (respectively), because we want to close the menu whenever the user interacts with something else in the application.  Using the UIElement.AddHandler(..,.., true) API, we can ensure we’ll still receive the events even if they’re already handled by other elements.

OnKeyDown

Most events will be handled at the MenuItem level, but some keyboard routed events that bubble up will be handled by the ContextMenu, since they deal with moving focus or closing the menu.  The code is fairly self-explanatory:

   1: private void OnKeyDown(object sender, KeyEventArgs e)
   2: {
   3:     switch (e.Key)
   4:     {
   5:         case Key.Escape:
   6:             // close menu
   7:             IsOpen = false;
   8:             break;
   9:         case Key.Up:
  10:             // select next item in up direction
  11:             ChangeFocusedItem(false);
  12:             break;
  13:         case Key.Down:
  14:             // select next item in down direction
  15:             ChangeFocusedItem(true);
  16:             break;
  17:     }
  18: }

Since the TabNavigation mode of the ContextMenu is set to Cycle by default, moving “up” past the first item will move focus to the last item, and vice-versa.

Template

The ContextMenu template (defined in themes\generic.xaml) has the ItemsControl used to hold items, and two VisualStates – Open and Closed.

   1: <ControlTemplate x:Key="ContextMenuTemplate" TargetType="local:ContextMenu">
   2:     <Canvas x:Name="Root" Opacity="0">
   3:         <VisualStateManager.VisualStateGroups>
   4:             <VisualStateGroup Name="OpenStates">
   5:                 <VisualStateGroup.Transitions>
   6:                     <VisualTransition GeneratedDuration="0:0:0.2" />
   7:                 </VisualStateGroup.Transitions>
   8:                 <VisualState x:Name="Closed" />
   9:                 <VisualState x:Name="Open">
  10:                     <Storyboard>
  11:                         <DoubleAnimation Storyboard.TargetName="Root" Storyboard.TargetProperty="Opacity" To="1" Duration="0" />
  12:                     </Storyboard>
  13:                 </VisualState>
  14:             </VisualStateGroup>
  15:         </VisualStateManager.VisualStateGroups>
  16:
  17:         <Border BorderBrush="Gray" BorderThickness="1" Width="150">
  18:             <Border.Effect>
  19:                 <DropShadowEffect ShadowDepth="2" Color="Gray" />
  20:             </Border.Effect>
  21:                 <ItemsControl x:Name="ItemList" Background="White" TabNavigation="Cycle" />
  22:         </Border>
  23:     </Canvas>
  24: </ControlTemplate>

 

5.  Drilldown: MenuItem Class

Simplified API listing:

   1: [TemplateVisualState(Name = MenuItem.StateNormal, GroupName = MenuItem.GroupCommon)]
   2: [TemplateVisualState(Name = MenuItem.StateActive, GroupName = MenuItem.GroupCommon)]
   3: public sealed class MenuItem : MenuItemBase
   4: {
   5:     public delegate void ClickHandler(object sender, EventArgs e);
   6:     public event ClickHandler Click;
   7: 
   8:     public static readonly DependencyProperty TextProperty =
   9:         DependencyProperty.Register("Text", typeof(string), typeof(MenuItem), null);
  10:     public static readonly DependencyProperty CommandProperty =
  11:         DependencyProperty.Register("Command", typeof(ICommand), typeof(MenuItem), null);
  12:     public static readonly DependencyProperty CommandParameterProperty =
  13:         DependencyProperty.Register("CommandParameter", typeof(object), typeof(MenuItem), null);
  14: 
  15:     public string Text { get; set; }
  16:     public ICommand Command { get; set; }
  17:     public object CommandParameter { get; set; }
  18: 
  19:     protected override void OnMouseEnter(MouseEventArgs e);
  20:     protected override void OnMouseLeave(MouseEventArgs e);
  21:     protected override void OnMouseMove(MouseEventArgs e);
  22:
  23:     protected override void OnGotFocus(RoutedEventArgs e);
  24:     protected override void OnLostFocus(RoutedEventArgs e);
  25:
  26:     protected override void OnKeyDown(KeyEventArgs e);
  27:
  28:     protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e);
  29:     protected override void OnMouseRightButtonUp(MouseButtonEventArgs e);
  30: }

Here we have the public Click event, as well as the Text, Command, and CommandParameter properties.  The Click event will be raised whenever the item is selected by clicking (with the left or right button), or by selecting with the keyboard (with the Space or Enter key).

The mouse, keyboard, and focus-handling overrides are fairly trivial, and primarily deal with selection and changing the visual state – an item’s visual state will be Active when it has focus, and Normal otherwise.  The main trick here is to focus an item whenever a user highlights it using the mouse or keyboard: we need to re-focus an item not only when it receives a MouseEnter event, but also when it receives a MouseMove event, since keyboard input may have changed the focused item since the last mouse event was received.

5.  Source Code

Full source available from:

Note this is written against Silveright 4 Beta in Visual Studio 2010 Beta 2, so content and APIs are subject to change.  I’ll try to update as needed.

13 Responses to “Implementing a ContextMenu in Silverlight 4 Beta”

  1. If you supress the silverlight menu on the context click how is the user suppose to get to those options? Is there a way to append menu items to the the native silverlight menu on the context click?

  2. You can’t add items to the native menu, but there are other ways to access those options.
    Some examples:
    -you can create a custom context menu entry to install an out of browser application using the Application.Current.Install() method
    -the configuration dialog is accessible via a shortcut in the start menu (or Applications folder on a Mac)

  3. Hi Jesse,

    Just published an article on my blog about the “right click support” and your awesome control did provide a realistic valuable instant solution for app developers.

    http://daron.yondem.com/tr/PermaLink.aspx?guid=b621640a-b973-45e9-a67a-7b8493ca872a

    Thanks for the great work! Hope to see your ContextMenu control in the Silvelright Toolkit soon!

  4. It seems that when I apply your ContextMenu to an element, it is ignoring Placement = PlacementMode.Mouse in your code. I can fiddle with the Xaml, and if I stick a Tooltip in addition to one of your context menu objects, then the placement works correctly.

    My scenario is a canvas like container with some simple rectangles…

    So if I change this:

    To:

    I get the correct placement…

    Any ideas?

    Thanks
    Rob

  5. This is a nice feature to have in Silverlight and I look forward to being able to use it. Unfortunately, I can’t get it to work for me in a scenario in which the UI elements to which I’m adding the context menu are created dynamically by the application. I create the context menu as a local resource on a page and attach it to the UI elements I create in the application. In my case, I’m trying to attach the menu to Pushpins displayed on a Bing! map through the Silverlight control, but I suspect the problem would manifest with any sort of UI element.

    When the context menu tries to open in the right mouse button up handler, setting .IsOpen to true causes an error deep inside the native SL stack. The error propagates back to the application as an argument exception:

    System.ArgumentException was unhandled by user code
    Message=Value does not fall within the expected range.
    StackTrace:
    at MS.Internal.XcpImports.CheckHResult(UInt32 hr)
    at MS.Internal.XcpImports.SetValue(INativeCoreTypeWrapper obj, DependencyProperty property, DependencyObject doh)
    at System.Windows.DependencyObject.SetValue(DependencyProperty property, DependencyObject doh)
    at System.Windows.Controls.ToolTip.HookupParentPopup()
    at System.Windows.Controls.ToolTip.OnIsOpenChanged(Boolean isOpen)
    at System.Windows.Controls.ToolTip.OnIsOpenPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    at System.Windows.DependencyObject.RaisePropertyChangeNotifications(DependencyProperty dp, Object oldValue, Object newValue)
    at System.Windows.DependencyObject.UpdateEffectiveValue(DependencyProperty property, EffectiveValueEntry oldEntry, EffectiveValueEntry& newEntry, ValueOperation operation)
    at System.Windows.DependencyObject.SetValueInternal(DependencyProperty dp, Object value, Boolean allowReadOnlySet)
    at System.Windows.DependencyObject.SetValue(DependencyProperty property, Boolean b)
    at System.Windows.Controls.ToolTip.set_IsOpen(Boolean value)
    at ContextMenuControls.ContextMenu.element_MouseRightButtonUp(Object sender, MouseButtonEventArgs e)
    at MS.Internal.CoreInvokeHandler.InvokeEventHandler(Int32 typeIndex, Delegate handlerDelegate, Object sender, Object args)
    at MS.Internal.JoltHelper.FireEvent(IntPtr unmanagedObj, IntPtr unmanagedObjArgs, Int32 argsTypeIndex, String eventName)
    InnerException:

    I found a similar, although not identical, thread from the Silverlight 2 era. It may just be that all communication with the native layer happens through the Ms.Internal.XcpImports class, so this may be a red herring:

    http://forums.silverlight.net/forums/t/31071.aspx

    Regards,
    Eric

  6. Very Nice!!!!!!!

    Jesse, I have question. Is there anyway to dynamically generate menu item using binding using this control?

    Thanks
    Anil

  7. Hi Jesse,

    I’m having the same issue as Eric Schoen: When the context menu tries to open in the right mouse button up handler, setting .IsOpen to true causes an error deep inside the native SL stack.

    Any ideas ?

    Thanks !

  8. Why ContextMenu inherits from UserControl, instead of inherit from ItemsControl? Your approach not provides ability to bind context menu items with datasource.

    Dmitriy.

Pingbacks

  1. Silverlight 4 and building business applications (PDC09-CL19) « davidpoll.com
  2. SL4: DataGrid and contextmenu « Lee’s corner
  3. Microsoft PDC09 and Silverlight Round-up
  4. Tweets that mention jebishop.blog » Implementing a ContextMenu in Silverlight 4 Beta -- Topsy.com
  5. DataGrid and contextmenu

Leave a Reply