devxlogo

Using LINQ to Manage File Resources and Context Menus

Using LINQ to Manage File Resources and Context Menus

anguage Integrated Query, or LINQ, is a new Microsoft technology introduced with the .NET 3.5 framework. This Microsoft quote describes its purpose succinctly:

“Traditionally, queries against data are expressed as simple strings without type checking at compile time or IntelliSense support. Furthermore, you have to learn a different query language for each type of data source: SQL databases, XML documents, various Web services, and so on. LINQ makes a query a first-class language construct in C# and Visual Basic. You write queries against strongly typed collections of objects by using language keywords and familiar operators.”

The main tasks you can perform with LINQ include:

  • Retrieve a subset of elements.
  • Retrieve a sequence of elements transformed to a new type of object.
  • Retrieve a singleton value about the source data, such as the largest element or the count of elements.

Traditionally, those types of operations are done on a database. While LINQ of course supports the database domain, the power of LINQ is that it can be used in the very same fashion when accessing data from any of these domains (linked to the MSDN reference pages for each technology):

The main advantages of using LINQ syntax include:

  • Code is more concise and readable, especially when using multiple filters.
  • You can filter, order, and group results with little coding.
  • You can easily port a LINQ query from one data source to another with little or no modification.

Author’s Note: I assume you have had some exposure to LINQ in this article; if not, take a break from this article now and peruse some of the material available on Microsoft’s site, particularly at the LINQ Home, or other tutorials.

This article explains some simple but useful applications of LINQ that lie at the heart of two moderately complex components that you can incorporate as building blocks in your production applications. The two components are quite different on the surface: one involves managing an application’s external file resources, while the other involves creating and initializing items in a context menu.

What This Article Covers

  • Techniques to Manage External File Resources: Package files with your applications and libraries as resources and then externalize them, i.e. unwrap them and use them as needed.
  • Techniques to Manage Context Menus: Create an arbitrarily complex context menu, set states, and manage mutually exclusive sets of menu items using a menu builder that leverages LINQ.
  • Using LINQ with .NET 2.0: Nominally for use with .NET 3.5, you can easily deploy LINQ into a .NET 2.0 environment (although you do need to compile the code with Visual Studio 2008).

What You Need

  • Visual Studio 2008
  • C# 3.0
  • .NET Framework: 2.0 or higher

LINQ to Objects

This article focuses on useful LINQ to Objects techniques. In this context, an object refers to any C# or Visual Basic object that implements the generic IEnumerable interface; that is, collections, arrays, lists, dictionaries, or your own user-defined types. This covers a vast spectrum of the data types that deal with groups of data?even more than you might think at first glance?because some types that are not IEnumerable are convertible to types that are. To quote MSDN again:

“In a basic sense, LINQ to Objects represents a new approach to collections. In the old way, you had to write complex foreach loops that specified how to retrieve data from a collection. In the LINQ approach, you write declarative code that describes what you want to retrieve.”

This is a remarkable achievement: it adds a layer of abstraction in your code where you can now describe what you want rather than how to go about it. Higher levels of abstraction lead to easier to read?and therefore more maintainable?code. Think of assembly language compared to C#, for example. Usually, greater abstraction has a cost of greater overhead, but my impression is that the overhead of LINQ is minimal, whether you use query expression syntax or method-based (also referred to as lambda) syntax.

The two types of syntax available for LINQ queries are exactly equivalent where they overlap, so which you use is merely a matter of personal preference. The performance question is moot; they perform exactly the same, because compilation converts expressions written in query syntax to lambda syntax as the first step. The lambda syntax, however, is much richer, particularly in C#. See Query Expression Syntax for Standard Query Operators for a reference chart that shows the subset of methods that may be invoked in query syntax in both C# and Visual Basic. If you are relatively new to LINQ, here’s a great chart that shows how to write expressions in both query syntax and in lambda syntax side by side.

LINQ, the .NET Framework, and Extension Methods

