Building a Dynamic Menu Structure


In Visual Basic (VB) 6 there were few options when it came to building menu structures. Although you could add menus dynamically, the top-level menu item had to exist first. You could not make all of the menu items in a group invisible, so hiding unneeded menu items was difficult at best. The .NET Framework provides several options because it is fully object oriented. The MainMenu control is just another object you are adding and removing other objects from—and because of this you truly have full control over the menu.

There are always differing points of view when it comes to creating a dynamic menu structure. To make this work, it is assumed that the application can connect to the database. If the application cannot connect, then the application may load, but the menu will not. And this creates problems. In some cases, applications are built to work offline. If that is the case, then this design will not work and the menu structure should be hard-coded into the executable. The major advantage to a dynamic menu structure is functional security. In other words, you can control access to the application's functions by just not giving access to the specific menu items. With a static structure, you must traverse the MainMenu object and disable specific menu items.

Note

A MenuItem object cannot be referenced by name through the MenuItemsCollection because it implements the IList, ICollection, and IEnumerable interfaces only. This presents a great deal of problems when it comes to referencing individual menu items dynamically. There is not a tag property or even a name property (that you can reference anyway), so the only way to figure out what menu item you are on is by recursively traversing the entire menu structure and examining the text property! My suggestion would be to either extend the MainMenu class or purchase a control such as the Infragistics UltraWinToolbars, which allows you to reference menu items by key.

In the following sections, you will dynamically create a menu structure from information stored in the database. For this application (and most sales/orders/ inventory applications) to work, the user must be connected to the database. You will create one menu table and two routines, one of which is a recursive routine to load all submenus. You will also create a data-centric menu class and a user-centric menu class.

Note

You can expand these menu classes later to create full-fledged security classes that control access to specific parts of the application.

Setting Up the Database

To begin, let's create the menu table in the database. Open the SQL Query Analyzer, enter the SQL in Listing 6-1, and execute it. Table 6-1 defines the menu table's columns.

Table 6-1: Menu Table Column Definitions

Column

Definition

menu_id

Primary key, identity.

menu_under_id

References the primary key. This controls whether the menu item is a submenu under the referenced menu.

menu_order

Controls the order that a menu appears in the specified menu. For example, if there are three menu items under the File menu, the value here controls the order the items appear under the File menu.

menu_caption

Represents the text displayed on the menu item.

menu_shortcut

Represents the shortcut associated with the menu item.

enabled

Sets whether the menu is enabled or disabled by default. The default is yes (1).

checked

Sets whether the menu item has a check displayed next to it. The default is no (0).

Listing 6-1: The Menu Table

start example
 Use Northwind CREATE TABLE menus (    menu_id int identity(1,1) primary key,    menu_under_id int null foreign key references menus(menu_id),    menu_order int not null,    menu_caption varchar(30) not null,    menu_shortcut varchar(5),    enabled int default(1) check(enabled in (0,1)),    checked int default(0) check(checked in (0,1))) 
end example

Tip

Using a self-referencing table makes it possible to create a recursive routine to load the menu structure. This saves you from having to write an inordinate amount of code to load your menu structure.

Next, add the menu items in Table 6-2, which form the basis for your menu structure. (When you are entering these items, I have assumed that the ID numbers will be generated in numerical order starting at 1. I have included the menu ID and the caption of the menu item so that you know the menu item it is supposed to be under.)

Table 6-2: Menu Table Data

menu_under_id

menu_order

menu_caption

menu_shortcut

enabled

checked

null

1

&File

NULL

1

0

null

2

&Edit

NULL

1

0

null

3

&Maintenance

NULL

1

0

null

4

&Window

NULL

1

0

null

5

&Help

NULL

1

0

1 (&File)

1

E&xit

NULL

1

0

2 (&Edit)

1

Cu&t

CtrlX

1

0

2 (&Edit)

2

&Copy

CtrlC

1

0

2 (&Edit)

3

&Paste

CtrlV

1

0

2 (&Edit)

4

NULL

1

0

2 (&Edit)

5

Select Al&l

NULL

1

0

2 (&Edit)

6

&Find

CtrlF

1

0

2 (&Edit)

7

Find &Next

F3

1

0

3 (&Maintenance)

1

&Region

NULL

1

0

3 (&Maintenance)

2

&Territories

NULL

1

0

3 (&Maintenance)

3

&Employees

NULL

1

0

4 (&Window)

1

&Cascade

NULL

1

0

4 (&Window)

2

&Tile

NULL

1

0

18 (&Tile)

1

&Horizontal

NULL

1

0

18 (&Tile)

2

&Vertical

NULL

1

0

5 (&Help)

1

&Report Errors

NULL

1

0

5 (&Help)

2

&About

NULL

1

0

This gives you the following menu structure:

 &File  E&xit &Edit  Cu&t  Ctrl+X  &Copy  Ctrl+C  &Paste Ctrl+V  --------  Select Al&l  &Find   Ctrl+F  Find &Next   F3 &Maintenance  &Region  &Territories  &Employees &Window  &Cascade  &Tile   &Horizontal   &Vertical &Help  &Report Errors  &About 

Now you will create the stored procedure to retrieve the menu items. Execute the following SQL statement against the Northwind database:

 Use Northwind Go CREATE PROCEDURE get_menu_structure AS SELECT * FROM     menus ORDER BY menu_under_id, menu_order 

Creating the Menu Objects

Next you will add the data-centric menu class, which retrieves this menu structure. Before you do that however, you need to create a new interface for your menu class. Modify the Interfaces code module so that it contains the following new interface:

 Public Interface IMenu      Function GetMenuStructure As DataSet End Interface 

Add a new class to the NorthwindDC project and call it MenuDC. Add the code from Listing 6-2 to the MenuDC code module.

Listing 6-2: The MenuDC Class

start example
 Option Strict On Option Explicit On Imports NorthwindTraders.NorthwindShared.Interfaces Imports System.Configuration Imports System.Data.SqlClient Public Class MenuDC     Inherits MarshalByRefObject     Implements IMenu     Public Function GetMenuStructure() As DataSet _     Implements IMenu.GetMenuStructure         Dim strCN As String = ConfigurationSettings.AppSettings("Northwind_DSN")         Dim cn As New SqlConnection(strCN)         Dim cmd As New SqlCommand()         Dim da As New SqlDataAdapter(cmd)         Dim ds As New DataSet()         cn.Open()         With cmd             .Connection = cn             .CommandType = CommandType.StoredProcedure             .CommandText = "get_menu_structure"         End With         da.Fill(ds)         cmd = Nothing         cn.Close()         Return ds     End Function End Class 
end example

Once you have added this code, rebuild the NorthwindDC project (which also causes the NorthwindShared project to be rebuilt) and copy the resulting two assemblies (NorthwindDC.dll and NorthwindShared.dll) to the bin folder in the directory to which the Internet Information Server (IIS) virtual directory points.

Because you have added another object that you need to reference on the data-centric side, you need to add the following tag to the web.config file:

 <wellknown mode="Singleton"      type="NorthwindTraders.NorthwindDC.MenuDC, NorthwindDC"      objectUri="MenuDC.rem"/> 

Add this tag in the same section as the wellknown tag for the RegionDC object. Save the web.config file.

Next, add a class to the NorthwindUC project called UIMenu. This class passes the menu structure to the user interface. Add the code from Listing 6-3 to the UIMenu code module.

Listing 6-3: The UIMenu Class

start example
 Option Strict On Option Explicit On Imports NorthwindTraders.NorthwindShared.Interfaces Public Class UIMenu     Private Shared mUIMenu As UIMenu     Public Shared Function getInstance() As UIMenu         If mUIMenu Is Nothing Then             mUIMenu = New UIMenu ()         End If         Return mUIMenu     End Function     Protected Sub New()         'Do nothing     End Sub     Public Function GetMenuStructure() As DataSet         Dim objIMenu As IMenu         Dim ds As DataSet         objIMenu = CType(Activator.GetObject(GetType(IMenu), _         AppConstants.REMOTEOBJECTS & "MenuDC.rem"), IMenu)         ds = objIMenu.GetMenuStructure         objIMenu= Nothing         Return ds     End Function End Class 
