Cutting Edge

Adding a Context Menu to ASP.NET Controls

Dino Esposito

Code download available at:CuttingEdge0502.exe(138 KB)

Contents

Outline of the ContextMenu Control
Usage of the ContextMenu Control
The Programming Interface
Implementation of the Control
Remarks on Absolute Positioning
Putting It All Together
The Menu in ASP.NET 2.0

Although the context menu is a common element of most desktop applications, it is still fairly uncommon in Web application names because it doesn't map well to a server-based technology like ASP.NET. To get context menu functionality, your browser needs strong DHTML support and a rich eventing model, both of which you get in Microsoft® Internet Explorer 5.0 and newer versions, as well as in Netscape 6.0 and others. However, the various browsers' object models, although nearly identical in functionality, feature different members and names, and require you to map events and objects from one model to the other.

In this column, I'll create an ASP.NET context menu that addresses the Internet Explorer object model. Along the way, I'll call out features that can work with other browsers with some modifications. The code download for this column works with ASP.NET 1.x, but can be compiled under ASP.NET 2.0 Beta 1 as well.

Outline of the ContextMenu Control

The MSDN® Library contains several examples of DHTML context menus. Each provides a different implementation of the same basic idea. A context menu is a modular piece of markup code that can move around the page. It consists of two distinct blocks—the user interface and the script code to connect the UI to whatever page control the user has right-clicked. The UI provides a list of clickable elements—the menu items—and defines their text, icon, command name, target URL, tooltip, and whatever else you feel is important to show. The context menu's UI is part of the page and takes up space in the page's control tree. (As a result, too many context menus can lead to performance issues as ASP.NET sends more content to the browser than will be needed by most users in a typical situation.) When the user right-clicks on an element in the page, some JavaScript fires, moves the menu's UI close to the coordinates of the clicked point, and toggles the visibility of the menu.

The context menu remains invisible until the user right-clicks on a page element bound to the context menu. The page element receives a script event and pops up a context menu in response. The script event depends on the browser's capabilities; in Internet Explorer 5.0, and newer versions, it's the OnContextMenu event. In Netscape 6.0 and newer versions you need to use OnMouseUp. (You can also make OnMouseUp work for Internet Explorer 5.0+ browsers but this requires a little more coding.) Upon receiving the event notification, the client script retrieves the block of UI code that forms the context menu and moves it to the exact location where the click occurred. It also toggles on the visibility property of the popup panel. When the user clicks on any of the menu's menu items, the page posts back and raises a server-side event. On the server, clicking on a context menu is no different from clicking on a traditional button.

What if the user decides to abort the operation once she has displayed the context menu? In Windows, the standard context menus are dismissed by hitting the Esc key or by clicking anywhere outside the menu region, so you'll need to build this behavior into your Web version. Note that for accessibility reasons, hitting Esc should only work when the context menu is actively selected since the Esc key could be used by other elements to provide keyboard shortcuts.

Here I'll also give the ContextMenu control the ability to hide when the user moves the mouse outside the control's UI. This is achieved using a script handler for the OnMouseLeave event.

Usage of the ContextMenu Control

Assuming the existence of my ASP.NET ContextMenu control, let's see how to use it to add one or more context menus to an ASP.NET page (you'll see how to build the control later in this column). You start by dropping one or more instances of the ContextMenu control onto a Web Form in Visual Studio® .NET. Next, add to each instance as many menu items as needed and configure them all with tooltips, command names, and whatever else you want to add, such as keyboard shortcuts and links to help topics. The command name will be used to track the clicked item once the page posts back in response to a click on the context menu; thus it must be a unique string within the collection of items for an instance of the control.

For the menu to pop up you need to attach some code to the HTML tag's OnContextMenu event handler. The actual code executed depends on a number of parameters, such as the x,y coordinates of the click, the element clicked, and the instance of the context menu you want to use. Note that this way you entirely replace the standard browser's context menu, if any exists. The JavaScript code to attach to the OnContextMenu event handler is determined dynamically at run time. The ContextMenu control will expose a collection property to contain references to bound controls, which will be given an oncontextmenu attribute at run time, thanks to the implementation of the ContextMenu control. With this completed, you're ready to test the control on some pages.

