Lựa chọn

 Danh mục

 Chi tiết tài nguyên
The Application Automation Layer - Using XML to generate Menus
   Đăng bởi: host | Ngày : 01:43 05/12/09 | Xem: 572 |        
 
The Application Automation Layer - Using XML to generate Menus
Table of contents
 
 
This installment of the AAL architecture takes a break from the design of the framework itself and deals with a less abstract issue, namely user interface automation. At this point, the AAL framework is sufficiently developed to explore the concepts of automation as applied to user interfaces. I had originally planned to discuss menus, toolbars, status bars, and controls. However, this turned out to be much too involved for a single article, so I'm limiting this article to menus and menu issues such as toolbars, status bars, and MDI support issues. The accompanying code includes controls, but I'll leave the issues involved there, for the next article.
Previous Installments
Bootstrap Loader And Component Manager (beware, requires considerable rework at this point)
While this section usually goes at the end, but I thought it would be nice to put it at the beginning to give proper appreciation to the people that I am indebted to for their article and code contributions. In some cases, I have borrowed heavily from their work.
Visual Studio .NET Menu Style, by Carlos H. Perez
Within the context of user interfaces, automation refers to several things:
1.      Reducing or eliminating repetitive tasks that the developer performs whenever creating an application;
2.      Managing events through an instrumented mechanism;
3.      Providing advanced features, such as status bar help and toolbar-menu item state integration;
4.      Enhancing the look and feel of the user interface to meet more with today's standards;
5.      Providing an infrastructure for the management of GUI data and data format translation;
6.      Provides a mechanism for automating testing;
7.      Make layout changes without exiting, editing, and recompiling the program.
As a consultant, I find myself developing one or two major applications for a client along with several “applets” to handle special or unique requirements. In a sense, the AAL is born out of the necessity to create professional looking applications, that can easily and quickly be developed and expanded as my client’s requirements change and grow. Even if this does not describe your situation, the AAL and GUI automation has a lot to offer even for a single product in terms of flexibility and maintainability.
I also find that for each end user, there is a minor amount of customization required at the GUI level. This is the nature of the businesses in which I primarily work, and while I can hear the moans and screams, the ability to have an externally driven GUI definition has greatly eased the issues involved with maintaining custom front ends of the software. The rules are simple--provide the same code base and database schema, but allow for the customization of the front end. This is not easily doable with the .NET style of hard coding GUI creation or MFC's resource file mechanism. Both require recompiling the source code when GUI changes are made.
I don't know about you, but I can put a GUI together faster, by editing a text file than using a form layout program. Regardless of your tool preference though, have you ever wished you could change a dialog box without exiting the program, making the change, recompiling the code, and setting up the conditions necessary to get to the dialog box? Well, that's what you can do with externally driven GUI definitions, if you have the right framework to support such a mechanism. (If you think about it, you'll realize that such a framework needs to be able to separate the entire issue of data and event management from the GUI components, which is exactly what the AAL does).
Neither MFC nor the .NET framework provides any pre-canned solutions to several simple and straightforward elements of any modern application. Useful status bars, standard MDI menu handlers, useful list controls, rebars, toolbars and toolbar/status bar management, menu-tool bar coupling, etc., must all be developed as additions to the .NET framework, and in many cases, the MFC framework.
This is both good and bad. It’s good because it leaves the developer free to create a unique look and feel, and it’s bad because this has to be done with every application and often does not keep up with Microsoft’s latest GUI “standards”. The developer must resort either to third party tools (and wait for new versions when GUI standards change, accompanied with the hassle of incorporating interface changes) or develop his/her own “library” (along with the hidden costs of maintenance, documentation, updating, and often tight coupling with application specific elements).
The AAL implementation offers some unique solutions to the specific problems of user interface design, whether using a third party library or developing your own. And along with those solutions, it incorporates the unattractive features of “rolling your own”. The issue then is whether the capabilities that the AAL offers in terms of productivity enhancement, outweigh the costs of using an architecture you downloaded from Code Project. I think they do. For one thing, they do not preclude the use of third party GUI libraries because these libraries often follow Microsoft's lead in implementation, form and function and do not provide a more general supporting framework. This makes such libraries suitable for plugging in to the AAL framework via some (hopefully) simple wrapper classes.
I outlined some points in general above. Let's look specifically at how the AAL provides cost benefit. First off, the AAL GUI architecture promotes:
·Component layout reuse;
·Menu layout reuse;
·Standard look and feel;
·Modern appearance;
·Modern features, such as layout persistence
Second, the solutions presented here:
·Decouples the user interface from application specific code;
·Enables the designer to modify the user interface without recompiling the application;
·Provides a framework for easily adding new components and customizing existing ones;
·Ties in with a robust component architecture as described in the previous articles;
·Promotes the robustness of the application by being flexible to requirement changes during the lifetime of the application;
·Incorporates instrumentation which facilitates testing;
·Allows for capabilities such as automated regression testing.
That said (and probably said with too many words), automation, in terms of re-use, is impossible without taking a first step — extracting GUI specification from the form designer and application code.
As indicated earlier, this article primarily discusses the issues involved in specifying menus externally, although it touches on status bar, toolbar, and MDI support issues.
Throughout this article you will see screen shots of XML schemas and data entry GUI's, which have been taken from my XSD Schema and XML Data Entry applications.
 
 
The above image illustrates a menu that has some of the aspects of a modern day, professional, menu. Menu items have optional bitmaps, and the icon bar is shaded.
The issues around menus are:
·Extracting the hierarchy from the XML file
·Displaying the menus
·Displaying/removing status bar help messages
·Dynamically adjusting the window list for MDI applications
·Responding to menu events
·Menu and toolbar coupling
The menu is generated from an XML file that defines the basic elements of a menu item. The schema and its corresponding data entry form is straightforward:
 
 
This structure provides for the basic requirements of a menu:
·Type - Item, Popup, Separator, or WindowList
·Caption - the menu title
·Shortcut - the shortcut key combination associated with the menu item
·Help - the help text to display on the status bar when the mouse hovers over the MenuItem
·Bitmap - an associated bitmap to display adjacent to the menu item
·OnSelect - the AAL event that is triggered when the menu is selected
·Data - any associated data that is to be passed on to the event handler
This structure is currently lacking an initial menu state. In general, I prefer to manage state information separately, including initial state, as opposed to including initial state information in the GUI definition.
There are two noteworthy issues here--one is that when the menu item is a popup, the OnSelect field contains the name of the child menu (the menu Name in the outer GroupBox). Second, the "WindowList" type is a special type that is a placeholder for MDI child window enumeration. A typical WindowList entry appears like this:
 
 
Building a menu from this structure requires an XPath query and an algorithm that recurses into the menu tree. For the actual menu drawing, I am using the code developed by Chris Beckett to draw the menus and icons. This code has undergone some minor modifications, primarily the stripping of the component capability, since this was no longer necessary, and some minor cosmetic changes, such as a shaded icon bar.
Extracting the menu hierarchy from the XML file and passing it to Chris Beckett's code is straightforward:
Collapse
privatevoid BuildMenu(string menuName, Menu menu, bool isMainMenu)
{
    DataTable dt=AAL.Lib.Xml.GetTable
        (doc, "//Menu[Name=\""+menuName+"\"]/MenuItem");
    foreach (DataRow row in dt.Rows)
    {
        // get menu item attributes
        string itemName=dt.Columns.Contains
            ("ItemName") ? (string)row["ItemName"] : "";
        string type=dt.Columns.Contains
            ("Type") ? (string)row["Type"] : "";
        string caption=dt.Columns.Contains
            ("Caption") ? (string)row["Caption"] : "";
        string onSelect=dt.Columns.Contains
            ("OnSelect") ? (string)row["OnSelect"] : "";
        string data=dt.Columns.Contains
            ("Data") ? (string)row["Data"] : "";
        string shortcut=dt.Columns.Contains
            ("Shortcut") ? (string)row["Shortcut"] : "";
        string helpText=dt.Columns.Contains
            ("Help") ? (string)row["Help"] : "";
        string bitmap=dt.Columns.Contains
            ("Bitmap") ? (string)row["Bitmap"] : "";
 
        Dbg.Assert(type != "", new DbgKey("NoMenuType"), menuName);
 
        if (type=="Item")
        {
            // if item, then create a MenuItem object
            Dbg.Assert(itemName != "",
                new DbgKey("NoMenuItemName"), menuName);
            Dbg.Assert(caption != "",
                new DbgKey("Caption"), caption);
            Image image=null;
            if (bitmap != "")
            {
                image=Image.FromFile(bitmap);
            }
            SmartMenuItem item=new SmartMenuItem(this, caption,
                onSelect, data, helpText, image, isMainMenu);
            if (shortcut != "")
            {
                item.Shortcut=shortcutMap.GetShortcut(shortcut);
            }
            menuItemList[itemName]=item;
            menu.MenuItems.Add(item);
        }
        elseif (type=="Popup")
        {
            // if popup, then create a MenuItem object
            //and recurse into its children
            Dbg.Assert(caption != "",
                new DbgKey("NoMenuCaption"), menuName);
            MenuItem item=new SmartMenuItem(this, caption,
                null, isMainMenu);
            menu.MenuItems.Add(item);
            BuildMenu(onSelect, item, false);
        }
        elseif (type=="Separator")
        {
            // if separator, use .NET's coding for a separator
            MenuItem item=new SmartMenuItem(this, "-", null, false);
            menu.MenuItems.Add(item);
        }
        elseif (type=="WindowList")
        {
            Dbg.Assert(itemName != "",
                new DbgKey("NoMenuItemName"), menuName);
            Dbg.Assert(caption != "",
                new DbgKey("Caption"), caption);
            SmartMenuItem item=new SmartMenuItem(this,
                caption, shortcut, onSelect, data, "", null, false);
 
            if (shortcut != "")
            {
                item.Shortcut=shortcutMap.GetShortcut(shortcut);
            }
            item.Visible=false;
            menuItemList[itemName]=item;
            menu.MenuItems.Add(item);
        }
        else
        {
            Dbg.Assert(false, new DbgKey("UnkMenuType"), menuName);
        }
    }
}
Notice that "Popup" menu item types recurs into the function, and that "WindowList" menu item items are initially hidden.
Following is a description of the issues involved in displaying menu item help, responding to events, integrating menus with and MDI framework, and coupling menus to toolbars.
 
 
 
 
 
In the implementation of the SmartMenuItem class, the Select event is used:
Select+=new EventHandler(OnSelectEvent);
to automate the handling of help text. The implementation:
publicvoid OnSelectEvent(object obj, System.EventArgs e)
{
    EventData ev=EventData.Marshal(null, null,
        new MED[] {new MED("HelpText", helpText)});
    menuManager.ICM.Invoke("FormMgr.SetMenuHelp", ev);
}
instructs the form manager to display the help text for the selected menu item in the designated status bar. While the select event displays the help text, there is no corresponding event to determine when the help text is no longer needed as a result of the menu being closed. This must actually be determined in the form(!) using the MenuComplete event:
MenuComplete+=new EventHandler(MenuItemComplete);
This currently has the simple implementation of clearing the status bar help text.
The form manager component is responsible for the actual implementation of displaying the menu help text and returning that display to a "ready" state, when the menu item action is completed. A complete discussion of those issues will be presented in the next article when I go into the details of using XML to specify form and control creation. For the time being, we'll look at the basic handlers themselves:
publicvoid SetMenuHelpText(string helpText)
{
    if (menuHelpPanel != null)
    {
        menuHelpPanel.Text=helpText;
    }
}
 
publicvoid MenuItemComplete(object obj, EventArgs e)
{
    if (menuHelpPanel != null)
    {
        menuHelpPanel.Text="";
    }
}
These are very straight forward. Given a status bar with a menu help panel (of type StatusBarPanel) defined somewhere on it, the above code sets the help text in the first method and clears it in the second method. This code is still under development, but a variety of things can be done with these two handlers to make the process more sophisticated. One thing that comes to mind is preserving the help panel text, on the first call to SetMenuHelpText and restoring it on the MenuItemComplete call. Also, keep in mind that the panel used for the menu help text has a special designation in the XML file that specifies the form's controls (including other automation functions, such as displaying the date and time, etc).
Each menu item of Item type has an associated event that is handled by the Component Manager. The following code processes the Click event associated with the menu item, passing the event name and data, as specified in the XML file for the menu item, to the component manager, which is responsible for invoking the appropriate delegate handler.
publicvoid OnClickEvent(object obj, System.EventArgs e)
{
    EventData ev=EventData.Marshal
        (new MED[] {new MED("MenuData", data)});
    menuMgr.ICM.Invoke(onSelect, ev);
}
If you've been keeping up with these articles, you may faintly remember that the Data Hub component interfaces through the workflow manager when events are triggered. So now we have an inconsistent behavior between how the Data Hub and the Menu Manager handles events. This needs to be resolved! But not right now. I'm going to have to think about the pro's and con's and the concepts behind each (including how I want to handle automatic worker thread creation, which I thought to originally make only a capability of the workflow manager. Any suggestions?).
For MDI applications, the window list is typically displayed in the "Windows" menu, along with a variety of menu items to control the display and selection of those windows. As with status bars, this involves an interaction between the form manager and menu manager components, and is complicated by the addition of a dialog, the user can invoke to choose, from a complete list of MDI child windows.
First, the menu manager component implementation. In this component, the MDI child list is maintained as a HashTable, where the index is the form identifier (which must therefore be unique for each MDI child, an issue in itself but not discussed here) and has the MDI child caption. In addition, the menu manager maintains an ArrayList of menu items designated with the "WindowList" type. When a change in the selected child form or in the number of child windows (I'm using "form" and "window" equally here) occurs, this method is ultimately invoked, which makes visible, the menu items designated as "WindowList" types, sets the captions of these items, and sets the check flag for the currently active child window:
Collapse
privatevoid UpdateWindowList()
{
    IDictionaryEnumerator iter=windowList.GetEnumerator();
    int i=0;
    while ( (iter.MoveNext()) && (i <
        windowMenuItemList.Count) )
    {
        string formName=iter.Key asstring;
        string caption=iter.Value asstring;
        WindowMenuItem wmi= (WindowMenuItem)windowMenuItemList[i];
        int j=i+1;
        wmi.mi.Text="&"+j.ToString()+" "+caption;
        wmi.mi.Visible=true;
        wmi.formName=formName;
        wmi.mi.Checked=false;
        if (activeMDIFormName==formName)
        {
            wmi.mi.Checked=true;
        }
        ++i;
    }
    while (i < windowMenuItemList.Count)
    {
        WindowMenuItem wmi=(WindowMenuItem)windowMenuItemList[i];
        wmi.mi.Visible=false;
        ++i;
    }
}
Any remaining unused "WindowList" items are hidden.
To demonstrate menu and toolbar coupling, I'm going to use the built in toolbar capability of .NET, which is pretty primitive, but sufficient for our purposes.
In the XML specification for menus, each menu item is given a unique name. In the GUI XML specification file for the Toolbar control, each toolbar button is (optionally) associated with the corresponding menu item name. When this association is made, the toolbar button passes the click event to the menu, and correspondingly, the menu state (enabled/disabled) affects the toolbar button. Further detail on the XML specification for form controls will be discussed in the next article.
The implementation is straightforward. When the menu state is set, it passes the state to the Form Manager, which finds the associated toolbar button and sets its state correspondingly:
Menu Manager:
privateobject SetState(EventData eventData)
{
    string menuItemName;
    bool state;
    eventData.UnMarshal("MenuItemName", out menuItemName);
    eventData.UnMarshal("State", out state);
    Dbg.Assert(menuItemList.Contains(menuItemName),
        new DbgKey("UnkMenuItem"), menuItemName);
    SmartMenuItem mi=(SmartMenuItem)menuItemList[menuItemName];
    mi.Enabled=state;
    icm.Invoke("FormMgr.SetToolbarButtonState", eventData);
    returnnull;
}
Form Manager:
private object SetToolbarButtonState(EventData eventData)
{
    string menuItemName;
    bool state;
    eventData.UnMarshal("MenuItemName", out menuItemName);
    eventData.UnMarshal("State", out state);
    if (toolbarButtonList.Contains(menuItemName))
    {
        // this is not an assertion. The app may not have a toolbar, and
        // may not associate all menu items to a toolbar.
        SmartToolBarButton btn=
            (SmartToolBarButton)toolbarButtonList[menuItemName];
        btn.Enabled=state;
    }
    return null;
}
To review, the framework we've just created helps us automate menus by:
·Externalizing menu definitions into XML, eliminating application specific coding
·Externalizing menu definitions to allow for easy menu re-use and customization
·Providing icon support
·Providing MDI support in conjunction with the form manager (required because of owner draw menus)
·Providing menu item help support in conjunction with the form manager
·Abstracting the event handler mechanism so that interface points registered by other components can be invoked without application specific code (more on this in future)
You will note that all "messaging" between the components is handled by the Component Manager. The Component Manager implements the "Mediator" design pattern: "Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently." This is a very powerful construct and resolves many, if not all, of the issues brought up by the Aspect Oriented Software Design people.
Other Sources
Conclusion
The next article will look more closely at form and control creation using an external XML specification.

About Marc Clifton


Marc is the creator of MyXaml, an open source, declarative, XML instantiation engine. He is an industry consultant working primarily with companies interested in utilizing declarative programming concepts to add flexibility to n-tier architectures on web, CE, and desktop platforms. His other major open source project is the Advanced Unit Testing framework.  He operates his own website, www.marcclifton.com, where you will find many of his articles.

Marc lives in Hudson, NY with his girlfriend Karen and his son Ian, who attends the Hawthorne Valley School. To contact Marc, email him at marc.clifton@gmail.com.

 
Bình luận

Thêm nhận xét