end example

As with the RegionMgr class, this class uses the Singleton pattern. The reason for this is simple—you only want to have to load up the menu information once.

Note

As mentioned earlier, you can alter this class to help control functional access. Once you do that, you do not want to make multiple trips to the database to get this information, so making this object a Singleton is the right choice. You will not be expanding upon this class in this chapter, but on the assumption that you can, you should leave this as a Singleton.

Now you have the dataset to use in creating your user interface's menu, you will actually create the menu structure. All of the code you will add now should be added to the frmMain class. To create the function that will actually handle the menu item click events, add the following code:

 Private Sub MainMenu_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) End Sub 

For the moment, you are not going to add any code to this method, but it needs to be here. Notice also that this method handles no events right now.

Note

Note the choice of the method name. I chose this name because it is consistent with the names .NET gives to methods. It is also a self-documenting name. The name of the method no longer matters in VB .NET gives you great latitude—but it also gives you enough rope to hang yourself with.

Add the following import statement to the frmMain code module:

 Imports NorthwindTraders.NorthwindUC 

Add the following module-level variable declaration in the frmMain class:

 Private mdsMenuLoad As DataSet 

The mdsMenuLoad dataset stores the dataset returned to you by the UIMenu object while you are loading up the menu structure.

Note

The choice to use a module-level variable to hold the dataset while you load the data is a personal choice. Because of how you are loading the data, it saves you from having to continually pass the dataset to the method that is using it, but it decreases the modularity of your code.

Now you will add the two routines that actually load the menu items and examine them in detail. To begin, add the LoadMenus routine in Listing 6-4 to the frmMain class. You will examine the code afterward.

Listing 6-4: The LoadMenus Routine

start example
 Private Sub LoadMenus()      Try           Dim objMenu As UIMenu           objMenu = objMenu.getInstance           mdsMenuLoad = objMenu.GetMenuStructure           Dim dv As New DataView(mdsMenuLoad.Tables(0))           Dim drv As DataRowView           dv.RowFilter = "menu_under_id is null"           dv.RowStateFilter = DataViewRowState.CurrentRows           For Each drv In dv                Dim mnuHeader As New _                MenuItem(Convert.ToString(drv.Item("menu_caption")))                AddItems(mnuHeader, Convert.ToInt32(drv.Item("menu_id")))                MainMenu1.MenuItems.Add(mnuHeader)           Next      Catch exc As Exception           LogException(exc)      End Try End Sub 
end example

The first three lines of code declare the menu object, get a reference to the menu object, and retrieve the dataset with the menu structure in it from the database. The next line declares something you have not seen before, a DataView object:

 Dim dv As New DataView(mdsMenuLoad.Tables(0)) 

The DataView object is exactly what it sounds like—a view of your data. However, this view acts exactly as a view in SQL Server. You can filter your dataset using a SQL Where clause so that you only see a portion of your data. It is similar to the Filter method of the recordset object in ADO. The constructor of the DataView object accepts the table of data from which you are creating the view. The next line declares a DataRowView object, which is the object that traverses the rows of data once you have filtered them in the DataView object. The next two lines filter the data that is accessed by the DataView object:

 dv.RowFilter = "menu_under_id is null" dv.RowStateFilter = DataViewRowState.CurrentRows 

You are looking for rows where the menu_under_id is null, which means you are looking for top-level menu items. The next line tells the DataView you want this filter to be applied to all of the rows currently available to the DataView object.

Note

There are many different things you can do with the DataView object, and this chapter only touches on a few of its aspects. A good reference on the subject is Database Programming with Visual Basic .NET Second Edition by Carsten Thomsen (Apress, 2002). It would be worth your time and effort to get to know this object better, especially if you deal with databases frequently.

