Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Dynamic Menu Creation

0.00/5 (No votes)
24 Sep 2002 3  
Dynamically create menus whose structure is defined in an Access database.

Introduction - Marc Clifton

OK, I�m an anarchist. I don�t like designing user interfaces with a �form designer�. I don�t think it promotes re-use and I think (especially in MFC), it promotes really bad code practices�functionality that is in reality general purpose becomes married to a particular GUI class. (You �good� programmers out there would never do anything like that, now would you?)

I also think that good code should look good. And frankly, the way VS.NET generates the code for menus is plain ugly. There�s nothing elegant about brute force instantiation of MenuItem objects and collections.

You might disagree with my anarchistic ideas, thinking them anachronistic, or even worse, you might think that my code is even uglier. So be it. Here is a set of libraries that generate a form�s menu structure as defined in an Access database.

Form Creation

The first step is to create the form and associate a menu to it. Stripping out all the .NET overhead, we can create a form easily:
  1. Some things we need:
  2. using System;
    using System.Windows.Forms;
    
    using KA;
    using KA.DataContainer;
    using KA.DatabaseIO;
    using KA.DataMatrix;
    using KA.DBG;
    using KA.EventManager;
    using KA.MenuManager;
    
  3. The form class must be created:
    public class Form : System.Windows.Forms.Form
    {
    }
    
  4. We define a class that contains the static Main method:
    class AppMain
    {
        static void Main()
        {
            app=new App();
            app.Init();
        }
        static public App app;
    }
    
  5. We define the App class:
    public void Init()
    {
        KA.DBG.Trace.Initialize("menuDemoTrace.txt");    
        sb=new StatusBar();
        sb.Text="Ready";
        Form form=new Form();
        form.Controls.Add(sb);
        menu=new KA.MenuManager.Menu();
        menu.helpText=new EventHandler(SetMenuHelp);
        menu.SetFormMenu(form, ".\\menu.mdb", "MainMenu");
        KA.EventManager.EventCollection.Add("ExitApp", new EventHandler(ExitApp));
        KA.EventManager.EventCollection.Add("About", new EventHandler(About));
        KA.EventManager.EventCollection.Add("CheckMe", new EventHandler(CheckMe));
        KA.EventManager.EventCollection.Add("RadioMe", new EventHandler(RadioMe));
        Application.Run(form);
        KA.DBG.Trace.Terminate();
    }
    
  6. And we declare the variables that are needed:
    private StatusBar sb=null;
    private KA.MenuManager.Menu menu=null;
    
What does the code in step 4 do?
KA.DBG.Trace.Initialize("menuDemoTrace.txt");
This initializes my trace debugger. One of the nice things about abstracting the event management is that all events can be logged in a trace file. Similarly with SQL statements, since there is a single interface class for all database functions. This makes our application code look cleaner, because it isn't sprinkled with logging functions (as if anyone's code actually is!)

The next several lines instantiate the status bar and form, which is used to display menu item context help. The menu system calls back to an application defined event for the actual display, so the application can determine where and how the menu help text is displayed. This is done in the following code, after the menu is instantiated:
menu=new KA.MenuManager.Menu();
menu.helpText=new EventHandler(SetMenuHelp);
Next, the menu is associated with the form, by specifying the form, the database containing the menu structure, and the root, or main menu collection.
menu.SetFormMenu(form, ".\\menu.mdb", "MainMenu");

Menu Event Association

Next, the event handlers are associated with the menu events. The event names are declared in the database, and the application code must match these names in order for the menu manager to find the handler for the respective menu item.
KA.EventManager.EventCollection.Add("ExitApp", new EventHandler(ExitApp));
KA.EventManager.EventCollection.Add("About", new EventHandler(About));
KA.EventManager.EventCollection.Add("CheckMe", new EventHandler(CheckMe));
KA.EventManager.EventCollection.Add("RadioMe", new EventHandler(RadioMe));
The remainder of the startup process launches the form, and on termination, closes the debug tracer.

Event Handlers

These event handlers demonstrate that the system works and some simple menu item manipulation:
private void ExitApp(object obj, EventArgs e)
{
    Application.Exit();
}

private void About(object obj, EventArgs e)
{
    MessageBox.Show("Dynamic menu creation demo.\nV 0.10", "About"); 
}

private void CheckMe(object obj, EventArgs e)
{
    menu["ViewChecked"].Checked^=true;
}

private void RadioMe(object obj, EventArgs e)
{
    menu["ViewRadio"].Checked^=true;
}

The Menu Manager

The menu manager reads the structure out of the database and instantiates the menu objects in the .Net framework, finally associating them with the form. The primary method that does this is SetFormMenu:
public void SetFormMenu(System.Windows.Forms.Form form, string dbName, string menuName)
{
    dc=new DC();
    dbio=new DBIO(dc);
    dbio.SetFileName(dbName);
    dbio.Open();
    sql="SELECT b.CAPTION, b.HELP, b.ENABLED, b.IS_CHECKED, " +
        "b.IS_RADIO, b.SHORTCUT_KEY, b.IS_VISIBLE, b.CLICK_EVENT, " +
         "b.SELECT_EVENT, b.POPUP_MENU, b.NAME FROM MENU_ITEM_COLLECTION AS a," +
         "MENU_ITEM AS b WHERE b.NAME=a.ITEM_NAME and " +
         "a.MENU_NAME='{menuName}' ORDER BY a.SEQ";
    MainMenu mainMenu=new MainMenu();
    LoadMenu(mainMenu, menuName);
    dbio.Close();
    form.Menu=mainMenu;
    form.MenuComplete+=new EventHandler(Complete);
}
This function instantiates a data container and database IO object. The SQL statement is the same for the top level and sub-levels of the menu structure, so only one SQL statement is needed. Next, the function instantiates the MainMenu .NET object and invokes our LoadMenu method, followed by cleanup.

