Implement Expandable Menus
Learn how to create expandable menus such as you find in Office; handle mouse up/down events properly; and create a custom toolstrip button.
by Kathleen Dollard
June 29, 2007
Technology Toolbox: VB .NET, C#
Q
My users sometimes have complex menus, and they've asked me to replicate the expanding menu style you see in Excel and Word 2003. Can I create this in .NET?
A
You can create a menu that initially displays a subset of menu items and displays the whole list when a period of time elapses or the user clicks on a special button. It's interesting code because it manages the drop down display with an extra menu item and a timer.
You can download the code and a test project from the VSM Web site (download the code here). You need to grasp only a few important concepts to implement this kind of functionality. Putting the new code in a derived class keeps the code organized and isolates the work for reuse. This SpecialMenuItem derives from a WinForms ToolstripMenuItem. You can include the SpecialMenuItem in the Visual Studio drop down lists using an attribute, a subject discussed later in this article in the question about using a custom toolstrip button to create buttons.
Before looking at how the code works, consider how you're going to use the class. It's simplest to always use the SpecialMenuItem ignoring the ToolstripMenuItem. SpecialMenuItem has two properties that appear in the property dialog to facilitate this. Setting IsStandardItem to true causes SpecialMenuItem to behave like a normal toolstrip menu item. You set the Visible property as part of menu expansion, so you can't use the Visible property to hide an item as part of your application logic. Instead, use the AlwaysHidden property to hide a menu item.
The dropdown needs an extra menu item to display the arrow button that the user clicks to expand the dropdown list explicitly. I created an extra ExpandMenuItem nested class as a convenient way to isolate this code. The constructor of this class sets important values such as the name, which you use to delete the "expand menu" item when the menu expands. Setting the background image to Center and supplying no text or image displays the icon alone, centered as the new menu item.
These details support the more interesting code for the dynamic behavior (Listing 1). The DropDownShow event checks whether DesignMode is active and whether the menu is currently expanded. You use a variable to track whether anything is hidden in the initially displayed menu; it makes sense to display the option to expand the menu only when something is hidden.
You'll also need to count the number of displayed items to ensure a minimum number appear. You want to count everything that might be visible except separators. You can hide an item if it is a SpecialMenuItem and its IsStandard property is false. If you hide the item, you need to indicate that something is hidden so you can later display the ExpandMenuItem. If you hide the item, you also need to decrement the displayed item count. After looping through all the controls, call a method to display the minimum number of controls if you have less than the minimum number. Displaying the ExpandMenuItem if needed completes the collapsed version of the dropdown menu that's initially displayed for the user.
The dropdown menu expands when the user clicks on the ExpandMenuItem or a specified amount of time elapses. You set the time interval and enable the timer so it starts the countdown when the user displays the menu. The timer fires its Tick event after the specified time has elapsed. Both the click event and the TimerTick event call the ExpandItems method (Listing 2). This makes previously hidden menu items and separators visible. The dropdown must be redisplayed explicitly, but calling the ShowDropDown method prompts the OnDropDownShow method to execute a second time. If you don't manage this with a flag, the menu closes up immediately.
When you expand the menu or close it, you need to take care of a little housekeeping, so leftovers from this dropdown display don't affect future application behavior. You must stop the timer and remove the expand button.
You can refine this behavior further by keeping a list of recently used menu items as a class level collection that updates when a menu click event fires. For best behavior, you should persist this list per user.
Expandable menus give your WinForms application extra polish and work best when the application menus have many items. I combined the drop down parent and the drop down items into a single class to make dropping this behavior into your applications easy. This also enables the expansion behavior for all menu depths. After you check whether your source control is up to date or make a backup, do a search and replace for "System.Windows.Forms.MenuItem" with the name of your special menu item. These changes appear in the .Designer file that you can find by clicking the "Show All Files" button at the top of Solution Explorer.
Q
I'm having a bit of an argument with someone on my team about whether to use the mouse up or mouse down event. Mouse down seems more logical to me.
A
Mouse down might seem more logical, but it's not the behavior we've grown to expect from applications like Word. To test this, highlight something, click on the bold icon but hold the mouse button down. Nothing happens until you release the mouse button. You'll rarely notice the difference because you generally press and release mouse buttons quickly. But for the smoothest behavior, handle the mouse up event.
Your user gets additional behavior for free if you use MouseUp. If the user changes their mind right after the mouse down, they can simply move off the control before the MouseUp. Because MouseUp now happens over a different control, the user easily cancels their operation. While this is subtle behavior, some experienced Word users rely on it.
Q
I've heard that using the framework's generic classes gives you more robust code. Can you explain why?
A
A programmer must resolve code issues that arise at compile time before anyone can run the code. Generics force a common category of programming errors to occur at compile time instead of runtime, enabling you to resolve them before they waste time in testing or slip out to users.
For example, generics eliminate a class of errors that occur when you treat items as Object data types, rather than as specific types. This is particularly common with collections. A collection designed for Object types can hold anything (legally), but a particular collection should logically hold only a specific type, interface, or base type. The compiler can't recognize when you put the wrong type of item in the collection, so a casting error arises at runtime if you cast an object from the collection to an unexpected type.
Generics circumvent this problem by literally creating a new type of collection at runtime, one designed specifically for the anticipated type. This new class knows exactly what it should hold, which means the compiler can raise an exception if you make a mistake and include the wrong type. A compiler exception is always better than a runtime exception.
The compiler creates this class at runtime (unlike the similar C++ templates), so there is no code bloat and the generic class can support types unknown at compile time. This means the framework generic collections can be specific to your custom types, although the .NET framework doesn't have the details of your class.
Object-based collections have another drawback: You need to cast items when they are extracted from the collection or when you use them in a foreach loop. Casting is a relatively costly operation in terms of performance, so using generics speeds up your code a bit. Don't worry about the performance of creating these new generic classes at runtime. This process is fast because it's baked deep into the framework. The performance penalty of creating the new generic class is so small I've never been able to measure it.
Another benefit of generics is allowing new capabilities for lists because they know the type they hold. Microsoft added filtering and new mechanisms for sorting that would have been slow if every comparison required type casts.
There's one situation where a runtime cast is still going to occur with generic collections. An implicit cast occurs (and fails) in both C# and VB .NET in this code:
public class Test
{ public static void Foo()
{ List<FooBit> fooBits =
new List<FooBit>();
fooBits.Add (new FooBit());
foreach (FooBitA fooBit
in fooBits)
{
Console.WriteLine(DateTime.Now);
}
}
}
public class FooBit {}
public class FooBitA : FooBit {}
If you declare the iteration variable to something the list type could be cast to, such as the narrowing cast in the code above, you don't get a compiler error. This is for backwards compatibility, and it means you have to give extra attention to each variable declaration.
You should use generic classes in all cases where they are available. There are still corners of the framework, such as localization extensibility, that use ArrayLists. But unless you're using one of these areas, remove the namespace imports or using statement for the System.Collection class and use only classes from the System.Collection.Generics class.
Q
I'm using a custom toolstrip button to create buttons. These custom buttons inherit from ToolstripButton to hold extra properties. Can I make these new buttons available in the dropdown designer?
A
Inheriting from a WinForms class is great way to provide extra information and behavior. This technique also identifies the buttons under your control, which you might be using as a façade to allow later enhancements. Unfortunately, Visual Studio doesn't recognize your ToolstripButton by default; you have to add an attribute.
ComponentModel, Windows.Forms, and the Windows.Forms.Design namespaces provide several attributes. The toolstrip uses the ToolStripItemDesignerAvailability attribute to indicate which contexts make sense for your derived class. You can supply menu item behavior with this line:
Imports System.Windows.Forms.Design< _
ToolStripItemDesignerAvailability( _
ToolStripItemDesignerAvailability. _
Toolstrip)> Public Class NameOfClass
If you want a class to appear in the dropdown for MenuStrip, combine the MenuStrip and ContextMenuStrip enum values with the bitwise OR operator:
Imports System.Windows.Forms.Design _
<ToolStripItemDesignerAvailability( _
ToolStripItemDesignerAvailability. _
MenuStrip Or _
ToolStripItemDesignerAvailability. _
ContextMenuStrip)> _
Public Class NameOfClass
Instructing Visual Studio how to use your class and your properties at design time is one of the common reasons to decorate your class with attributes. Unfortunately, Microsoft did a poor job documenting where attributes are appropriate and what they do. You can find the information you need once you know the name of the required attribute. That said, knowing the task you want to perform rarely points you directly to the attribute/s you need (Table 1). Table 1 shows a few of the most important attributes from the System.ComponentModel namespace that can appear on properties to support design time behavior.
About the Author
Kathleen Dollard has been developing business applications for over 20 years, programming in Visual Basic for ten years, and working with .NET since the early betas. As an independent consultant, she has worked in a variety of domains, including the finance and justice sectors. Kathleen has worked extensively with application code generation and is the author of Code Generation in Microsoft .NET (from Apress). She has published numerous articles on a range of .NET technologies, including WPF, WF, XSLT, debugging, ADO.NET, and code generation. Kathleen is also a member of the INETA speaker's bureau, a long time Microsoft MVP, founding member of the Northern Colorado .NET SIG, and is an active member of the Denver Visual Studio User Group. Contact her at kathleen@mvps.org.
Back to top
|