Ask Kathleen

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.

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 is a consultant, author, trainer and speaker. She’s been a Microsoft MVP for 10 years and is an active member of the INETA Speaker’s Bureau where she receives high marks for her talks. She wrote "Code Generation in Microsoft .NET" (Apress) and often speaks at industry conferences and local user groups around the U.S. Kathleen is the founder and principal of GenDotNet and continues to research code generation and metadata as well as leveraging new technologies springing forth in .NET 3.5. Her passion is helping programmers be smarter in how they develop and consume the range of new technologies, but at the end of the day, she’s a coder writing applications just like you. Reach her at [email protected].

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube