Mega Code Archive

 
Categories / Delphi / Examples
 

Factory patterns for menu operations

In this article I press the Factory pattern into service for menu operations. Are You Being Served? In this article we take a short break from the rigours of implementing business objects to deal with something that almost every application requires: a menu. Not a menu in the sense of a TMainMenu derivative, but rather some sort of front-end which shows a number of available options and requires one to be selected by the user. A typical example would be a list of options presented to the user when an application starts: they are required to select whether to launch the main application, or maybe the reporting package, general utilities or admin systems. These options are typically presented to the user in fairly crude ways, maybe a small form with a number of buttons each with a caption describing the application to be run, or a list box with a number of entries. Most often these menu-type forms are hard coded and intrinsically “know” how to launch the selected option. Such solutions share two properties: they’re inflexible and unattractive. This month we use the Factory pattern (and judicious choice of visual component) to provide a better solution. Going to work A factory pattern should be used when “a class can’t anticipate the class of objects it must create”. Essentially a client object uses the properties and methods of an abstract ancestor object, while in actual fact it is manipulating concrete descendants that have been supplied by the class factory. The key to the design is that the client does not need to know any specific details about the actual objects it is manipulating, except those that they share in their common ancestor. This effectively decouples such classes at compile time. With respect to our required menu screen in Delphi, this means that the menu form does not need to use the units that contain the forms to be displayed when a particular menu item is selected. This elegance provides us with the possibility of placing such forms inside DLL’s, that are dynamically loaded when the use selects the appropriate menu option. This has the obvious benefits of memory consumption (only the menu option actively selected by the user at runtime consumes memory), and that the application can be extended by simply supplying a new DLL – the existing menu screen can simply detect it’s presence and add it to the list presented to the user. Initially, we will consider the case of a statically-linked set of menu operations; that is, all available operations will be compiled within the application. Note, however, that this is simply a convenience; the menu screen itself still has no explicit knowledge of the operations available, or how they are linked into the application. Instead, each menu operation will register itself with the factory and the menu form will interrogate the factory for the available entries. The menu form is then free to present these entries in any way it sees fit. When the user selects a menu entry, the factory is requested to construct a concrete instance of the particular type and this is then displayed. The factory itself has a very simple interface. It has a method called Register that is used by the menu entries themselves to notify the factory of their existence (I have used Register as it mimics the nomenclature used by other factories within Delphi itself, such as the registration of components for display in the component palette). Additionally, the factory has two properties that let another class observe the number of members within the factory, and provides access to each. Listing 1 shows a complete implementation for this factory. We now need to add some menu operations to the factory. This is very simple: create some new forms, add the MenuActionFactory unit to the uses clause in the implementation section, and add the following code to the foot each form unit: initialization MenuActions.Register (TMyNewForm); The TMyNewForm reference should be replaced with the class name of the particular form for the unit in question. Now that our class factory has been initialised, we can turn our minds to how it can be applied. A la carte menus As previously observed, many “obvious” ways of requiring the user to select one choice from many are visually unappealing. A vertical list of wide buttons (with long captions) is a particularly unattractive but surprisingly common solution. Consider how Windows resolves a similar user interface issue: the Control Panel is a good example of a list of disparate operations. This is nothing more than a list view, but a set of well designed icons and consistent layout goes a long way to providing an attractive interface. Add to this the in-built ability of a list view to rearrange it’s icons as it is resized, and to display icons of different sizes or a more descriptive report view, and you have an almost ideal selection tool. To create a new menu screen, drop a list view on a blank form and align it to the client area. I like to drop a TCoolBar top-aligned onto the form and add a few flat buttons (using images ripped from Explorer) to allow the user to change the ViewStyle of the list view. To make the list view auto-arrange the icons, add the following code to the FormResize event: lvwForms.Arrange (arDefault); Finally we need to add an image list to the form and link it to the list view LargeImages property. We now need to display the menu options available within the factory within the list view. This involves cycling through the registered menu options and creating a new list item for each. Each list item will need to be assigned an image index for their icon, and a caption – these will be taken directly from the same named properties from the registered form. It is possible to add further information to the menu items for the instance when the list view is in vsReport mode; it can be a good idea to add a long description displayed as a second column so that novice users can have a better understanding what each action does. A good example of this can be seen in the Management Console of Windows 2000. The key to this process is that the menu presentation form itself needs to use only the MenuActionFactory unit itself and none of the specific menu option forms. These specialisations are completely hidden inside the factory; as its name implies it will create a concrete class of the correct type, without the client ever needing to know the exact details. Listing 2 shows the code necessary to populate the list view. Note that as it cycles through each registered menu action, the factory creates the required class. The menu form then creates a new list item with pertinent details before destroying the object. Note that in order for this to work correctly the constructors on the registered classes must be virtual; this is already true for visual components such as forms (because Delphi uses a factory to create them as they are streamed in from .DFM files and resources), but for custom classes it will be necessary to introduce a virtual constructor into their class hierarchy. When this form is presented to the user and an item is selected by double clicking, then we can again ask our factory to create the specific form required, which can then be displayed. There are absolutely no restrictions about what these forms can do, but the elegance of the factory pattern is that the client of the factory can remain ignorant of the detail. Further menu options can be added in to an evolving system simply by creating a new form and registering them with the factory – they will immediately be added into the menu system the next time the application is run. This lack of coupling between elements of the system greatly eases application maintenance for large projects – simply by registering classes with a factory the developer knows that any part of the application that has an interest in such things will be updated. It is easy to see how this menu solution is, in itself, generic and can be applied to a number of applications or even the same application a number of times. In many of my applications I have a main menu that displays all of the major modules within the suite, two such modules being reporting and general utilities. When these particular modules are selected they in turn display a further menu of the reports and utilities present within the system respectively. All of these menu forms share code through visual form inheritance, differing only in the factory that they use (there are different factories for modules, reports and utilities). The visual form ancestor shared by all three can be quite functional, dynamically adding TMenuItem entries to the application for each menu option. The keyword here is code reuse through loose coupling – a major benefit of object oriented principles. By comparison the traditional approach of closely coupling the unique menu forms to their specific menu actions looks inflexible and unwieldy. Last article’s problem Last article we discussed how a delegate object could be provided to persist business object information, and the question was asked, where might this provision take place? The obvious answer to the question is to create the data management object in the constructor of the business object: constructor TMyPDObject.Create; begin inherited; // Pass required information to constructor DMObject := TMyDMObject.Create; end; The issue with this approach is that every business object we create has the overhead of constructing a data management object. Depending upon your database, this may be an expensive operation, and thus extremely undesirable, especially when it is considered that many business objects will be constructed and destroyed when the application runs. By examining the interface to the class, it can be seen that the Load and Save methods are stateless - all work is achieved in the single method call. Therefore, provided the application does not require multi-threaded database activity in separate parts of the problem domain, a single data management object can be shared amongst the many instances of the business objects it can handle. Even better, this one data management object can be created at application startup time (or the first time it is required). These data management objects can be created as private global objects within the implementation section of the problem domain, or, more elegantly, a factory can be used to associate data management classes with business object classes and create them appropriately. ((( Listing 1 – An implementation of a factory for menu operations ))) unit MenuActionFactory; interface uses Classes, Forms; type TMenuAction = TForm; TMenuActionClass = class of TMenuAction; TMenuActionFactory = class private MenuActionList: TList; function GetCount: Integer; function GetMenuAction (Index: Integer): TMenuAction; public constructor Create; destructor Destroy; override; procedure Register (MenuActionClass: TMenuActionClass); property Count: Integer read GetCount; property MenuAction[Index: Integer]: TMenuAction read GetMenuAction; default; end; var MenuActions: TMenuActionFactory; implementation constructor TMenuActionFactory.Create; begin inherited; MenuActionList := TList.Create; end; destructor TMenuActionFactory.Destroy; begin MenuActionList.Free; inherited; end; function TMenuActionFactory.GetCount: Integer; begin Result := MenuActionList.Count; end; function TMenuActionFactory.GetMenuAction (Index: Integer): TMenuAction; begin Assert ((Index >= 0) and (Index < Count), 'Index out of range.'); // Construct and return the selected entry Result := TMenuActionClass (MenuActionList[Index]).Create (nil); end; procedure TMenuActionFactory.Register (MenuActionClass: TMenuActionClass); begin MenuActionList.Add (MenuActionClass); end; initialization MenuActions := TMenuActionFactory.Create; finalization MenuActions.Free; end. ((( End Listing 1 ))) ((( Listing 2 – Using a factory to populate a list view))) procedure PopulateListView (lvwMenu: TlistView; Factory: TMenuActionFactory); var Index: Integer; MenuAction: TForm; begin for Index := 0 to Factory.Count – 1 do begin MenuAction := Factory.MenuAction[Index]; with lvwMenu.Items.Add do begin Caption := MenuAction.Caption; ImageIndex := lvwMenu.LargeImages.AddIcon (MenuAction.Icon); SubItems.Add (MenuAction.Hint); end; end; end; ((( End Listing 2 )))