Introduction
I have been thinking of writing an article for a long time now. Why? I've been feeling a bit guilty lately since I have used allot of excellent code from here.
My Menu Generator
So, with all the stuff I have been working on lately, I've decided to post something that I created a couple of years ago, a database generated menu for the intranet site at my company. The reason, might as well start small and post something that might be remotely useful for some of you reading this.
Why does my menu generator stand apart from others on the net? Not much, I'm sure it has been done before. But one reason I find it useful is the fact that it can be traversed both forwards and backwards while keeping a fixed 2 line format (as seen above).
One warning I'm going to make before I begin is that my code is probably sloppy and outdated, but who cares, lets pretend it's pseudo-code. :)
Menu Examples
Here are some examples of what the menu looks like when traversing it (attached picture doesn't really show this). Here is my default setting:
Clicking on "Home" brings you here:
Clicking on "Outside" results in the following:
And finally clicking on "Microgaming" would show this:
If you follow the steps I did, you can see how the menu is traversed and your previous selection is always shown so you can navigate backwards in case of error.
The Goods
How does it work? Basically the menu items are stored in a simple database table containing the required information. Here is how I created the tables:
USE [Utopia]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
CREATE TABLE [dbo].[MENU](
[LEVELID] [int] IDENTITY(1,1) NOT NULL,
[LEVELCODE] [int] NOT NULL,
[DESCRIPTION] [varchar](25) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[LEVELWITHIN] [int] NOT NULL,
[URL] [varchar](100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[HINT] [varchar](100) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[IMAGEURL] [varchar](100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[CREATIONDATE] [datetime] NOT NULL,
[USERTXN] [varchar](25) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
CONSTRAINT [PK_MENU] PRIMARY KEY CLUSTERED
(
[LEVELID] ASC
)WITH (PAD_INDEX = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
SET ANSI_PADDING OFF
for the main table and here is the following type table:
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
CREATE TABLE [dbo].[MENUTYPE](
[LEVELCODE] [int] IDENTITY(1,1) NOT NULL,
[DESCRIPTION] [varchar](25) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[CREATIONDATE] [datetime] NOT NULL,
[USERTXN] [varchar](25) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
CONSTRAINT [PK_MENULEVEL] PRIMARY KEY CLUSTERED
(
[LEVELCODE] ASC
)WITH (PAD_INDEX = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
SET ANSI_PADDING OFF
The most important items here are the level id and the level within which define the navigation order. This would be similar to child and parent nodes in a tree.
Data
Nothing would stop you from reading your items from an XML file or web service by the way. It's all up to you. The presentation of the menu is done via a custom control (ascx file) with a simple literal web control:
litMenu.Text = GenerateMenu(myMenu);
where the myMenu object is simply:
using System;
namespace SmartNav
{
public class MenuHelper
{
protected int _Level;
protected int _Parent;
protected Boolean _Row1;
protected Boolean _Row2;
public int Level
{
get { return _Level; }
set { _Level = value; }
}
public int Parent
{
get { return _Parent; }
set { _Parent = value; }
}
public Boolean Row1
{
get { return _Row1; }
set { _Row1 = value; }
}
public Boolean Row2
{
get { return _Row2; }
set { _Row2 = value; }
}
}
}
A small note I should mention is that before the GenerateMenu(MenuHelper inMenu)
procedure is called, I have some code (rather ugly but functional, refer to the source code) that uses cookies to save the menu navigation and loads a default location if no cookie is present.
Like I said, it doesn't really matter where the data is stored, here is how I extract the data from the tables:
set ANSI_NULLS OFF
set QUOTED_IDENTIFIER OFF
GO
ALTER PROCEDURE [dbo].[ReturnMenu]
@inLoc int
AS
SET NOCOUNT ON;
select
l.description as "Level",
l.hint as "Level Hint",
m.description as "Name",
m.url as "URL",
m.imageURL as "ImageURL",
m.hint as "Hint",
m.levelid as "ID",
l.levelid as "Top"
from
menu m,
menutype mt,
(select description, hint, levelid from menu where levelID = @inLoc) l
where
m.levelcode = mt.levelcode
and m.levelwithin = @inLoc
This returns all the rows in the menu and gets populated into several MenuRow
objects. These rows are all part of the menu object which can be found in the SmartNav zip file.
Main Code
So here is the meat of the code, where the menu is actually created:
private string GenerateMenu(MenuHelper inMenu)
{
string menuStr;
MenuRow headerRow = new MenuRow();
MenuRow navigationItem = new MenuRow();
SmartNav menu = new SmartNav();
SqlDataReader dr;
MenuRow menuRow;
ArrayList menuItems = new ArrayList();
menu.WriteStartMenu();
menu.WriteNewLine();
#region Fetch First Row from DB
...
#endregion Fetch First Row
menu.WriteHeaderItem(headerRow);
menu.WriteMenuItem(menuItems);
navigationItem.Top = (int)SqlHelper.ExecuteScalar(
System.Configuration.ConfigurationSettings.AppSettings["ConnectionStringUTOPIA"],
CommandType.StoredProcedure, "ReturnParent",
new SqlParameter("@inLoc", navigationItem.Top));
menu.WriteNavigationItem(navigationItem, 0, Request["s"]);
menu.WriteEndLine();
menuItems.Clear();
menu.WriteNewLine();
if (inMenu.Parent != 0)
{
#region Fetch Second Row from DB
...
#endregion Fetch Second Row
menu.WriteHeaderItem(headerRow);
menu.WriteMenuItem(menuItems);
navigationItem.ID = (int)SqlHelper.ExecuteScalar(
System.Configuration.ConfigurationSettings.AppSettings["ConnectionStringUTOPIA"],
CommandType.StoredProcedure, "ReturnTop");
menu.WriteNavigationItem(navigationItem, 1, Request["s"]);
}
else
{
menu.WriteHeaderItem();
menu.WriteMenuItem();
menu.WriteNavigationItem();
}
menu.WriteEndLine();
menu.WriteEndMenu();
menuStr = menu.WriteMenu();
menuItems.Clear();
menu.Close();
return menuStr;
}
And that's pretty much it. Code needs some heavy refactoring but in a nutshell, we can all pretend that it's pseudo-code and that it works.
Things To Consider
There are many modifications that could be made to this menu generator (outside of code re-work/refactoring) that I can think of off the top of my head:
- C# 3.0's use of LINQ for accessing the data from various different types of sources.
- using stylesheets or config files to alter the look and feel of the menu.
- using a config file to make the menu a n-tier menu with a simple variable stating how many tiers to display dynamically.
- using C# 2.0 generics to have more creative menu items instead of text.
- using AJAX and eliminating frames (this is how it is implemented now).
Conclusion
I created this 2-tier menu because I wanted a menu that could shrink/grow in a defined space on top of my web application which could be navigated forwards/backwards in a logical way.
All the source code is included either in the article or in the attached file. Any comments will be appreciated.
History
-
2/5/2007: First draft.
-
2/6/2007: Formatting and "Menu Examples" section.