LINQ, by default, requires the .NET 3.5 framework. At the time of writing, though, .NET 3.5 is still relatively new and it would be unrealistic to expect that all your potential users have it, particularly in corporate environments where upgrading infrastructure can be costly. If you only need LINQ-to-Objects and do not need the other flavors of LINQ listed above, the open source LINQBridge library provides a clever solution that lets you operate with the .NET 2.0 framework. As explained on the LINQBridge page, LINQ queries do not depend on the 3.5 framework per se; rather, they depend on the presence of certain method signatures. It does not matter where those methods reside, so the LinqBridge.dll library file supplies them in a 2.0 environment. You will need to use Visual Studio 2008 though; 2005 is not sufficient. VS2008 provides a multi-targeting feature, allowing you to specify which framework (2.0, 3.0, or 3.5) to compile to. To use LINQBridge, therefore, set the target framework on the Application page of your project properties to 2.0, and then add a reference to the LINQBridge.dll library to your project.

Since you must use Visual Studio 2008 you are therefore automatically using C# 3.0, independent of which framework version you target. One other key feature of C# 3.0 is extension methods. Extension methods let you add methods to existing types without subclassing, recompiling, or modifying the original type in any way. You simply define a new method with a special signature, which you may then attach to an existing class with a simple using statement. Oddly enough, you define such a method as a class method yet use it like an instance method.

LINQ itself makes extensive use of extension methods for the standard query operators: Where, Select, GroupBy, etc. are all extension methods defined on the IEnumerable type. Including a using System.Linq statement in your code exposes these methods for you to freely use in LINQ queries against IEnumerable objects.

With the foundation of LINQ and extension methods laid you now have a good base upon which to build. The rest of this article will discuss a fairly simple technique involving both LINQ-to-Objects and extension methods. It is used at the heart of two moderate complexity components that could themselves be immediately incorporated as building blocks in a production application. What is interesting is that the two components are quite different on the surface: one involves managing an application’s external file resources, while the other involves initializing choices in a context menu upon a right-click to open the menu.

Practical Techniques: Managing External File Resources

Often an application requires files other than just the executable and associated DLLs, such as XML files, text files, languages files, etc. One approach to file management is to package these files with an installer for your application. A second approach, taken here, is to include these files as resources packaged within your executable or DLL, which are then unpackaged not by an installer but at runtime, by the application itself and only if needed. Often you may allow or even encourage users to edit such externalized files; that is a common reason for making them external. But as an application designer you must decide wisely when?or if?to regenerate these files. You do not, for example, want to recreate them every time the application starts; otherwise your users would lose any changes they might have manually made. A good compromise is to overwrite an existing version of the file only when a new version of an application runs for the first time?and only then if it is not protected with the read-only attribute. This strategy lets each user decide whether to keep user changes from version to version or get a fresh (and possibly updated) version with any new release.

Another advantage is that you need to externalize a file only when it’s needed. An application may run many times before that happens.

?
Figure 1. Resource Management Algorithm: The basic algorithm for managing externalizable file resources is split into two parts in a common library.
Author’s Note: The flowchart in Figure 1 illustrates the concept only; it’s not an accurate model of the code itself. Your application code invokes ResetResources indirectly, through a higher level ResetAllResources method, which accommodates a more realistic, hierarchical presentation layer.

Figure 1 formalizes this idea into a detailed algorithm. Starting from the application launch, you first check if this is the first time the user has executed this version of the application. If so, your application calls a common library ResetResources method that determines whether to delete the existing file or leave it alone based on whether a user has explicitly set the read-only attribute to preserve it. After completing this “new version” check, the program continues with other initialization business. At a later point, when you need to use a managed resource file, invoke the InstantiateResource method to guarantee the file exists. Your execution path arrives at InstantiateResource with your resource in one of these states:

  • An existing, write-protected file that the user does not want to update;
  • An existing, writable file that still exists because this is not a new version of the application;
  • No file if the application was never executed before; or
  • No file if this is a new version and the file was not write-protected.

To ensure that the file exists upon return, InstantiateResource first checks if it already exists. If so, it does nothing further. If the file does not exist, it creates it by externalizing the application resource into a file. In either case, your code may then proceed to use the file with the assurance that it is present.

The algorithm is separated in two parts for several reasons. First, files may be deleted for a variety of reasons unrelated to the running of this application. Good design, therefore, suggests that the application must check for a needed file just before using it or risk a runtime exception if it does not exist. Just as a runtime loads assemblies only when they are about to be used, this technique can generate a file on a just-in-time basis. Because a file may not be needed during any given run of the application, its use may or may not coincide with a new version of the application being run for the first time.

You do not, however, want to generate the file every time the application runs, because that would potentially overwrite a user’s manually entered changes. Furthermore, separating out the pruning step allows it to be encapsulated into a code library with completely generic code. Any and all classes that use file resources can then reuse the library code.