The next block of code actually does the work of creating your menu structure:

 For Each drv In dv      Dim mnuHeader As New _      MenuItem(Convert.ToString(drv.Item("menu_caption")))      AddItems(mnuHeader, Convert.ToInt32(drv.Item("menu_id")))      MainMenu1.MenuItems.Add(mnuHeader) Next 

This code loops through all of the rows in the DataView object. In this case, it is limited to looping through the top-level menu items. First, it creates a new MenuItem object, which takes the menu caption as an argument to its constructor. Second, you call the AddItems method (you will create this method next), which is responsible for adding all of the subitems to a top-level menu. It is the AddItems method, which is recursive. This method takes a menu and a menu ID as arguments. Finally, once the AddItems method is done running, you add the top-level menu item to the MainMenu1 control.

Now it is time to add the routine that does all the work—the AddItems method. Add the code from Listing 6-5 to the frmMain class, and then you will examine what is occurring.

Listing 6-5: The AddItems Method

start example
 Private Sub AddItems(ByVal mnuTop As MenuItem, ByVal intMenuID As Integer)      Dim dr As DataRowView      Dim dv As New DataView(mdsMenuLoad.Tables(0))      Try           dv.RowFilter = "menu_under_id = " & intMenuID           dv.RowStateFilter = DataViewRowState.CurrentRows           If dv.Count > 0 Then                For Each dr In dv                     Dim mnuItem As New _                     MenuItem(Convert.ToString(dr.Item("menu_caption")))                     If Not IsDBNull(dr.Item("menu_shortcut")) Then                          mnuItem.Shortcut = _                          CType([enum].Parse(GetType(Shortcut), _                          Convert.ToString(dr.Item("menu_shortcut"))), _                          Shortcut)                     End If                     If Convert.ToInt32(dr.Item("enabled")) = 0 Then                          mnuItem.Enabled = False                     Else                          mnuItem.Enabled = True                     End If                     If Convert.ToInt32(dr.Item("checked")) = 0 Then                          mnuItem.Checked = False                     Else                          mnuItem.Checked = True                     End If                     AddItems(mnuItem, Convert.ToInt32(dr.Item("menu_id")))                     AddHandler mnuItem.Click, AddressOf MainMenu_Click                     mnuTop.MenuItems.Add(mnuItem)                Next           End If      Catch exc As Exception           LogException(exc)      End Try End Sub 
end example

Let's examine this code to find out what this routine is doing, starting with the method signature. The AddItem method accepts a menu item and a menu ID. The menu that is passed in is a menu with submenu items. The menu ID is the ID of the menu that has been passed to it. For example, during the fourth iteration of the LoadMenus method, the &Window menu item is passed in with the menu ID of 4 because that is its key in the database:

 Private Sub AddItems(ByVal mnuTop As MenuItem, ByVal intMenuID As Integer)      Dim dr As DataRowView      Dim dv As New DataView(mdsMenuLoad.Tables(0)) 

Next you filter the DataView rows so that you only get the menus that come under the menu that you passed in. Continuing with the previous example, when you perform this filter by using the ID of 4, you come up with the &Cascade and &Tile menu items:

 dv.RowFilter = "menu_under_id = " & intMenuID dv.RowStateFilter = DataViewRowState.CurrentRows 

Then you check to see if the count of rows is greater than zero. If it is, then you know there are menu items under the menu item that you passed in, and you loop through them:

 If dv.Count > 0 Then      For Each dr In dv 

For each menu item you find, do the following:

  1. Create a new menu item.

  2. Check to see if the menu_shortcut column has data in it.

  3. If it does, then add a shortcut to the menu item.

To add a shortcut you take your string entry (which matches the enumerated entry for the shortcut keys) and turn it into an enumerated shortcut type. Notice that you are calling the parse method on the [enum] object. This is a static method, so you can call it on any [enum] object:

 Dim mnuItem As New MenuItem(Convert.ToString(dr.Item("menu_caption"))) If Not IsDBNull(dr.Item("menu_shortcut")) Then      mnuItem.Shortcut = _      CType([enum].Parse(GetType(Shortcut), _      Convert.ToString(dr.Item("menu_shortcut"))), Shortcut) End If 