But first let me spend a minute on the applicability of a design-time binding mechanism between the ContextMenu control and any page elements that will display the menu. Ideally, you would have a ContextMenuId property on each Web control exposed directly by the base class Control. Then you could go to the Properties window, select the property, and see the list of ContextMenu controls on the page. Of course, the ContextMenu control I'm discussing is one I've created; ContextMenuId doesn't exist in ASP.NET 1.x or in the upcoming ASP.NET 2.0.

In the Visual Studio .NET 2003 IDE, an ASP.NET extender control would do the job quite nicely. Extender controls (see Cutting Edge November 2003) extend existing controls without creating new specific classes and, more importantly, can add the defined extensions to a variety of controls much like a common base class would do. The code that performs the extension is loaded into an external class that plugs into the base class at run time. There's no sophisticated interception layer in the middle. The excellent design-time support in Visual Studio .NET makes the presence of provider controls almost invisible and a few lines of auto-generated code bind the extender with the array of extendees.

I'm not going to use extenders in this column for a couple of reasons. One is that support for extenders in Visual Studio .NET 2003 is partially broken when it comes to extending the capabilities of controls on a Web Form. (See Extender provider components in ASP.NET: an IExtenderProvider implementation for more details and a workaround.) Second, there will be no Component Tray area in the ASP.NET designer of Visual Studio 2005. The Web Forms designer now supports only ASP.NET controls and ignores nonvisual components like extenders. Visual Studio 2005 no longer relies on an InitializeComponent section, and there's never any tool-generated code inserted into your code files. ASP.NET controls are not designed to have a context menu, so you can bind them to a custom context menu only through the implementation of the ContextMenu control itself. I've chosen a programmatic approach here, but a solution similar to how ASP.NET validator controls are linked to the controls being validated would also work.

The Programming Interface

Our ContextMenu control inherits from WebControl and acts as a naming container:

public class ContextMenu : WebControl, INamingContainer

Figure 1 details the members of the control. The key property is the ContextMenuItems collection, which contains objects of type ContextMenuItem, each of which represents a menu item. The source code of the ContextMenuItem class is shown in Figure 2. Each menu item has display text, a command name, and a tooltip. You can extend this class in various ways, for example by adding an image URL, a disabled state, or a target URL. The display text is the text rendered on the menu; the command name is a unique string that describes the command associated with the item. Finally, the tooltip describes the purpose of the currently selected item.

Figure 2 The ContextMenuItem Class

[TypeConverter(typeof(ExpandableObjectConverter))] public class ContextMenuItem { public ContextMenuItem() {} public ContextMenuItem(string text, string commandName) { _text = text; _commandName = commandName; } private string _text; private string _commandName; private string _tooltip; public string Text { get {return _text;} set {_text = value;} } public string CommandName { get {return _commandName;} set {_commandName = value;} } public string Tooltip { get {return _tooltip;} set {_tooltip = value;} } }

Figure 1 Members of the ContextMenu Control

Property Description
AutoHide Indicates whether the context menu should be dismissed as the user moves out of the control's boundaries
BoundControls Gets the collection of controls for which the context menu should be displayed
CellPadding Gets and sets the number of pixels around each menu item
ContextMenuItems Gets the collection of the menu items
RolloverColor Gets and sets the background color to set when the mouse hovers over any menu items
Method Description
GetEscReference Returns the JavaScript code needed to dismiss the current context menu when the user hits the Esc key
GetMenuReference Returns the JavaScript code needed to attach the current context menu to the specified HTML element
GetOnClickReference Returns the JavaScript code needed to dismiss the current context menu when the user clicks outside the menu
Event Description
ItemCommand Occurs when the user clicks on a menu item

When the user clicks on a menu item, the page posts back and a server-side ItemCommand event is fired. By handling that event, the host page can execute some code in response to the user clicking the menu. Figure 3 shows the ContextMenu control in the Visual Studio .NET 2003 IDE in the sample project.

Figure 3 ContextMenu Control in the Visual Studio .NET 2003 IDE

Figure 3** ContextMenu Control in the Visual Studio .NET 2003 IDE **

For the context menu to work, you need to fill the ContextMenuItems collection with menu item objects, adjust some visual styles, and add at least one control to the BoundControls collection. Then, point the browser to the sample page and right-click on any bound control. You should see something like Figure 4.

Figure 4 ContextMenu Control in Action

Figure 4** ContextMenu Control in Action **

