Login | Register   
Twitter
RSS Feed
Download our iPhone app
TODAY'S HEADLINES  |   ARTICLE ARCHIVE  |   FORUMS  |   TIP BANK
Browse DevX
Sign up for e-mail newsletters from DevX


advertisement
 

Using LINQ to Manage File Resources and Context Menus : Page 5

LINQ queries simplify deep introspections of control collections to help manage embedded file resources and context menu initialization.


advertisement

Practical Techniques: Initializing Context Menu Choices

A context menu is an arbitrarily complex structure of menu items and submenus that opens when you right-click on an appropriately instrumented control in a WinForm application. There are a variety of techniques to create and initialize context menus, but broadly you can categorize them into three groups (see Table 2).

Table 2. Menu Initialization Strategies: These three strategies differ in startup overhead, invocation overhead, and maintenance effort.
# Category Startup overhead Invoking overhead Maintenance
1 Create and initialize a menu as a single operation at the time it is invoked via right-click. Low High Low
2 Create a menu during program startup and initialize it at the time it is invoked Medium Medium Low
3 Create a menu and initialize during program startup then update it as the user interacts with the program so when it is finally invoked, no further action is needed. High Low High

The table shows the relative strengths and weaknesses of the three approaches. Note that these are relative measures—in an absolute sense you could argue that the overhead differences are minuscule, which is generally true. The maintenance differential, however, is non-trivial because category 3 requires a lot more programming effort to identify every program location that could affect the menu, and adjust the menu state appropriately. For that reason I prefer not to use category 3. On balance, categories 1 and 2 require about the same effort, but I lean toward category 2, because it cleanly separates startup tasks (creating a context menu) from operational tasks (setting the states of entries in the context menu). The following discussion takes the category 2 approach.

Working with hierarchical context menus has key similarities to hierarchical user controls:

  • You want to be able to operate on all menu items regardless of their depth, just like you operated on all controls.
  • You want to be able to operate on a specific set of menu items just as you operated on specific types of controls (those that implemented IResourceUser).
