Build Menus Dynamically

Enhance your UIs' flexibility with dynamic coding that uses runtime conditions to determine menu behavior.

Technology Toolbox: VB.NET

You can determine your program's behavior at either design time or run time. You're familiar with determining behavior at design time as you position controls and write code. When you define behavior at run time, you're doing dynamic programming. Dynamic programming has many uses. I'll show you how to base multiple document interface (MDI) menus on runtime conditions. Intelligent use of inheritance enhances this technique by organizing the code and providing a consistent interface. You'll also see how to use resources with the dynamic techniques to prepare the resulting menus for later localization.

You'll base the dynamic menus on the types in available assemblies. This lets you include your parent MDI form in the bootstrapping portions of your application, which change rarely. You can extend this technique for granular declarative authorization—a long name for specifying valid roles in the code itself. You might not use the technique for these reasons, but simply because it provides an easy way to write MDI behavior once and reuse it whenever you need it. You can use similar techniques with both hand-crafted and generated MDI child forms.

You'll often start an application with nothing to customize, but before you've even left development, you'll find something special to do. A base MDI form contains the bulk of the code. You derive another MDI parent form from the base form for any customization. Treating the base form as a component with reusable code and deriving a specialized form for your application is a fundamental pattern. The trick to using it successfully is that you always instantiate the derived class, even if there's nothing in it. This change can be transparent to external code only if the external code is instantiating the derived class already (see Figure 1 for a UML diagram of the dynamic-menu system).

Dynamic coding relies on underlying object-oriented programming (OOP) principles. In particular, derived forms are also the base form. You can identify them as such by using the TypeOf operator in VB.NET, "is" in C#, and the IsSubClassOf method when you work with Reflection. This article's code relies on this technique in handling the BaseEditForm and the derived classes specific to each functional area, such as customers or orders. BaseEditForm derives from Windows.Forms.Form, so you can rely on full WinForms behavior.

Inheritance works well only if you use a good strategy for overriding methods and properties. Simply marking all your methods and properties with Overrides isn't an adequate strategy. The key is to give your base class methods the appropriate granularity. For example, using separate overridable methods for each standard main menu option lets you override any piece selectively without needing to repeat code simply because it's in the same method.

However, VS.NET throws a glitch into the base/derived class strategy for the MDIParent forms. The menu designer in VS.NET can't design menus defined in the base class visually within a derived class. You have two ways to work around this limitation.

Add Menu Items Programmatically
You can create independent menus and merge them, or you can create the menu in the derived class, set a base-class variable to it at run time, then add standard and dynamic-menu items programmatically in the base form. I prefer the second approach. An overridable property in the base class, named MainMenu, makes this approach as flexible as possible. The base-class default provides a new menu. The derived class can override it to return the customized menu the original programmer designed visually.

The sample application's files are arranged into three assemblies, indicated by the background color in Figure 1 (download the sample code). You facilitate reuse by keeping the common functionality in a separate assembly. Splitting the two application-centric assemblies mimics the kinds of scenarios that arise in real-world applications that segment functionality to support development or deployment scenarios. Loading the assemblies—which you must do for Reflection to find them—is the only aspect of the dynamic menus this structure makes more difficult. You load them through a Touch method in a class named Singletons in the ApplicationStub assembly. Calling this method from the ApplicationMDIParent's constructor gets the application ready for dynamic-menu processing.

Now, take a look at the code that creates the dynamic menu (see Listing 1). The form's OnLoad event calls methods to add standard menus (New, Save, and so on) and BuildMenuFromAssemblies. If a main menu isn't available, the application throws an exception and exits; otherwise, the method retrieves the available assemblies. You can provide assemblies either by retrieving all loaded assemblies or by overriding this behavior and specifying assemblies in the ApplicationMDIParent. You should ignore Framework assemblies, because their thousands of types are of no interest. The code ignores everything in the System classes and compares the assembly name to an array of values. This array, which contains mscorlib and Microsoft.VisualBasic, is protected so you can add types from your own assemblies that you'd like to skip checking. You might use this technique to avoid checking all the objects in your business tier.

The method traverses the array within the appropriate assemblies, looking at each type within the assembly. The code to select types for the menu is a little messy, so you offload it into the IsValidChildForm function. You need to take a little detour into types and attributes to understand this function. An attribute is a way to decorate an assembly, class, or method with an entirely independent class—a capability with powerful implications but a simple design. This code instantiates two additional classes when it instantiates the Customers class:

<DynamicSupport.BaseMenu( _
	"Purchases"), _
Public Class Customers 
	Inherits DynamicSupport.BaseEditForm 

You declare attributes with the Attribute suffix, but use them without it. The BaseMenuAttribute class's constructor takes a parameter, and the IncludeInMenuAttribute class doesn't. Defining these classes is quite simple. One adds the predictable backing field, parameterized constructor, and single read-only property, and the other takes only three lines:

Public Class IncludeInMenuAttribute 
	Inherits System.Attribute
End Class

Retrieving custom attributes for types is a tad ugly. A GetCustomAttribute method doesn't exist as it does for the assemblies, so the Utility class's GetCustomTypeAttribute method loops through the CustomAttributes to search for a match. It requires that both the attribute name and namespace match, to avoid collisions. This method returns either a custom attribute or Nothing if there is no corresponding attribute.

Evaluate the Type
The IsValidChildForm method performs four tests. If the type is specifically excluded through an attribute, the method does no further testing and returns False, and the type isn't processed. If the type is explicitly included, derives from BaseEditForm, or implements the IProcess interface, the method returns True and the type is included in the menu. IsValidChildForm illustrates the three primary mechanisms for evaluating types for dynamic programming: attributes, base classes, and interfaces.