Each menu item consists of a LinkButton control with an internal handler bound to the Click event. When a click is detected, the page posts back and fires the Click event. In turn, the predefined handler bubbles the event up one level, changing its name to ItemCommand.

The control also features a few visual properties like CellPadding, RolloverColor, and AutoHide. As mentioned, in Windows a context menu disappears when you click outside its boundaries or when you hit the Esc key. For a Web-based context menu, the AutoHide property adds an OnMouseLeave script handler to the root tag of the ContextMenu control so that the subtree is hidden from view as soon as the user's mouse leaves the area of the control. A more fine-grained approach would implement AutoHide as a settable property so that the user can set the desired time for the mouse to hover outside of the context menu before it disappears.

To be able to hide the context menu by clicking or pressing Esc, you need to add event handlers:

<body onkeypress="..." onclick="...">

Script handlers can be programmatically added to virtually any page element, provided that the page element is marked with runat=server. This fact creates a logical dependency between the ContextMenu control and the page. In addition, it forces you to have an extra server control defined on the page. Sure, one extra control instantiated at run time does not have a huge effect on performance, but why have a useless control that you instantiate only because you want to consume another control more easily? In this alternative technique, the final effect is the same: the body catches both Esc hits and mouse clicks and you save the overhead of one server control:

<body onkeypress="<% = ContextMenu1.GetEscReference() %>" onclick="<% = ContextMenu1.GetOnClickReference() %>">

Let's examine the control's implementation in more detail.

Implementation of the Control

The heart of the ContextMenu control lies in the overridden CreateChildControls method. In this method, the control creates its own UI and injects any needed script into the host page. As mentioned, the user interface of the ContextMenu control is divided into two parts—graphics and script. Let's consider graphics first.

CreateChildControls generates the HTML block that will then be moved around the page to serve as a popup menu on demand. From this perspective, a context menu is a <DIV> tag that contains a table with one row for each menu item. Using a table here is arbitrary but simplifies a number of development tasks (such as borders and overall layout) and lends itself very well to further enhancements like side images:

HtmlGenericControl div = new HtmlGenericControl("div"); div.ID = "Root"; div.Style["Display"] = "none"; div.Style["position"] = "absolute"; if (AutoHide) div.Attributes["onmouseleave"] = "this.style.display='none'";

The outermost <DIV> tag is hidden from view using cascading style sheets (CSS) styles and is marked for absolute positioning. If auto-hiding is enabled, the element also handles the mouse-leave event to hide itself when the mouse exits the area. It is interesting to note the difference between the onmouseout and onmouseleave events. The former occurs when the mouse is moved onto a new element. The latter fires only if the mouse is moved out of the boundaries of an object. For example, imagine you have a table with two rows. The onmouseout event on the table fires each time you move the mouse between the rows; the onmouseleave event fires only if you move the mouse outside the table.

The table contains as many rows as there are menu items to display. Each row has a single cell filled with a LinkButton object. The menu is built through a loop like this:

foreach(ContextMenuItem item in ContextMenuItems) { TableRow menuItem = new TableRow(); menuTable.Rows.Add(menuItem); TableCell container = new TableCell(); menuItem.Cells.Add(container); LinkButton button = new LinkButton(); container.Controls.Add(button); ... }

The row cell has a couple of script handlers—onmouseover and onmouseout—to provide rollover effects. When the mouse hovers over the item, the background is set to a different color. The original background is restored when the mouse moves away. The BackColor property inherited from WebControl indicates the default background color. The new RolloverColor property gets and sets the highlight color:

string color = String.Format(ContextMenu.OnMouseOver, ColorTranslator.ToHtml(RolloverColor)); container.Attributes["onmouseover"] = color; color = String.Format(ContextMenu.OnMouseOut, ColorTranslator.ToHtml(BackColor)); container.Attributes["onmouseout"] = color;

You need to translate the .NET System.Drawing.Color value into a string that represents a valid HTML color. Interestingly, neither the ToString method of the Color class nor the Name property on the same class returns the correct HTML string in all cases, nor is it intended to do so. The Name property works fine in all cases but one. When the color doesn't match any known color, the property returns the RGB components of the color plus the alpha channel. To obtain the correct HTML color string you must remove the alpha channel (usually a leading hex ff string) and replace it with the # symbol. Fortunately, the System.Drawing.ColorTranslator class does this automatically.