Tip

One of the cool features of .NET is the ability to turn an enumerated value into its string equivalent and vice versa. You can find more about this in the MSDN documentation under the Enum.Parse method.

Next you check to see if the menu item should be enabled and if the menu item should be checked:

 If Convert.ToInt32(dr.Item("enabled")) = 0 Then      mnuItem.Enabled = False Else      mnuItem.Enabled = True End If If Convert.ToInt32(dr.Item("checked")) = 0 Then      mnuItem.Checked = False Else      mnuItem.Checked = True End If 

After that, you call the AddItems method again, but this time with the menu item that was just created and the ID of this menu item. Using the previous example, the second time through the &Windows menu item, this value would be &Tile and the menu ID would be 18. When this routine started again, it would note that there were two menu items under &Tile, and these would be added as submenus to &Tile:

 AddItems(mnuItem, Convert.ToInt32(dr.Item("menu_id"))) 

This next block of code delegates the responsibility of handling the menu item's click event to the MainMenu_Click method (which you created previously). Then you add the menu item to the top-level menu item:

 AddHandler mnuItem.Click, AddressOf MainMenu_Click mnuTop.MenuItems.Add(mnuItem) 

Note

Because the AddItem routine is called recursively before you get to these two lines, you are guaranteed that all of the menus that belong below your top-level menu will be added before these two lines of code are called.

The last step you need to perform to make this routine work is to call the LoadMenus method from the constructor of frmMain. Edit the constructor so that the LoadMenus call comes at the end of the constructor method as follows:

 LoadMenus() 

Finally, before you run this routine for the first time, you need to delete the two temporary menu items that we have created.

You should now be able to run the application, and when you do, you should have a fully loaded menu structure that duplicates what you have placed in the database. However, there is no functionality at this point, so you will add that now. Modify the MainMenu_Click method so it matches the method shown in Listing 6-6.

Listing 6-6: MainMenu_Click Method

start example
 Private Sub MainMenu_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs)      Dim mnu As MenuItem = CType(sender, MenuItem)      Select Case mnu.Text           Case "&Regions"                LoadRegion()           Case "&Report Errors"                LoadErrors           Case "E&xit"                Close()      End Select End Sub 
end example

Note that you will receive an error when you enter the LoadRegion and LoadError methods, because you have not added them yet. This code converts the sender object, which is a MenuItem object in this routine into a MenuItem object. Then you perform a select case on the text to find out what was clicked and you respond appropriately.

Note

It is usually my preference to place one-line or two-line commands directly in the Select Case statement. However, if other code will be accessing this functionality, you should move them to individual routines. First, it means you do not have to duplicate code, and second, it makes for a much cleaner Select Case block.

Next, let's create the LoadRegion method. The easiest way to do this is to change the name of a routine you have already created and add a few more lines of code. The current method that launches the Region list form looks like the code in Listing 6-7.

Listing 6-7: Current Code to Launch the Region List

start example
 Private Sub MenuItem1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs)      Try           Cursor = Cursors.WaitCursor           mfrmRegionList = New frmRegionList()           mfrmRegionList.MdiParent = Me           mfrmRegionList.Show()      Catch exc As Exception           LogException(exc)      Finally           Cursor = Cursors.Default      End Try End Sub 
end example

Alter the method signature to read as follows:

 Private Sub LoadRegion() 

Also, alter the MenuItem2_Click method so that the signature now reads as follows:

 Private Sub LoadErrors() 

After doing this, everything should run fine. The menu should load up, and selecting Maintenance Regions should load the frmRegionList form.

Note

You have not added the functionality for the Territories or Employees yet. You will add this functionality in Chapter 7, "Revisiting Objects and Rules," and Chapter 9, "Understanding Server-Side Business Rules."




Building Client/Server Applications with VB. NET(c) An Example-Driven Approach
Building Client/Server Applications Under VB .NET: An Example-Driven Approach
ISBN: 1590590708
EAN: 2147483647
Year: 2005
Pages: 148
Authors: Jeff Levinson

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net