Introduction
Once when building the MVC website, I needed a nested menu. Well, it is not too hard to build the menu from the plain HTML (or Razor), however I needed not one, but many of them and manually building dozens of individual menus seemed to be a considerable waste of time. Needless to say, I hate doing something manually that can be done automatically.
The search I delved into yielded almost nothing – there was no decent and usable menu HTML helper. I was surprised. Sure, you can find some third party solutions, but I was looking for something generic.
The nested menu is the base component of almost any large website.
Eventually, I found a few menu HTML helpers, but they were unusable. Some of them were terribly complicated. Some were simply bugged and have only one level, but the bottom line was that none of them were able to produce the menu out of the Model. So, I made my own one.
The Objective
The menu should be simple, easy to use and most importantly, it must create the menu out of the data model (the class or structured container).
Solution
It is not a revelation that HTML is not the best way for delivering the structured data to the website. Simply, it is a historically accepted technology for the website building. There were however cautious attempts to replace the HTML with something better, like XHTML, but they were not adopted by the major players. Strictly speaking, replacing HTML with XHTML is not a solution. Each of these languages requires a parser and parsing of the HTML context is the most time consuming operation. Why am I talking about the HTML? The reason is that the solution comes down to the HTML itself.
The elements of the HTML code are displayed as the tags, plus the inner HTML. Inner HTML can also contain tags and so on. In order to build the HTML helper, we have to inject the HTML content into the parent tag inner HTML space, and because the Menu is technically a tree, it has to be done recursively.
The problem with the tree, however is that the content of node is not known at the moment of parsing because it has to contain the content of the nodes below, that have not been processed yet.
The solution I came up to includes the following steps.
Algorithm
- Recursively finding the first node without children
- Analyzing the node
- Injecting the content of the node into the parent inner HTML
- Moving up the tree (branch)
- Checking if the node has children, if not go to 3
- Cutting off the branch
- Recursively do these steps until the single node is left. This node will contain the required HTML.
Implementation
The HTML helper consists of two parts:
- The generic (base) HTML
BaseHtmlTagEngine
that implements the above algorithm. - The actual HTML renderer is derived from the generic engine by overriding
protected override void BuildTagContainer(MvcMenuItem itm, TagContainer parent)
abstract
method.
If the node has children, we use method:
TagBuilder Add_Ul_Tag()
which wraps the content into <ul>
tag.
The reason why the engine is split into two parts is that the base Engine can be reused in other HTML
helpers. The menu is just one of the possible ways of using the engine. In fact, any HTML structure can be built with this engine for any HTML code is the hierarchy of the nodes.
It can be used for building the Grid
, for instance, though it will not be that beneficial, the Grid
has only 2 levels in its hierarchy.
public abstract class BaseHtmlTagEngine<T> where T : IItem<T>
{
protected int _CntNumber = 0;
TagContainer _TopTagContainer;
string _OutString;
protected HtmlHelper _htmlHelper;
public BaseHtmlTagEngine(HtmlHelper htmlHelper)
{
_htmlHelper = htmlHelper;
}
public TagContainer TopTagCont
{
get { return _TopTagContainer; }
}
public void BuildTreeStruct(MenuViewModel<T> mod)
{
_CntNumber = 0;
try
{
_TopTagContainer = new TagContainer(ref _CntNumber, null);
foreach (T Mi in mod.MenuItems)
{
BuildTagContainer(Mi, _TopTagContainer);
}
}
catch (Exception ex)
{
}
}
public string Build()
{
try
{
while (true)
{
TagContainer tc = GetNoChildNode(_TopTagContainer);
bool PrcComplete = false;
PropagateInnerContentOneLevelUp(tc, ref PrcComplete);
if (PrcComplete)
break;
}
}
catch (Exception ex)
{
}
return _OutString;
}
void PropagateInnerContentOneLevelUp(TagContainer tc_int, ref bool ProcessingComplete)
{
TagContainer tc = tc_int;
while (tc != null)
{
if (tc.ParentContainer != null)
{
if (tc.ParentContainer.Tb != null)
{
tc.ParentContainer.Tb.InnerHtml += tc.Tb.ToString();
_OutString = tc.ParentContainer.Tb.ToString();
}
else
{
ProcessingComplete = true;
break;
}
if (tc.ParentContainer.ChildrenContainers.Count > 1)
{
tc.ParentContainer.ChildrenContainers.Remove(tc);
break;
}
tc = tc.ParentContainer;
}
else
{
ProcessingComplete = true;
break;
}
}
}
TagContainer GetNoChildNode(TagContainer tc)
{
List<TagContainer> ContsList = new List<TagContainer>();
CollectNoChildNodes(tc, ContsList);
return ContsList.First();
}
void CollectNoChildNodes(TagContainer tc, List<TagContainer> ContsList)
{
if (tc == null)
return;
if (tc.ChildrenContainers.Count == 0)
{
ContsList.Add(tc);
}
foreach (TagContainer tcc in tc.ChildrenContainers)
{
CollectNoChildNodes(tcc, ContsList);
}
}
protected static bool HasChildren(T itm)
{
if (itm == null)
return false;
return itm.GetChildren().Count > 0;
}
protected abstract void BuildTagContainer(T itm, TagContainer parent);
}
The actual Menu renderer
public class HtmlBuilder : BaseHtmlTagEngine<MvcMenuItem>
{
public HtmlBuilder (HtmlHelper htmlHelper): base(htmlHelper)
{
}
protected override void BuildTagContainer(MvcMenuItem itm, TagContainer parent)
{
TagContainer tc = FillTag(itm, parent);
foreach (MvcMenuItem mii in itm.GetChildren())
{
BuildTagContainer(mii, tc);
}
}
TagContainer FillTag(MvcMenuItem itm, TagContainer tc_parent)
{
TagContainer Li_Tc = new TagContainer(ref _CntNumber, tc_parent);
Li_Tc.Name = itm.Text;
Li_Tc.Tb = AddItem(itm);
if (HasChildren(itm))
{
TagContainer Ul_container = new TagContainer(ref _CntNumber, Li_Tc);
Ul_container.Name = "**";
Ul_container.Tb = Add_Ul_Tag();
return Ul_container;
}
return Li_Tc;
}
TagBuilder Add_Ul_Tag()
{
TagBuilder Ul_Tag = new TagBuilder("ul");
Ul_Tag.MergeAttribute("id", "menu1");
Ul_Tag.AddCssClass("dropdown-menu");
Ul_Tag.AddCssClass("MenuProps");
return Ul_Tag;
}
string GenerateUrlForMenuItem(MvcMenuItem menuItem, string contentUrl)
{
var url = contentUrl + menuItem.Controller;
if (!String.IsNullOrEmpty(menuItem.Action)) url += "/" + menuItem.Action;
return url;
}
string GenerateContentUrlFromHttpContext(HtmlHelper htmlHelper)
{
string RetStr = UrlHelper.GenerateContentUrl("~/", htmlHelper.ViewContext.HttpContext);
return RetStr;
}
TagBuilder AddItem(MvcMenuItem mi)
{
var Li_tag = new TagBuilder("li");
var a_tag = new TagBuilder("a");
var b_tag = new TagBuilder("b");
var image_tag = new TagBuilder("img");
if (mi.IconImage != null)
{
string pth = "/Images/" + mi.IconImage;
image_tag.MergeAttribute("src", pth);
image_tag.AddCssClass("CoolImgMenu");
}
b_tag.AddCssClass("caret");
var contentUrl = GenerateContentUrlFromHttpContext(_htmlHelper);
string A_refStr = GenerateUrlForMenuItem(mi, contentUrl);
a_tag.Attributes.Add("href", A_refStr);
if (mi.MnuTyp == MenuType.Top)
{
Li_tag.AddCssClass("dropdown");
a_tag.MergeAttribute("data-toggle", "dropdown");
a_tag.AddCssClass("dropdown-toggle");
}
else
{
Li_tag.AddCssClass("dropdown-submenu");
Li_tag.AddCssClass("CoolMenuLi");
}
a_tag.InnerHtml += image_tag.ToString();
a_tag.InnerHtml += mi.Text;
if (HasChildren(mi))
{
a_tag.InnerHtml += b_tag.ToString();
}
Li_tag.InnerHtml = a_tag.ToString();
return Li_tag;
}
}
The interface...
public interface IItem<T>
...is used by the BaseHtmlTagEngine
as the primary source of the data. Any class implementing this interface can be processed by BaseHtmlTagEngine
.
public enum MenuType
{
submenu,
Top
}
public class MenuViewModel<T>
{
public IList<T> MenuItems = new List<T>();
}
public class MvcMenuItem : IItem<MvcMenuItem>
{
public string Text { get; set; }
public string Action { get; set; }
public string Controller { get; set; }
public string IconImage { get; set; }
public MenuType MnuTyp { get; set; }
public List<MvcMenuItem> MenuChildren = new List<MvcMenuItem>();
public MvcMenuItem AddItem(string txt)
{
MvcMenuItem mi = new MvcMenuItem() { Text = txt };
MenuChildren.Add(mi);
return mi;
}
public MvcMenuItem AddItem(string txt, string controller, string action, string icon)
{
MvcMenuItem mi = new MvcMenuItem()
{ Text = txt, Action = action, Controller = controller, IconImage = icon };
MenuChildren.Add(mi);
return mi;
}
public override string ToString()
{
return Text;
}
public IList<MvcMenuItem> GetChildren()
{
return MenuChildren;
}
}
public interface IItem<T>
{
IList<T> GetChildren();
}
TagContainer
holds the structure and the TagBuilder
associated with it.
public class TagContainer
{
public int OrdinalNum;
public string Name;
public TagBuilder Tb;
public TagContainer ParentContainer;
public List<TagContainer> ChildrenContainers = new List<TagContainer>();
public TagContainer(ref int Num, TagContainer parent)
{
OrdinalNum = Num++;
ParentContainer = parent;
if (parent != null)
{
parent.ChildrenContainers.Add(this);
}
}
public override string ToString()
{
string str = OrdinalNum.ToString() + "," +
Name + "," + ChildrenContainers.Count.ToString() + "#";
if (Tb != null)
str += Tb.ToString();
return str;
}
}
Using the Code
How to use the CoolMenu
helper. Could not be easier…
Create a standard ASP.NET MVC project.
Create the collection of items for the Model.
Create the root item. This item will go to the MVC application main menu.
MvcMenuItem itemRoot = new MvcMenuItem() { MnuTyp = MenuType.
itemRoot.Text = "Cool Menu 2";
MvcMenuItem Contacts_Mi = itemRoot.AddItem
("Invoke Contact", "home", "Contact", "Contact.png");
Where home is the controller name, action is the action name, Contact.png is the icon file.
Contacts_Mi.AddItem("Some Another Item");
The dummy models are in Models folder of the demo project.
Put the icon images (if any) in /Images directory.
Copy the DropdownMenuExtra.css file to Content folder.
Copy all source C# files to some project directory to compile.
In the demo project, the files are in HtmlMenuHelper folder.
For the production, these files could be compiled to Library (DLL).
The most important things
In the _Layout.cshtml file, make following changes:
@using CoolMenu @* !!!! don't forget about namespace*@
@Styles.Render("~/Content/DropdownMenuExtra.css") @* must be included*@
Add the Root item of your menu to the existing menu items list:
@Html.Raw(Html.GetMenuHtml("Menu1")) @* We render menu here*@
Run your project and enjoy the menu.
History