To further explore the workings of this seemingly straightforward algorithm, take a look “under the hood” in the following sections that expound upon its details with actual code examples.

Detecting a New Application Version

A simple technique to detect a new application version is by defining an application setting?call it NewVersion or something equivalent. Defining this as a Boolean with an initial value of True, you can then include this code in your initialization:

   if (Properties.Settings.Default.NewVersion)   {      // migrate settings from previous version, if any      Properties.Settings.Default.Upgrade();         // deactive the NewVersion flag for subsequent invocations      Properties.Settings.Default.NewVersion = false;         // Include this here or during your shutdown code to save the above changes      Properties.Settings.Default.Save();         // Reset all file resources associated with this WinForm application      Collection l = ResourceMgr.ResetAllResources(this);   }

When a new version runs for the first time, it retrieves the NewVersion setting from the application.exe.config file, because no user.config file will exist yet. NewVersion will therefore be true and the conditional code above will execute. The Upgrade() call migrates any settings from the previous version?quite useful to ensure you do not lose user customizations. Then the code sets NewVersion to false. That value gets stored in the newly created user.config file with the call to Save because application.exe.config is always considered read-only. Subsequent invocations will still read the application.exe.config file but will override any settings with values from the user.config file, so the conditional code will never execute again with this version of the application.

The File Resource Demo Application

The static ResetAllResources method performs the first part of the algorithm, the part that purges any externalized file resources. Using LINQ queries, the method locates file resources attached to any top level controls or forms plus any attached to their child controls or forms. The File Resource Demo application presented next elucidates the details.

Figure 2 shows a class diagram for a sample WinForm application (MainForm) whose principal control is a SqlEditor component. This control is itself comprised of two other controls, a ChameleonRichTextBox and an ExtendedDataGridView. The SqlEditor control also includes a child form, an instance of an internal StandardQueryPickerForm, which contains a QueryPicker control. These controls are real, production components that are part of my open-source CleanCode library. (The hyperlinks on each control take you directly to the API description for each control.) The File Resource Demo application, however, uses stub classes of the same name as each of these controls for illustrative purposes. Figure 3 shows a screenshot of the demo application. The colored text makes it easy to distinguish the output from each button.

?
Figure 2. Class Diagram of the Demo Application: This chart illustrates the relevant associations between hierarchically structured subforms and subcontrols in the File Resource Demo. It also shows the controls that implement the IResourceUser interface, which allows the parent application to find the controls that use managed file resources without having advance knowledge.
?
Figure 3. File Resource Demo Screenshot: This demo application simply shows the color-coded text shown for each button: (1) The Go button reveals the five direct child controls shown on the form. (2) The Go Deep button reveals all nested controls, exposing those inside the nested structure on the left half of the form. (3) The Go Filter button does the same but filters the list to only those that implement the IResourceUser interface as noted in Figure 2.

Here’s a brief description of how each button generates its output, followed by a detailed discussion about how the queries actually work.

The “Go” button simply enumerates all the controls in the application. The Controls collection of a form includes only its direct children, in this case three buttons used for input, a TextBox used for output, and the SqlEditor control (on the left). This code outputs the text highlighted in green in Figure 3:

   private void goButton_Click(object sender, EventArgs e)   {      foreach (Control control in Controls)         EmitControlInfo(control);   }   

The “Go Deep” button uses the All extension method, which collects not only a form’s direct children, but also performs an exhaustive probe for all controls that are descendants of the main form. To accommodate child forms as well as child controls, the code includes a separate loop for the known subform included in the SqlEditor control:

   private void goDeepButton_Click(object sender, EventArgs e)   {      foreach (var control in Controls.All())         EmitControlInfo(control);      foreach (var control in sqlEditor.queryForm.Controls.All())         EmitControlInfo(control);   }   

The “Go Filter” button performs the same operations as Go Deep, but filters the output using a standard LINQ extension method called OfType to return only those controls that implement IResourceUser:

   private void goFilterButton_Click(object sender, EventArgs e)   {      foreach (var resourceUser in Controls.All().OfType())         EmitControlInfo((Control)resourceUser);      foreach (var resourceUser in         sqlEditor.queryForm.Controls.All().OfType())            EmitControlInfo((Control)resourceUser);   }

The Controls.All() Extension Method

The All extension method is a handy mechanism for performing a deep search for all nested controls contained within a Control collection. I found the version shown here on David Findley’s blog page, entitled The ?ber-Find Control (you will see an adaptation of this in the discussion of context menus later in the article):

   public static IEnumerable All(this Control.ControlCollection controls)   {      foreach (Control control in controls)      {         foreach (Control grandChild in control.Controls.All())            yield return grandChild;            yield return control;      }   }

This method takes as input a collection of direct child Controls and returns an IEnumerable collection of all descendant controls. The magic in this method stems from the yield statement, also new in C# 3.0. The presence of the yield statement indicates that the method which contains it is an iterator block, which typically means that the method must return some variation of an IEnumerable collection. So this method implicitly creates and returns a collection on the fly. Because LINQ has a great affinity for IEnumerable collections, you can wire this method into a chain of LINQ operators as shown in the goFilterButton_Click method earlier.

The IResourceUser Interface

The Go Filter button code filters the list of controls returned by the All method to just those that implement the IResourceUser interface. The interface is quite simple; it requires implementing only one method, which returns a collection of file names it has processed:

   public interface IResourceUser   {      Collection ResetResources();   }

The ResetAllResources Method

The ResetAllResources method?the method called when a new version is detected?is reasonably straightforward. You pass in one or more forms to process?the main form and any sub-forms. The code uses the same iteration as the Go Filter button in the earlier demo; however rather than displaying only the name and type, because the object is known to implement IResourceUser, the inner loop calls the method required by its contract, ResetResources. The method returns a collection of file names that were purged. It adds that collection to the cumulative names list, and eventually converts that list to a collection and returns it.

   public static Collection ResetAllResources(params Form[] forms)   {      List names = new List();      foreach (Form form in forms)      {         foreach (Control c in form.Controls.All())         {            if (c is IResourceUser)            {               names.AddRange(((IResourceUser)c).ResetResources());            }         }      }      Console.WriteLine(string.Join(", ", names.ToArray()));      return new Collection(names);   }

The inner loop takes advantage of the All extension method but does not add any LINQ leverage beyond that. Here’s a better rendition that uses LINQ query expression syntax. This query returns just those controls that implement IResourceUser, and then loops through that collection. No conditional code is needed so it is less complex and easier to read.

   public static Collection ResetAllResources(params Form[] forms)   {      List names = new List();      foreach (Form form in forms)      {         var resourceUsers = from control in form.Controls.All()                        where control is IResourceUser                        select control;         foreach (var c in resourceUsers)         {            names.AddRange(((IResourceUser)c).ResetResources());         }      }      Console.WriteLine(string.Join(", ", names.ToArray()));      return new Collection(names);   }

In this particular case, you can continue to leverage LINQ but do it more concisely by switching to the lambda syntax. Here you chain the LINQ OfType operator to do exactly what is needed, eliminating both the conditional code and practically the query as well.

   public static Collection ResetAllResources(params Form[] forms)   {      List names = new List();      foreach (Form form in forms)      {         foreach (var resourceUser in form.Controls.All().OfType())         {            names.AddRange(resourceUser.ResetResources());         }      }      Console.WriteLine(string.Join(", ", names.ToArray()));      return new Collection(names);   }

The next section explains design decisions when implementing the ResetResources method.

The IResourceUser.ResetResources Method

You use the IResourceUser interface when you have embedded file resources that you wish to externalize at first use yet allow for periodic updating. This interface provides a callback to remove the externalized versions so they may be refreshed (typically when a new version is detected).

A typical implementation of the IResourceUser.ResetResources method calls the eponymous static method of the ResourceMgr class. This example erases all XML files tied to this application.

   public Collection ResetResources()   {      return ResourceMgr.ResetResources("*.xml", ref resetResources);   }   private static bool resetResources;

The Boolean flag lets multiple instances of a class all make the call, even though only the first one will actually reset the resources (because external resources are by definition class resources).

Note that you do not specify a path, just a file name or file name pattern (for multiple files). The application stores externalized files in a subdirectory of the system-defined Application Data directory, where the subdirectory name is the base name of the application. For example, if your application is named MyProgram.exe and the user’s is “smith,” the folder is ?smithApplication DataMyProgram.

Author’s Note: The initial portion of the path depends upon which version of Windows you are running.

An application with only one form or window is fairly simplistic; it is more realistic to consider those with multiple forms. For such applications you need a different implementation for the ResetResources method. If you refer back to the goFilterButton_Click method of the demo application, you’ll notice that there are actually two loops, one that processes controls for the main form, and a second that processes the controls of the sqlEditor control’s queryForm. That explicit code references the known subform of one of its controls?but it is impractical for applications to have to know about all of the subforms used by all of its controls. Even if they did initially, library changes could easily change the list of subforms. Instead, the architecture of IResourceUser lets each control be in charge of its own subforms, and to handle these through the ResetResources method. The implementation of this method should include calls to the static ResourceMgr.ResetAllResources method for any of its child forms?the same top-level method called initially. You simply supply a list of arbitrary length enumerating all subforms for the control. This will then search for embedded controls in each. Generically, the code is just:

   public Collection ResetResources()   {      return ResourceMgr.ResetAllResources(subform1, subform2, ...);   }

With this in mind, take a look back at Figure 2. Three relevant classes in the CleanCode library implement IResourceUser (see Table 1).

Table 1. Three Implementations of IResourceUser: These three controls each implement IResourceUser, but for different reasons.
ControlReason to Implement IResourceUserBody of ResetResources Method
SqlEditorContains a subform
return ResourceMgr.ResetAllResources(
queryForm);
ChameleonRichTextBoxUses up to four XML files
string CONTEXT_FILE_FORMAT = 
"Context-{0}.xml";
return ResourceMgr.ResetResources(
string.Format(CONTEXT_FILE_FORMAT,
"*"), ref resetResources);
Query PickerUses one XML file
return ResourceMgr.ResetResources(
"QueryLibrary.xml",
ref resetResources);

Each control implements IResourceUser for exactly one of the two possible reasons (subforms or files). If some control happens to qualify on both reasons, then its ResetResources method should include calls to both methods in the ResourceMgr class, combining the collections returned from the two into a single return value.

To summarize, there are two reasons controls need to implement IResourceUser:

  • To manage external resource files used directly by a control.
  • To manage potential external resource files used by any subforms used by a control.

To err on the conservative side, whenever you use a subform you could just assume it has descendants that have resource files to manage. If it does not, there are no harmful consequences; no action will be taken.

The ResourceMgr.ResetResources Method

The static ResourceMgr.ResetResources method invoked by some instances of the IResourceUser.ResetResources method is the final piece of code needed to complete the picture. It attempts to delete all the files matching the filePattern supplied in the subdirectory of Application Data named for the currently running application. Even if two different applications use the same library on the same machine, each will have its own copies of externalized file resources, so there is no contention or confusion between applications. The method adds any files that it fails to delete to the returned collection, but with an error indicator prefix and a suffix indicating why it failed. The resetResources flag, as mentioned earlier, is so that only one instance of any class will execute the main code. Any subsequent attempts will be blocked by the gatekeeper flag. Here’s the code:

   public static Collection ResetResources(      string filePattern, ref Boolean resetResources)   {      Collection names = new Collection();      if (!resetResources)      {         resetResources = true; // flag that it has been done         string appDirName = ApplicationSpecificApplicationData();         string[] fileList;         try { fileList = Directory.GetFiles(appDirName, filePattern); }         catch (DirectoryNotFoundException) { fileList = new string[] { }; }         catch (PathTooLongException) { fileList = new string[] { }; }         foreach (var fileName in fileList)         {            FileInfo fileInfo = new FileInfo(fileName);            try            {               fileInfo.Delete();               names.Add(fileInfo.Name);            }            catch (Exception e)             { names.Add("*** " + fileInfo.Name + ": " + e.Message); }         }      }      return names;   }      public static string ApplicationSpecificApplicationData()   {      return Path.Combine(         Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),         Path.GetFileNameWithoutExtension(Application.ExecutablePath));   }

That completes the discussion of how to purge resource files generically when a new application version runs. The next section examines the second part of the algorithm: regenerating the necessary resource files.

The ResourceMgr.InstantiateResource Method

Typically, the InstantiateResource method is all you need to generate or regenerate a resource file stored in the exe or dll. As an example, consider the QueryPicker control from the CleanCodeControls library, which contains a resource file named QueryLibrary. The control calls InstantiateResource to create the file when it does not exist. The call returns the full path and name of the externalized file, ready for use:

   string fileName = ResourceMgr.InstantiateResource(      "QueryLibrary.xml", Properties.Resources.QueryLibrary);

The code for the InstantiateResource method is relatively simple: it generates the full path name, checks whether the file exists, and if not, creates the new file, writes the passed-in contents, and returns the full name:

   public static string InstantiateResource(string fileName, string resourceText)   {      string appDirName = ApplicationSpecificApplicationData();      string fullName = Path.Combine(appDirName, fileName);      if (!File.Exists(fullName))      {         try         {            DirectoryInfo appDirInfo = new DirectoryInfo(appDirName);            if (!appDirInfo.Exists) { appDirInfo.Create(); }            File.WriteAllText(fullName, resourceText);         }         catch (Exception) { /* no action */ }      }      return fullName;   }

Storing a File as a Resource

?
Figure 4. Packaging a File Resource: The figure shows the steps to embed a file resource in your application.

Figure 4 shows the steps to include a file resource into your application (.exe) or library (.dll). First, open the properties editor (1) from the Solution Explorer. Select the Resources page (2). In the resource type drop-down (3) select files. In the Add Resource drop-down select Add Existing File (4). Select the file you wish to include (5). The file is added to the canvas as a resource (6).

The only remaining piece you need to know to use embedded resource files is how to store one in your executable in the first place. Note that you may attach resources to your Visual Studio project whether it is an executable (EXE) or a library (DLL). The techniques for handling such resources are identical in either case. There are several different types of resources that Visual Studio knows about, but for this discussion the file resource is the only one of interest.

Open the Resources page of the properties editor in Visual Studio. The Resources page defaults to showing image resources on the canvas. Switch to the collection of file resources using the resource type drop-down selector. The canvas then shows any file resources in your project, and you may add or delete additional file resources. Visual Studio derives new resources names from the name of the file you selected, converted to a valid identifier name. Typically this is just the file name without the extension; however, the domain of valid characters for file names is slightly larger than the domain of identifier characters, so some characters may be either deleted or mapped. In Figure 4, which illustrates the entire procedure, the example file name Context-Oracle.xml gets mapped to Context_Oracle. Note that the hyphen in the file name gets mapped to an underscore in the resource name.

Whenever you add a resource, Visual Studio generates backing code so that you can access the resource from your application via the Properties.Resources class. Continuing the example in Figure 4, you would use the expression Properties.Resources.Context_Oracle to access the resource. That expression returns the contents of the original file. The previous section shows an example of using a file resource such as this in the call to InstantiateResource.

Dynamic Resource Lookup

The previous section described how to use file resources when you can hardwire the resource name a priori. As an added bonus, this section describes how to dynamically access a resource when the name is not specified in advance. That is, instead of using code like this (as was shown from the QueryPicker control)…

   string fileName = ResourceMgr.InstantiateResource(      "QueryLibrary.xml", Properties.Resources.QueryLibrary);

… you could use code like this (drawn from the ChameleonRichTextBox control):

   string filename = ResourceMgr.InstantiateResource(      string.Format(CONTEXT_FILE_FORMAT, contextName),         GetResourceText(contextName));

The ChameleonRichTextBox control manages a set of file resources; a user’s choice from this set is stored in the contextName variable. That value is passed to its private GetResourceText method to dynamically grab the resource associated with that particular name:

   private string GetResourceText(string contextName)   {      ResourceManager rManager =         new ResourceManager(            "CleanCodeControls.Properties.Resources",            typeof(CleanCodeControls.Properties.Resources).Assembly);      string context = rManager.GetString(         string.Format(CONTEXT_RESOURCE_FORMAT, contextName),         CultureInfo.CurrentCulture);      if (context == null) { throw new ArgumentException("resource not found"); }      return context;   }

The code fragments above rely on these constants to map the user’s input to both a file name and a resource name. Compare these formatting templates to the names shown in Figure 4 to see how they match up:

   private const string CONTEXT_FILE_FORMAT = "Context-{0}.xml";   private const string CONTEXT_RESOURCE_FORMAT = "Context_{0}";

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.
#CategoryStartup overheadInvoking overheadMaintenance
1Create and initialize a menu as a single operation at the time it is invoked via right-click.LowHighLow
2Create a menu during program startup and initialize it at the time it is invokedMediumMediumLow
3Create 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.HighLowHigh

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      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 = ;   

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 =      ;

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:

   
devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist