Silverlight: Dropdown Menu Control

Print Content | More

In a previous post I showed how we can build a simple menu control for Silverlight 2 Beta 2 (Silverlight: how to build a simple menu control); then with the release of Silverlight RC0 some things were changed and the menu stopped working, due to how events are handled for disabled controls (Silverlight 2 RC0 – first problems due to undocumented breaking changes on disabled controls).

The only way to overcome these problems was to completely rewrite the control, I also took advantage of this to implement some more features, such as the possibility to have nested menus and a better support for skinning through styles.

Here’s what we want to obtain:

SilverlightMenu3 SilverlightMenu4

The new dropdown menu is now composed of 4 different controls:

- MenuBar: a container for the menu, it only supports horizontal placing of elements right now.

- MenuPanel: a control that provides the frame in which the menu items are displayed (for styling purposes).

- MenuItem: the base class for a single menu item, it can contain a list of menu items (to support nested menus).

- MainMenuItem: the class that ‘describes’ a menu item that belongs to the MenuBar, it keeps a list of Menu Items.

The MenuItem control is still based on the Button control, but instead of having it placed in a xaml file alongside a grid, this time we derive the new control from the Button class. This way the control can be skinned using the standard way with templates.

The MainMenuItem derives from MenuItem and overrides some internal members.

The previous PopupProvider class was renamed MenuPopupProvider and modified to handle special cases related to the way the popup windows of chained menus are closed when the user clicks on menu items or when he moves the mouse outside the menu itself; with the current modification this class is no more a generic popup handler but it’s became specific for this menu control.

The two major changes were made to the ClosePopup() function, which now has to take into account the fact that we cannot close a menu if there’s a submenu opened and also, if the closing of the current menu is confirmed, it has to forward the closing request to the parent menu (if there’s any).

   1: private void ClosePopup()
   2: {
   3:     if (_isPopupOpen && _isPopupClosing)
   4:     {
   5:         _closeTimer.Stop();
   6:         //the popup logically belongs to the owner, so we have to check in one of his children have a popup opened. 
   7:         if (_owner is MenuItem)
   8:         {
   9:             MenuItem mi = _owner as MenuItem;
  10:             if (mi.HasSubItems)
  11:                 foreach (MenuItem m in mi.MenuItems)
  12:                     if (m.IsSubMenuOpen)
  13:                     {
  14:                         //if so we cancel the closing request
  15:                         _isPopupClosing = false;
  16:                         return;
  17:                     }
  18:         }
  20:         _isPopupOpen = _isPopupClosing = _popup.IsOpen = false;
  22:         //if this is a menuitem and his parent is not null, we have to ask it to close its menu too,
  23:         //however we have to close the parent menu only if the mouse is not over the menu itself
  24:         if (_owner.GetType() == typeof(MenuItem))
  25:         {
  26:             MenuItem parent = (_owner as MenuItem).ParentMenuItem;
  27:             if (parent != null)
  28:                 parent.CloseMenuPopup();
  29:         }
  30:     }
  31: }

We also added an event handler for the MouseEnter() event on the _trigger control to open a submenu when the user hovers the parent item with the mouse.

The most interesting thing is however how we deal with the disabled controls, if a menu item is disabled it will not fire any mouse event (as it should be), but with the RC0 release it seems that when we move the mouse over a disabled control we also go outside the scope of his parent/container too... let me explain: the menu items are contained in a StackPanel, if we move the mouse over a disabled control inside the StackPanel we also get a MouseLeave() event from the StackPanel too..and this caused the PopupProvider to close the menu (see my previous posts on the subject).

To overcome this situation we have to keep the control alive (enabled) and we have to do some magic to ‘fool’ the user and let him see a disabled control, we put our ‘Magician Hat’ on and we write some code to create a Custom Visual State Manager, that we inject in the control template:

   1: public class MenuVisualStateManager : VisualStateManager
   2: {
   3:     protected override bool GoToStateCore(Control control, FrameworkElement templateRoot, string stateName, VisualStateGroup group, VisualState state, bool useTransitions)
   4:     {
   5:         MenuItem mi = (MenuItem)control;
   6:         if (mi.IsEnabled == false)
   7:         {
   8:             //force the control to have a disabled appearence
   9:             stateName = "Disabled";
  10:             if (group != null)
  11:                 for (int i = 0; i < group.States.Count; i++)
  12:                     if ((group.States[i] is VisualState) && (((VisualState)(group.States[i])).Name == stateName))
  13:                         state = group.States[i] as VisualState;
  14:         }
  15:         if (state != null)
  16:             return base.GoToStateCore(control, templateRoot, stateName, group, state, useTransitions);
  17:         else
  18:             return true;
  19:     }
  20: }

What it does is quite simple: every time the control tries to change state, our MenuVisualStateManager gets called and we have a chance to modify the next state that will be displayed: if the MenuItem related is disabled we force the VSM to display the ‘Disabled’ visual state of the button; the trick works cause the appearance of the control is completely disjoint from its operational status.

We assign the custom visual state manager in the xaml that defines the default template for the menu in the Generic.xaml file:

   1: <Style TargetType="ctrl:MenuItem">
   2:         <Setter Property="IsEnabled" Value="true"/>
   3:         ...
   4:         <Setter Property="Template">
   5:             <Setter.Value>
   6:                 <ControlTemplate TargetType="ctrl:MenuItem">
   7:                     <Grid x:Name="MenuItemLayout">
   8:                         <Grid.Resources>
   9:                         ...
  10:                         </Grid.Resources>
  11:                         <vsm:VisualStateManager.CustomVisualStateManager>
  12:                             <ctrl:MenuVisualStateManager x:Name="vsm" />
  13:                         </vsm:VisualStateManager.CustomVisualStateManager>
  14:                         <vsm:VisualStateManager.VisualStateGroups>
  15:                         ...

The last thing to do is to override the default behavior of the IsEnabled property:

   1: public new bool IsEnabled
   2: {
   3:     get { return _isEnabled; }
   4:     set
   5:     {
   6:         _isEnabled = value;
   7:         ForceVisualState(value);
   8:         //this is not strictly needed if the menu control is implemented as a button and we can
   9:         //'really' disable the control, cause if the control is disabled no event will be fired
  10:         if (value)
  11:             EnablePopupProvider();
  12:         else
  13:             DisablePopupProvider();
  14:     }
  15: }
  16: private bool _isEnabled = true;

At this time you can use the new Dropdown Menu Control adding menu items in code only, with something like this:

   1: //Dynamically buildup a menu
   2: MainMenuItem m1 = Menu.AddMenu("Test1");
   3: m1.AddSubmenu("Sub1");
   4: m1.AddSubmenu("Long Sub1 string");
   5: MenuItem sb1 = Menu.CreateMenuItem("Disabled Sub1");
   6: sb1.IsEnabled = false;
   7: m1.AddSubmenu(sb1);
   8: sb1 = Menu.CreateMenuItem("Click Me! Sub1");
   9: sb1.MenuClick += new MenuClickEventHandler(sb1_MenuClick);
  10: m1.AddSubmenu(sb1);
  12: MainMenuItem m2 = Menu.CreateMainMenuItem("Disabled Test2");
  13: m2.IsEnabled = false;
  14: m2.AddSubmenu(Menu.CreateMenuItem("Sub2 1"));
  15: m2.AddSubmenu(Menu.CreateMenuItem("Sub2 2"));
  16: m2.AddSubmenu(Menu.CreateMenuItem("Sub2 3"));
  17: Menu.AddMenu(m2);
  18: ...

You can override the default template for PanelMenus, MainMenuItems and MenuItems using the PanelMenuStyle, MainMenuItemStyle and MenuItemStyle properties exposed by the MenuBar control:

   1: <StackPanel x:Name="LayoutRoot" Background="White">
   2:     <ctrl:MenuBar x:Name="Menu"
   3:         MenuItemStyle="{StaticResource mi}"
   4:         MenuPanelStyle="{StaticResource miPanel}"></ctrl:MenuBar>
   5: </StackPanel>

I’m not a professional graphic designer so my templates are actually quite questionable, in the demo solution you can find a couple of pages that show you how to use and skin this control with different graphic templates.