To demonstrate the LINQ techniques you can leverage in context menu handling, I will use the context menus from the ChameleonRichTextBox control from the CleanCode libraries. Figure 5 shows all the menu choices. Any given menu within a menu structure may contain either menu items or sub-menus or both. You can see illustrations of all three combinations in the figure. The top level menu for the ChameleonRichTextBox control has three submenus and no menu items (frame 1). Frames 2, 4, and 5 show the next level down. The submenu in Frame 2 has both items and submenus (shown open in Frame 3) while the submenus in Frames 4 and 5 contain only menu items and no submenus.

 
Figure 5. ChameleonRichTextBox Context Menus: This expanded view of the control's context menu reveals (1) the top level submenus; (2) the Highlight submenu containing state items and submenus; (3) leaf submenus for Keywords and Variables; (4) the Command Completion submenu; and (5) the Tab Control submenu. The brackets indicate mutually exclusive sets of menu choices.
The context menu structure in Figure 5 has these interesting characteristics:

  • Hierarchical menus: Rather than just a single, flat menu, there are submenus that go three levels deep. The techniques to be discussed work equally well on any arbitrary depth.
  • Non-unique display names: The Highlighting → Keywords submenu and the Highlighting → Variables submenu both have the same set of child menu items. In fact, the Command Completion submenu also shares those menu item names along with some others. This precludes identifying individual menu items exclusively by display name.
  • Menu items that set a menu state along with possibly performing an action: Any given menu item may perform an action, or set a state, or both. Menu items such as Cut and Paste perform an action without affecting the menu item itself: As it turns out, the ChameleonRichTextBox context menu includes no items that only perform an action. Some items on this context menu tree set a state (e.g. Command Completion ( Uppercase) while others both set a state and perform an action (e.g. Highlighting → Keywords ( Uppercase). Menu items that solely perform an action—from the perspective of the context menu—are simpler, because they do not need to manipulate the visual state of the menu tree at all.
  • Mutually exclusive set of menu items: As an example, in the Highlighting → Keywords and Highlighting → Variables submenus, the choices listed act like radio buttons. When you select one, the others are deselected. Because selecting a menu item also closes the menu, implementing this mutual exclusion is simpler than it is with actual radio buttons that remain visible after selection. The Command Completion submenu is a variation of this. It includes one menu item that stands alone, and four choices that comprise yet another mutual exclusive set.
  • Menu items tied to control properties: Although it's not obvious from the illustration, most menu items that set state are bound to control properties , so setting a menu state is isomorphic to setting a property value.

Creating the Context Menu

Listing 1 provides the source code for the two methods used in ChameleonRichTextBox code for the context menu: SetupContextMenu, which creates the context menu, and contextMenuStrip_Opening, which initializes the context menu when its opening event fires. These two methods work closely together to manage the characteristics of the menu: how you choose to build a context menu depends heavily upon the characteristics you wish to exploit.

You create the menu using a MenuBuilder support class, referenced on almost every line of SetupContextMenu. The code is quite straightforward when you break it down: each submenu first has a line to create the submenu, as in the line shown below. This adds a submenu to the top-level ContextMenuStrip. The display text of the submenu is contained in the constant SUBMENU_HIGHLIGHT; the use of a constant here is crucial, as you will see shortly:

ToolStripMenuItem highlightMenu = menuBuilder.CreateSubMenu(ContextMenuStrip, SUBMENU_HIGHLIGHT);

After the submenu creation, one or more lines populate the submenu, as shown below. Again, note the string constant for the display text in the second parameter. The third parameter specifies the event handler for the menu item, and the fourth specifies a property name, discussed next. Both the third and fourth parameters may be null. Also note that the CreateMenuItem method has a return value of the same type as CreateSubMenu, but because you don't need it for anything, it is not assigned to any local variable here:

menuBuilder.CreateMenuItem(highlightMenu, MENUITEM_ENABLE_HIGHLIGHT, enableHighlightMenuItem_Click, "EnableHighlighting");

The final parameter to the CreateMenuItem method, if supplied, is the name of a property in your control that this menu item reflects. This is most useful for Boolean properties and Boolean menu items, (i.e. menu items that are checked for an enabled condition and unchecked for a disabled condition). Because the description of such a property and menu item would be essentially the same in many cases, CreateMenuItem provides a mechanism for automatically applying the property description to the menu item tooltip. The automatic property description requires that you decorate your property with a DescriptionAttribute; CreateMenuItem automatically harvests the value. This line of code in CreateMenuItem retrieves the description:



item.ToolTipText = GetDescription(propertyName);

GetDescription does the real work: it retrieves the appropriate property, gets the DescriptionAttribute, and returns its description string:

private string GetDescription(string propertyName) { AttributeCollection attributes = TypeDescriptor.GetProperties(control)[propertyName].Attributes; DescriptionAttribute myAttribute = (DescriptionAttribute)attributes[typeof(DescriptionAttribute)]; return myAttribute.Description; }

Finally, SetupContextMenu method can create a line to separate disparate groups within a single level of the context menu. This example is used in the middle of the Tab Control submenu to provide a logical separation between the top and bottom halves of the submenu:

tabMenu.DropDownItems.Add(new ToolStripSeparator());

Initializing the Context Menu

As used here, the term "initializing" means set the state of each menu item to accurately reflect the application state. As the contextMenuStrip_Opening method name implies, this initialization happens "just in time"—as soon as the user opens the context menu. The code in this method may be split into three pieces of functionality, as I have notated in Listing 1:

  1. Resetting the entire menu tree to a known state
  2. Synchronizing simple menu choices to their corresponding properties
  3. Setting one of several menu items in a mutually exclusive set.
You use the All() extension method to reset the tree.

The ToolStripItemCollection.All() Extension Method

The All extension method for menu items is isomorphic to the All method discussed earlier in this article for controls. Using this helper, you can process all elements in a menu tree. In the following code, note the conversion from ToolStripItems to ToolStripMenuItems. Because a ContextMenuStrip maintains a collection of ToolStripItems, the method takes care to filter out items that are not menu items:

public static IEnumerable<ToolStripMenuItem> All(this ToolStripItemCollection items) { foreach (ToolStripItem item in items) { if (item is ToolStripMenuItem) { ToolStripMenuItem menuItem = item as ToolStripMenuItem; foreach (ToolStripMenuItem grandChild in menuItem.DropDownItems.All()) yield return grandChild; yield return menuItem; } } }

A control maintains a context menu in its ContextMenuStrip property, to which you assign a ContextMenuStrip control that embodies the context menu. The ContextMenuStrip control has an Items property that is a collection of ToolStripItems. Therefore, you can use a simple loop that applies the All extension method to process all menu items regardless of nesting depth. The contextMenuStrip_Opening method uses this notion to reset all menu items to a known state. The ChameleonRichTextBox happens to have all menu items that are Boolean indicators, so the code is almost trivial:

foreach (ToolStripMenuItem item in ContextMenuStrip.Items.All()) { item.Checked = false; }

If your context menu happens to have, for example, menu items that are disabled under certain runtime conditions, you could add a line to set the enabled state of each item to a default, by just adding item.Enabled = true; to the loop. It would then be up to the subsequent code in your menu-opening routine to selectively disable those that should be disabled.

Synchronizing Simple Menu States

After resetting the collection of items in a context menu to a default starting state, the next task is to set them to the appropriate state based on the application state. Recall that menu items were defined with string constants for display text. That makes it simple to use LINQ in conjunction with the All extension method to find any particular menu item again, so you can enable or disable it, or set its checked state:

ToolStripMenuItem targetItem = (ToolStripMenuItem)(from ToolStripItem item in ContextMenuStrip.Items.All() where item.Text.Equals(MENUITEM_ENABLE_COMPLETION) select item).Single(); targetItem.Checked = <state of some property>;

If more than one menu item with the same display text exists, the preceding code will throw an exception because the enumerated set will contain more than one value—and the Single query operator requires that it be a singleton set. You could work around the exception by using the First operator instead, but there is no guarantee that the item returned by First will be the one you need. As long as you're searching for a uniquely named menu item, this approach works fine.

Remember the old adage that if you have a hammer, everything looks like a nail? LINQ is not always the ideal solution. If you have a lot of menu items, repeating that LINQ construct does add some overhead. To avoid that, the MenuBuilder class has a simple associative reference that keeps track of every menu item you create. Remembering the reference (see the CreateMenuItemBase method) requires only a single assignment, where menuItem is an associative array (a Dictionary object), displayText is the display text of the menu item, and item is the menu item itself:

menuItem[displayText] = item;

Then you can fetch the stored reference to the menu item using this simple GetMenuItem accesssor:

public ToolStripMenuItem GetMenuItem(string displayText) { return menuItem[displayText]; }

The code on the application side replacing the LINQ construct shown above is just this assignment statement:

menuBuilder.GetMenuItem(MENUITEM_ENABLE_COMPLETION).Checked = <i><state of some property>;</i>

Section 2 of the contextMenuStrip_Opening method in Listing 1 shows several such lines in practice. Again, any such referenced items must have unique display text. The next section addresses menu items that are not unique.

Handling Non-Unique Menu Items

It's common to have menu items that are not uniquely named. The ChameleonRichTextBox has non-unique names in several locations in the menu tree. For example, in Figure 5, you can see that "Uppercase," "Lowercase," and "Default case" occur three times, at different points in the tree. The key to handling non-unique items is to notice that as you prune the tree, the occurrences of duplicate items diminish. From the top-level menu in Figure 5 you already know there are three occurrences of the menu item "Uppercase." Proceed down one level to the subtree rooted with the Highlighting menu item. This tree now has only two occurrences of "Uppercase." Proceed down further to the subtree rooted with the Keywords menu item and "Uppercase" appears only once. The point is, if you can search this individual subtree for the "Uppercase" item, you are guaranteed a unique occurrence.

For this situation, the MenuBuilder class provides a LINQ-based two-argument version of the GetMenuItem accessor method. The single-argument version in the last section used a parameter that held the display text of the menu item sought; the second argument here limits the context to a particular subtree rather than the entire menu tree. The method uses a LINQ query and the by-now-familiar All extension method to complete the task:

public ToolStripMenuItem GetMenuItem( string subMenuDisplayText, string displayText) { return (from ToolStripMenuItem item in menuItem[subMenuDisplayText].DropDownItems.All() where item.Text.Equals(displayText) select item).Single(); }

If you refer to the third section of the contextMenuStrip_Opening method in Listing 1, you'll see several instances that use this accessor, including the one shown below. You pass the submenu name as the first argument here, while the choice variable fills the second argument slot. The next section discusses the choice variable in the context of mutually exclusive choices:

menuBuilder.GetMenuItem(SUBMENU_VARIABLES, choice).Checked = true;

Author's Note: The ChameleonRichTextBox happens to satisfy the constraint that for any menu item, there exists a submenu in which the menu item is unique. But there is no guarantee that you can find a submenu where a particular menu item is unique; it is up to you to create the menu that way if you wish to use these techniques.

Synchronizing Mutually Exclusive Menu Choices

Figure 5 shows that the ChameleonRichTextBox control has four sets of mutually exclusive menu groups, indicated with purple brackets. In frame 4, for example, the Command Completion submenu has one menu item to enable/disable command completion, followed by a group of four mutually exclusive items that determine the behavior when command completion is enabled: default case, uppercase, lower case, or match user case. The control stores the desired behavior in its CompletionAction property, an enumeration that corresponds to the four menu choices. Whenever the user opens the context menu, you want to add a checkmark to the item corresponding to the current value of the CompletionAction property. This code fragment handles that by mapping the enumeration choices to the menu choices, then passing the mapped value to the GetMenuItem accessor you just saw in the previous section:

switch (CompletionAction) { case CompletionOption.ToLowerCase: choice = MENUITEM_LOWERCASE; break; case CompletionOption.ToUpperCase: choice = MENUITEM_UPPERCASE; break; case CompletionOption.ToUserCase: choice = MENUITEM_MATCH_USER_CASE; break; default: choice = MENUITEM_DEFAULT_CASE; break; } menuBuilder.GetMenuItem(SUBMENU_COMPLETION, choice).Checked = true;

As you have seen, the All() extension method is at the core of both the file resource manager and the context menu manager. Listing 2 provides the source code for this FormExtensions class, with the variants of this method; my CleanCode web site contains the FormExtensions API for reference. Listing 3, the source code for the ResourceMgr, brings together the techniques discussed for managing file resources; also see the ResourceMgr API. Listing 4 brings together all the code for the context menu builder; also see the MenuBuilder API:



Michael Sorens is a freelance software engineer, spreading the seeds of good design wherever possible, including through his open-source web site, teaching (University of Phoenix plus community colleges), and writing (contributed to two books plus various articles). With BS and MS degrees in computer science and engineering from Case Western Reserve University, he has worked at Fortune 500 firms and at startups, using C#, SQL, XML, XSL, Java, Perl, C, Lisp, PostScript, and others. His favorite project: designing and implementing the world's smallest word processor, where the medium was silicon, the printer "head" was a laser, and the Declaration of Independence could literally fit on the head of a pin. You can discuss this or any other article by Michael Sorens here.
Comment and Contribute

 

 

 

 

 


(Maximum characters: 1200). You have 1200 characters left.

 

 

Sitemap