Main menu items are instantiated in the MainMenu object, whereas sub-menu items are instantiated in MenuItem objects. I didn't do this! This is the way .NET does it, therefore, our menu loader has to have two different load mechanisms, one for the main menu items and one for sub-menu items.

Also, notice that the MenuComplete event is defined. Thanks to James T. Johnson for finding this for me! This event must be defined so that after the user clicks on a menu item, or clicks somewhere outside of the menu in order to cancel the menu operation, the status bar can be restored to the application's default value. MFC handles this much easier and better. Oh well.

Loading The Main Menu Items

private void LoadMenu(MainMenu mainMenu, string menuName)
{
    dc.Set("menuName", menuName);
    DM menuList=dbio.QueryMultiRow(sql, "menuList");
    for (int i=0; i<menuList.GetRows(); i++)
    {
        KAMenuItem menuItem=new KAMenuItem(menuList.GetCell(0, i),
                                            "", "", "");
        mainMenu.MenuItems.Add(menuItem);
        menuItemList[menuList.GetCell(10, i)]=menuItem;
        string subMenu=menuList.GetCell(9, i);
        if (subMenu != "")
        {
            LoadMenu(menuItem, subMenu);
        }
    }
}
The above code declares the menu collection name menuName in the data container, which is extracted by the SQL pre-parser in the database I/O manager (not discussed in this article--a lot of it is still under construction). The database is queried, and the resulting rows are iterated, creating top level menu items. If a sub-menu is defined for the menu item, the LoadMenu method is recursed. But What! It's a different LoadMenu method, one for loading sub-menus and their sub-sub-menus, etc.

Loading Sub-Menu Items

private void LoadMenu(MenuItem menuItem, string menuName)
{
    dc.Set("menuName", menuName);
    DM menuList=dbio.QueryMultiRow(sql, "menuList");
    for (int i=0; i<menuList.GetRows(); i++)
    {
        KAMenuItem menuItem2=new KAMenuItem(menuList.GetCell(0, i), 
                 menuList.GetCell(7, i), menuList.GetCell(8, i), 
                 menuList.GetCell(1, i));
        menuItemList[menuList.GetCell(10, i)]=menuItem2;
        menuItem2.Click+=new EventHandler(ClickMenuItem);
        menuItem2.Select+=new EventHandler(SelectMenuItem);
        if (sortedShortcutMap.Contains(menuList.GetCell(5, i)))
        {
            menuItem2.Shortcut=
                (Shortcut)sortedShortcutMap[menuList.GetCell(5, i)];
        }

        if (menuList.GetCellInt(2, i) != 1)
        {
            menuItem2.Enabled=false;
        }

        if (menuList.GetCellInt(3, i) == 1)
        {
            menuItem2.Checked=true;
        }

        if (menuList.GetCellInt(4, i) == 1)
        {
            menuItem2.RadioCheck=true;
        }

        menuItem.MenuItems.Add(menuItem2);
        string subMenu=menuList.GetCell(9, i);
        if (subMenu != "")
        {
            LoadMenu(menuItem2, subMenu);
        }
    }
}
This method is very similar to the main menu loader. It additionally performs the task of setting various menu item state information. It also sets the Click and Select events to ones internal to the menu manager. Also, it defines the menu shortcut key based on the text supplied in the database definition for the menu item. This was really annoying, because I had to map the Shortcut enumeration to text mesages. This was a lot easier to do in MFC where you could define the virtual keys!

The Select Event

Whenever the mouse moves over a menu item, we want the opportunity to call an application defined select event and to display the item's help text on the status bar. This is accomplished with the following event handler:
private void SelectMenuItem(object obj, EventArgs e)
{
    KAMenuItem menuItem=(KAMenuItem)obj;
    EventCollection.Invoke(obj, menuItem.selectEvent, "");
    if (helpText != null)
    {
        helpText(menuItem, new MenuArgs(menuItem.help));
    }
}
This code invokes any application defined event handler, and if the help text event handler is instantiated, it invokes it as well. The helpText event handler is defined by the application (as described above).

The Click Event

This event invokes the handler for when the user actually clicks on a menu item.
private void ClickMenuItem(object obj, EventArgs e)
{
    KAMenuItem menuItem=(KAMenuItem)obj;
    EventCollection.Invoke(obj, menuItem.clickEvent, "");
}

The Complete Event

This event is invoked when the user clicks on a menu item or clicks outside of the menu, causing it to lose focus and close. Handling this event provides the application the opportunity to restore the status bar text to its default.
private void Complete(object obj, EventArgs e)
{
    if (helpText != null)
    {
        helpText(obj, new MenuArgs(""));
    }
}

What Is This EventCollection Thing?

The EventCollection is my abstraction layer between system events (like menu events) and application specific handlers. You Design Patterns people have various names for this concept. Further discussion of this class is out of the scope of this article, and beware that this class is still under construction!

So what's this KA namespace?

Well, KA stands for Knowledge Automation, which is my company name!

Conclusion

Well, that's all there is to it! The source code includes all the project stuff and a sample database. The database architecture is really simple and you should be able to figure it out on your own. Now all I need is for someone to write a menu designer that loads the information into the database (ha ha ha).

Update - Martin Robins

While the above code certainly gets the job done, it is dependant on the base library and requires database access routines for menu population. I decided in response to the comments posted below to try and make a good thing better.

The code provides a base form that loads menu items from an XML file (included), displaying forms specified in the XML file or displaying errors if the XML refers to something that cannot be found.

It is not pretty; in fact it is very to the point - but it gets the job done!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here