The link button requires some adjustments to work properly. In particular, you set the link width to 100 percent to ensure that the hand cursor displays throughout the whole row without additional settings. Likewise, you set text, tooltip, and command name getting data from the corresponding menu item object. Finally, you assign an internal handler to the Click event:

LinkButton button = new LinkButton(); container.Controls.Add(button); button.Click += new EventHandler(ButtonClicked); button.Width = Unit.Percentage(100); button.ToolTip = item.Tooltip; button.Text = item.Text; button.CommandName = item.CommandName;

As a result, the page that hosts the context menu posts back when the user clicks a link button. On postback, the sender of the event is identified as the LinkButton control in the ContextMenu naming container and is given a chance to handle the postback event. The Click internal handler packs any information into a new ItemCommand event and raises it (see Figure 5).

Figure 5 Button Click Handler

private void ButtonClicked(object sender, EventArgs e) { LinkButton button = sender as LinkButton; if (button != null) { CommandEventArgs args = new CommandEventArgs( button.CommandName, button.CommandArgument); OnItemCommand(args); } } protected virtual void OnItemCommand(CommandEventArgs e) { if (ItemCommand != null) ItemCommand(this, e); }

The code in the host page that receives an ItemCommand event is passed the instance of the ContextMenu which fired the event and the command name associated with the clicked item.

The table built so far represents the user interface of the context menu. It is initially placed anywhere in the page and is hidden from view using CSS styles. This piece of HTML code (which has absolute positioning capabilities) will display a context menu exactly where the user right-clicks. JavaScript code to intercept the event and move the menu block is also required (see Figure 6). The __showContextMenu function sets the left and top properties of the ContextMenu's style object to make it display around the point of click. The small negative offset ensures that the mouse is already over the menu when the menu displays. This prevents small movements from causing the mouse to cross the boundaries of the element resulting in the hiding. The bubbling of the mouse event must also be stopped so that elements higher in the document object model hierarchy won't handle the right-click event.

Figure 6 JavaScript to Activate the Context Menu

<script language="Javascript"> function __showContextMenu(menu) { var menuOffset = 2 menu.style.left = window.event.x - menuOffset; menu.style.top = window.event.y - menuOffset; menu.style.display = ""; window.event.cancelBubble = true; return false; } function __trapESC(menu) { var key = window.event.keyCode; if (key == 27) { menu.style.display = 'none'; } } </script>

So, who calls the __showContextMenu function? The browser does when it finds the function linked to the oncontextmenu event of an HTML element. As mentioned earlier, oncontextmenu is an event specific to Internet Explorer that is not recognized by Netscape browsers. An alternative is to use onmouseup to catch any mouse release and check for the right button.

The ContextMenu control is also in charge of adding an oncontextmenu handler to each control or page element that registers to get one. I defined two ways for an element to get its own context menu. The BoundControls collection property (see Figure 1) is an array that the page's code fills with references to all page controls that require a particular context menu. Here's an example:

void Page_Load(object sender, EventArgs e) { ContextMenu1.BoundControls.Add(Button1); ... }

The preceding code produces the following markup:

<input type=submit ... oncontextmenu="__showContextMenu(...)" />

When the user right-clicks on the control, the context menu pops up. This approach requires that any page element with a context menu be a server control, a requirement that may or may not be acceptable. For example, suppose you want to replace the default context menu for an image. Must you mark the <img> tag as runat=server? Not necessarily. Here's an example:

<img oncontextmenu="<% = ContextMenu1.GetMenuReference() %>" src="...">

The GetMenuReference method on the control returns the script call that brings the context menu up. The page element still features the desired menu without defining a new server control.

Remarks on Absolute Positioning

The ContextMenu control as developed here requires absolute positioning that not all browsers support. However, a browser that supports a rich object model and a good deal of events will probably have advanced positioning capabilities as well.

As far as Internet Explorer is concerned, there's another way to implement a context menu. Instead of moving a DIV's position around the page you could just create a popup window and display it at the specified position. Then you dynamically load the DIV representing the context menu into the popup document's body. This technique is demonstrated in the article at Using the Popup Object.

While implementing this ContextMenu control, I first used the aforementioned technique and created and displayed a popup window. I discovered a couple of things about popups. One benefit is that the popup object automatically works like a desktop menu and disappears as soon as you click outside or hit Esc. There's no need to write a single of line of code for this behavior to occur.

On the downside, I experienced problems with the View Source function in Internet Explorer 6.0. According to my tests, having the context menu in the popup object alters the page's tree of elements and this seems to have repercussions on the View Source window; it just doesn't show up. Also, I want my context menu to work like an interactive ASP.NET control meaning that I'd like it to post back to the current page (bringing back view state and input fields) rather than jumping to a completely different URL. Posting to the parent window from inside a popup window is still possible but it requires changes at the page level and a bit of tricky scripting. (This is easier in ASP.NET 2.0, thanks to cross-page posting.) In the MSDN examples, it works just great because the context menu simply links to an external URL.

An ASP.NET postback is a little trickier; using absolute positioning maintains the context menu in the realm of the current page and doesn't apply changes to the page's elements tree. As a result, none of the aforementioned drawbacks apply. As for capturing the events that typically dismiss the popup menu, you can still consider capturing mouse events. This can be done using a number of Internet Explorer DHTML methods as described here: How To Create a Mouse Capture Context Menu.

Putting It All Together

The most important part of the ContextMenu control is the list of menu items. You can specify these items either programmatically or at design time. The following code snippet is an example of the design-time approach:

<cc1:contextmenu id="ContextMenu1" runat="server"> <cc1:ContextMenuItem Text="Do This" CommandName="ThisCommand" Tooltip="..." /> <cc1:ContextMenuItem Text="Do That" CommandName="ThatCommand" Tooltip="..." /> <cc1:ContextMenuItem /> <cc1:ContextMenuItem Text="Think ..." CommandName="ThinkCommand" Tooltip="... " /> </cc1:contextmenu>

The empty <cc1:ContextMenuItem> tag indicates a separator. Note that you can have Visual Studio .NET properly handle child tags simply by using a handful of well-placed attributes:

[DesignerSerializationVisibility( DesignerSerializationVisibility.Content)] [PersistenceMode(PersistenceMode.InnerDefaultProperty)] public ContextMenuItemCollection ContextMenuItems {...}

This configuration doesn't let you host other types of child tags, however. If you did that, a parser exception would be thrown. This means, for example, that you can't serialize the contents of the BoundControls in the body of the ContextMenu root tag. By using a different set of design-time attributes, you can work around it. (I expect to cover these aspects of control design in the future).

If you double-click on the context menu control in the Visual Studio .NET designer an event handler is added and is registered with the ContextMenu's ItemCommand event. You can then handle each action by name, filling in the context menu to look something like the following:

void ContextMenu1_ItemCommand(object sender, CommandEventArgs e) { switch(e.CommandName) { case "ThinkCommand": ... break; case "ThisCommand": ... break; default: ... break; } }

In Figure 3, you see the ContextMenu control rendered at design time. The ASP.NET designer just calls RenderControl for each control hosted in the page being designed. When this applies to ContextMenu, though, how could RenderControl render out a row to mimic the selected menu item? That is the effect of a custom designer for the ContextMenu control. You'll find the source code of this component in the companion code. In a nutshell, it captures the HTML string generated by RenderControl and modifies it by adding an extra row with a different background color. In this way, the user has a clearer idea of the control output.

The Menu in ASP.NET 2.0

The ContextMenu accompanying this column is written for ASP.NET 1.x, but it can be easily ported to ASP.NET 2.0. As you probably know, in ASP.NET 2.0 there's a brand new Menu control. However, you should not necessarily use it as a context menu in ASP.NET 2.0 applications. It's too heavy and designed for a different scenario. A ContextMenu control complements the ASP.NET 2.0 Menu control, which is specifically designed to operate as a static menu and lacks two key settings for performing the functions of a context menu: it can't be hidden and doesn't support absolute positioning. Both settings could be added declaratively by editing the control's markup. On the other hand, the ASP.NET 2.0 Menu control provides some key advantages, including support for multilevel, hierarchical menus, accessibility, and scrollbars for resized browser windows. Deciding which one best suits your needs is completely up to you.

Send your questions and comments for Dino to  cutting@microsoft.com.

Dino Esposito is a Wintellect instructor and consultant based in Italy. Author of Programming ASP.NET and the new book Introducing ASP.NET 2.0 (both from Microsoft Press), he spends most of his time teaching classes on ASP.NET and ADO.NET and speaking at conferences. Get in touch with Dino at cutting@microsoft.com or join the blog at weblogs.asp.net/despos.