At this point, you know the type belongs in a menu, and you need to decide where to position it and what caption to use. The BaseMenu attribute identifies the menu where you'll add the new item by its caption. A little extra work here prepares your application for use in other cultures (see the sidebar, "Use Resource Files for Localization"). The dynamic menus support isolation of strings, which is the first step in localizing your application. The GetBaseMenu function uses an existing string but allows resource-file values to override it as needed. Ultimately, you determine the base menu by captions—either the ones you specify or their corresponding resource-file values.

You need to put the new item somewhere if the original programmer didn't provide the BaseMenu attribute. The GetDefaultMenu provides this default. Granularity is important here. You want to allow ApplicationMDIParent to override only the default menu position, without repeating any of the complex code in GetBaseMenu. The separate GetDefaultMenu enables you to do this. The GetBaseMenu method's second override simply finds the correct menu, based on the caption.

When you're ready to display an instance of the type later, you'll need to know which type to use. The easiest way to do this is to store it when you create the menu item by deriving from Windows.Forms.MenuItem and adding a ShowType property to access the type along with a corresponding backing field. This derived MenuItemWithType class's constructor also takes the standard MenuItem parameters and passes them on to the Windows.Forms.MenuItem constructor.

Returning to BuildMenuFromAssembly, you can see the new menu option is instantiated and added to the base menu. At this point, the method also defines a delegate that fires when the user clicks on the new menu item. This delegate points to the mnuDynamicItem_Click method (see Listing 2). Note the pattern used to handle this event. Defining a simple event handler that simply calls a protected method makes it easy to work with the method rather than the event. Events are great, but you have no control over how base-class and derived-class events fire. All registered event handlers fire. You return control of managing the interactions between base and derived methods to the original programmer by switching to a method-based approach as early as possible.

The method can't do anything and quietly exits if the sender's type isn't MenuItemWithType. This is a valid situation, because the original programmer might have forced attachment of a type (through the IncludeInMenu attribute) and be providing behavior in an overriding method. You must assume your methods will be called both in the context you expect and from a derived-class method that enhances your functionality.

The mnuDynamicItem_Click method retrieves the type from the MenuItemWithType derived menu item and creates an instance of the class described by the type. Not much else happens if this type isn't a recognized type. You must know which methods to call in order to work with dynamic coding. Your primary tools are base classes and interfaces. You can cast and call the required methods, because the common base class or interface guarantees that an implementation exists. In this case, you assign a handler for the StateChanged event and call a special show method.

Manage State
The StateChanged event is core to state management (see Figure 2). State management allows the menus to reflect the current condition of the application's data. You don't want the Save menu option enabled until the user changes data. You probably don't even want it visible when no form is open.

The MDI child form knows when an event happens that might change the state. It raises a StateChanged event. The MDI parent is listening to this event. When the event fires, the MDI parent calls some rather ordinary code (in the SetState method in the sample code) that queries the child for currently valid operations:

If TypeOf sender Is BaseEditForm Then
	frm = CType(sender, BaseEditForm)
	Me.mnuSave.Enabled = frm.CanSave
	Me.mnuNew.Enabled = frm.CanMakeNew
	Me.mnuClose.Enabled = frm.CanClose
End If

The mnuSave, mnuNew, and mnuClose variables are class fields in BaseMDIParent that contain references to some of the standard menu items. These class fields are protected, so you can create them in the ApplicationMDIParent if the implementation in BaseMDIParent doesn't fit your application.

BaseMDIParent sends an event to the active MDI child when the user clicks on one of these standard menu options. Again, the example application uses the quick pass-off approach to put the functionality in an overridable method and return control over base/defined method interactions to the original programmer, who understands the inheritance hierarchy. It then casts to the BaseEditForm so you have a standard set of properties and methods. For example, if the user clicks on the Save menu option, BaseMDIParent calls DoSave in the BaseEditForm, which your derived classes might override:

Private Sub mnuSave_Click(ByVal _
	sender As Object, ByVal e As _
	OnSave(sender, e)
End Sub
Protected Overridable Sub OnSave( _
	ByVal sender As Object, _
	ByVal e As System.EventArgs)
	If TypeOf Me.ActiveMdiChild Is _
		BaseEditForm Then
	CType(Me.ActiveMdiChild, _
	End If
	End Sub

The preceding pattern provides an amazing amount of flexibility. It doesn't matter how many tabs and user controls your derived editing form has. The form can participate in dynamic menuing, regardless of its complexity, as long as it can fill the base-class contract. This complex functionality lies in the interaction among the classes. The individual classes stay quite simple, at least in regard to dynamic programming. In fact, if you built an interface for the BaseEditForm, it would have only 10 properties and methods. Building an interface in this way would allow classes that don't derive from BaseEditForm to participate in dynamic menuing.

In the broader picture, dynamic programming relies on five important .NET features. Reflection provides four of them: discovering class details, creating objects, invoking methods, and decorating classes and methods with attributes. You realize the power of the Reflection features when you team them with contracts about the nature of the unknown objects you're working with. You express contracts about types as base classes and interfaces, which work equally well in dynamic programming.

You can take the example code further by adding a toolbar, refining the child events, implementing alternate interfaces—perhaps for read-only classes—adding richness with attributes, and supporting satellite assemblies. Keep an eye on the underlying pattern to appreciate how key Reflection features working with contracts allow you to define an application's features at run time.

comments powered by Disqus
Most   Popular