As usual this is a starting point for further work and a lot of improvements can be made (I’m thinking to completely remove the button control as base class and implement it from scratch, add support for icons and some cool animations, also I’m working on keyboard navigation of the menu itself).

There are also some fix to make to improve the templates assignment to each control, actually you need to use the factory methods provided by the MenuBar control to have templates correctly assigned to each MenuItem.

The solution still contains the old menu control code for you to check for changes made.

Note: I believe there are still some bugs in the way the Popup control interacts with the normal event flow, you can see this when you open a submenu window: if you keep moving the mouse over another menu item at the same level you will notice that the status of the control is not changed and the hover effect is not applied.

(thanks to Giuseppe Polverini for reviewing my English)

Example Solution:



Control, Menu, Menu control, Silverlight, Window, Xaml, Drop down menu


Related Post

  1. #1 da lmcwhirter - Saturday March 2010 alle 01:15

    Guardian, I commend you! This is the first really working dropdown menu control that I have been able to find. Nice work!

  2. #2 da Faisal - Saturday March 2010 alle 01:15

    Excellent post.

  3. #3 da JimBruskalski - Saturday March 2010 alle 01:15

    I agree with lmcwhirter, this is excellent! Thanks!

  4. #4 da Robert - Saturday March 2010 alle 01:15

    Do you have an update to this code that runs with the released version of Silverlight V2 ? I'm trying to use your code and it doesn't seem to work with the relased version of Silverlight v2

  5. #5 da Robert - Saturday March 2010 alle 01:15

    Sorry for my earlier post... I was just being way too dumb to make use of your code ... after a few cups of coffee got it all to work... THANK YOU for posting this!!!

  6. #6 da Guardian - Saturday March 2010 alle 01:15

    No problem at all, if you need some help with the code I post just gimme a shout.

  7. #7 da rajesh - Saturday March 2010 alle 01:15

    i am using ur control and i made come chages so that i can drag the control now the problem is i am not able to fire the event with which the menu is disappered

  8. #8 da Ravikumar - Saturday March 2010 alle 01:15

    HI, When I use your control, I am not able to add more than 2 menus, each having 3-4 menus with Click() method for each of them. Please tell me if you've set any limitations somewhere in your code.

  9. #9 da Guardian - Saturday March 2010 alle 01:15

    No there isn't any limitations in my code regarding the number of menus or menu voices. I'm using the same code in my production environment without any problems, but maybe I made some fixes to it. Take a look at my framework posed here: it contains an updated version of the menu control, maybe it can solve your problems, if not..just email me some sample code to reproduce the bug.

  10. #10 da Ramesh - Saturday March 2010 alle 01:15

    Hi can any one tell me how to bind data to Silver Light 2.0 Menu control using WCF service...? Thanks and Regards, Ramesh J

  11. #11 da Guardian - Saturday March 2010 alle 01:15

    Binding is not yet fully supported in that control, you have to build your controls in code after reading the configuration from the service.

  12. #12 da Ashraf - Saturday March 2010 alle 01:15

    how we can put this Exellent menu to real webpage; Menu go over the other parts of the page?

  13. #13 da Maxstepper - Saturday March 2010 alle 01:15

    Add to my Bookmarks )

  14. #14 da Silverlight Travel - Saturday March 2010 alle 01:15

    Looks great. Can I ad som animations??

  15. #15 da Guardian - Saturday March 2010 alle 01:15

    Yes you can, just change the template of the item you want to animate. I haven't done it yet cause of my very limited time, but it shouldn't be too hard (I hope :D)

  16. #16 da Andrus - Saturday March 2010 alle 01:15

    How to use keyboard navigation ? DevExpress free AgMenu allows to navigate using keyboard, why not to add this ? Andrus.

  17. #17 da Sickandar Navab - Saturday March 2010 alle 01:15

    Hi, I tested your code. Do you have idea how to display the same menu as a context menu .i.e the context menu should be displayed when i right click on the canvas/or grid Can you please help me out.

  18. #18 da maqqju - Saturday March 2010 alle 01:15

    thanks i found how.

  19. #19 da Shoaib Shaikh - Saturday March 2010 alle 01:15

    Such a blunder i did. I should have checked it before. anyways thnx alot u made my day.

  20. #20 da Shoaib Shaikh - Saturday March 2010 alle 01:15

    hi,nice menu control.. i think this is the most important control which was missing in silverlight. i just downloaded ur code and it is working fine, but for some reason when i tried to copy all control related file to new solution it gives me error "Catastrophic failure (Exception from HRESULT: 0x8000FFFF (E_UNEXPECTED))" while adding any MainMenuItem to MenuCtrl. i just googled this error and found that this error occured due to change of behavior of Popups in RTM. but i don't know is it the cause behind this error. if anyone had integrated this menu in webapp let me know how can i do this?

  21. #21 da Guardian - Saturday March 2010 alle 01:15

    I'm using it in two different projects and I never experienced this problem, the first thing that comes in my mind is to check for namespaces of the templates in generic.xaml. If you copied the files and changed the namespaces maybe you forgot to change them there. Also you can look here: this project contains an updated version of the menu control (these are minor changes however).

  22. #22 da Guardian - Saturday March 2010 alle 01:15

    Take a look at the first public version of my framework Structura (I dont have much time to develop it atm, but I will get over it in the future again); there you can see some exaple of usage of this menu control. Here's the link to the post:

  23. #23 da Heartburn Home Remedy - Saturday March 2010 alle 01:15

    After reading the article, I just feel that I need more information on the topic. Could you share some more resources ?

  24. #24 da Guardian - Saturday March 2010 alle 01:15

    Keyboard navigation was in my todo list..unfortunately the time I could invest in developing this control was almost zeroed by all the work I had in the past months.

  25. #25 da maqqju - Saturday March 2010 alle 01:15

    hi, i downloaded your code and am trying to include it in my project, but since i am new to the .NET framework i cannot find a good way for it. can you help me out please? thanks

  26. #26 da Rich L - Saturday March 2010 alle 01:15

    Know of a way to get the menu items to overlay HTML?

  27. #27 da David - Saturday March 2010 alle 01:15

    Thank you for sharing this project. I did end up using your menu in my project. After removing the internal declarations and adding this as an assembly it worked out fine. So far, I've found one bug which happens when you enable a previously disabled menu item. When MenuItem calls MenuPopupProvider it throws an exception because p is null. I fixed it by creating a new p before this call. private void EnablePopupProvider() { if(p == null) p = new System.Windows.Controls.Primitives.Popup() if (pp == null) pp = new MenuPopupProvider(this, this, p, pnl, MenuDirection); } Again, thanks for sharing you work!

  28. #28 da David - Saturday March 2010 alle 01:15

    Is there some reason you used internal methods in your classes? This prevents using your project with an assembly reference... and more difficult to integrate into other applications... Thanks!

  29. #29 da Guardian - Saturday March 2010 alle 01:15

    Thanks for the fix. I missed that bug even in later releases of this control.

  30. #30 da Guardian - Saturday March 2010 alle 01:15

    That was just a sample, look for some later version of the same project in my blog. Otherwise you can just grab the code and modify it as you like.

  31. #31 da Arthritis Onslaught 2 8/8/08 | What your joints need - Saturday March 2010 alle 01:15

    [...] Silverlight: Dropdown Menu Control at Primordial Code [...]

  32. #32 da Web Designing Tips | Web Design and all stuff related to it! - Saturday March 2010 alle 01:15

    [...] Silverlight: Dropdown Menu Control at Primordial Code [...]

  33. #33 da Web Design Articles Tools for Professional Web Design | Web Design and all stuff related to it! - Saturday March 2010 alle 01:15

    [...] Silverlight: Dropdown Menu Control at Primordial Code [...]

  34. #34 da Ashish Pujara - Thursday May 2010 alle 07:23

    Hi,i use menu but it gives me error like System.Web.UI.Silverlight assembly can not find.???
    I am using Visual studion 2010 + silverlight 4.0
    How can resolve this problem.??

  35. #35 da Alessandro Giorgetti - Saturday May 2010 alle 10:05

    Hi, the project is quite old and was initially built for Silverlight 2, you need to rebuild it all from scratch for Silverlight 4

All fields are required and you must provide valid data in order to be able to comment on this post.

(will not be published)