Introduction
In this article, you will learn some solutions to deal with the menus in ASP.NET Core. You also can find something common that can be used in other frameworks or other programming languages too.
Background
Menus are required in most of our systems, such as electronic mails and manager systems .etc. Most of the time, we may store the menus in our databases, so that we can manage them easily. And, this article is based on this scenario.
I will use the menu structure of an open source project named AdminLTE to demonstrate my solutions.
Let's take a look at the menu structure at first.
- <ul class="sidebar-menu">
- <li class="treeview">
- <a href="#">
- <i class="fa fa-share"></i> <span>Multilevel</span>
- <span class="pull-right-container">
- <i class="fa fa-angle-left pull-right"></i>
- </span>
- </a>
- <ul class="treeview-menu">
- <li>
- <a href="#"><i class="fa fa-circle-o"></i> Level One
- <span class="pull-right-container">
- <i class="fa fa-angle-left pull-right"></i>
- </span>
- </a>
- <ul class="treeview-menu">
- <li>
- <a href="#"><i class="fa fa-circle-o"></i> Level Two
- <span class="pull-right-container">
- <i class="fa fa-angle-left pull-right"></i>
- </span>
- </a>
- <ul class="treeview-menu">
- <li><a href="#"><i class="fa fa-circle-o"></i> Level Three</a></li>
- </ul>
- </li>
- </ul>
- </li>
- </ul>
- </li>
- </ul>
After looking the structure of the menu, we can design a table to store the infomation of the menus. Here is the sample table on my PostgreSQL.
- DROP TABLE IF EXISTS "public"."menu";CREATE TABLE "public"."menu" (
- "id" varchar(50) NOT NULL COLLATE "default",
- "parentid" varchar(50) COLLATE "default",
- "content" varchar(50) COLLATE "default",
- "iconclass" varchar(100) COLLATE "default",
- "href" varchar(200) COLLATE "default",
- "order" int8
- )WITH (OIDS=FALSE);
- ALTER TABLE "public"."menu" OWNER TO "postgres";
- ALTER TABLE "public"."menu" ADD PRIMARY KEY ("id") NOT DEFERRABLE INITIALLY IMMEDIATE;
After creating the table, we also need some sample data to demonstrate.
- BEGIN;
- INSERT INTO "public"."menu" VALUES ('E3A80BB6-B6BA-4F51-8EF2-123D6CD21E7F', null, 'first level 01', 'fa fa-book', '/', '1');
- INSERT INTO "public"."menu" VALUES ('7034B962-C54C-4B2C-92D4-439C0320F04B', 'E3A80BB6-B6BA-4F51-8EF2-123D6CD21E7F', 'second level 01', 'fa fa-circle-o text-aqua', '/', '1');
- INSERT INTO "public"."menu" VALUES ('F3400C61-D324-4078-A88D-5CD66E143216', 'E3A80BB6-B6BA-4F51-8EF2-123D6CD21E7F', 'second level 02', 'fa fa-circle-o text-aqua', '/', '2');
- INSERT INTO "public"."menu" VALUES ('B819B800-484F-4947-A36E-9A5E34F8CCA9', '7034B962-C54C-4B2C-92D4-439C0320F04B', 'third level 01', 'fa fa-circle-o', '/', '1');
- INSERT INTO "public"."menu" VALUES ('1FC65DDB-94F2-4ADF-8891-C8E853362D59', null, 'first level 02', 'fa fa-book', '/', '0');
- INSERT INTO "public"."menu" VALUES ('E8C67B83-D38C-4682-A39E-0CE5BDC16FA5', '1FC65DDB-94F2-4ADF-8891-C8E853362D59', 'new menu', 'fa fa-circle-o', '/', '100');
- COMMIT;
So far, we have finished the previous job of our database. Here is the result of my sample data.
Now, we need to define some Entities, map the columns of our table, and define some functions to operate.
Entity definition first
- public class Menu
- {
- public string ID { get; set; }
- public string ParentID { get; set; }
- public string Content { get; set; }
- public string IconClass { get; set; }
- public string Url { get; set; }
- public long Order { get; set; }
- }
- public class MenuViewModel
- {
- public string ID { get; set; }
- public string Content { get; set; }
- public string IconClass { get; set; }
- public string Url { get; set; }
- public IList<MenuViewModel> Children {get;set;}
- }
Functions to opreate the menus,
- private IList<Menu> GetAllMenuItems()
- {
- using (IDbConnection conn = new NpgsqlConnection(connStr))
- {
- try
- {
- conn.Open();
- var sql = @"SELECT * FROM public.menu;";
- return conn.Query<Menu>(sql).ToList();
- }
- catch (Exception ex)
- {
- return new List<Menu>();
- }
- }
- }
-
- private IList<Menu> GetChildrenMenu(IList<Menu> menuList,string parentId=null)
- {
- return menuList.Where(x => x.ParentID == parentId).OrderBy(x=>x.Order).ToList();
- }
- private Menu GetMenuItem(IList<Menu> menuList, string id)
- {
- return menuList.FirstOrDefault(x => x.ID == id);
- }
Solutions are coming!
Solution #1 Building HTML string in Controller
In this solution, we will build an HTML string in our Controller which will return a string to the View, and we need to render this string through `Html.Raw()` or other ways.
Controller first
- [Route("string")]
- public IActionResult RenderString()
- {
- return View("String",GetMenuString());
- }
- private string GetMenuString()
- {
- var menuItems = GetAllMenuItems();
-
- var builder = new StringBuilder();
- builder.Append("<ul class=\"sidebar-menu\">");
- builder.Append(GetMenuLiString(menuItems, null));
- builder.Append("</ul>");
- return builder.ToString();
- }
- private string GetMenuLiString(IList<Menu> menuList, string parentId)
- {
- var children = GetChildrenMenu(menuList, parentId);
-
- if(children.Count<=0)
- {
- return "";
- }
-
- var builder = new StringBuilder();
-
- foreach (var item in children)
- {
- var childStr = GetMenuLiString(menuList, item.ID);
- if(!string.IsNullOrWhiteSpace(childStr))
- {
- builder.Append("<li class=\"treeview\">");
- builder.Append("<a href=\"#\">");
- builder.AppendFormat("<i class=\"{0}\"></i> <span>{1}</span>",item.IconClass,item.Content);
- builder.Append("<span class=\"pull-right-container\">");
- builder.Append("<i class=\"fa fa-angle-left pull-right\"></i>");
- builder.Append("</span>");
- builder.Append("</a>");
- builder.Append("<ul class=\"treeview-menu\">");
- builder.Append(childStr);
- builder.Append("</ul>");
- builder.Append("</li>");
- }
- else
- {
- builder.Append("<li class=\"treeview\">");
- builder.AppendFormat("<a href=\"{0}\">",item.Href);
- builder.AppendFormat("<i class=\"{0}\"></i> <span>{1}</span>",item.IconClass,item.Content);
- builder.Append("</a>");
- builder.Append("</li>");
- }
- }
-
- return builder.ToString();
- }
As you can see, we just build a string based on the menu structure. You also can use ViewBag or ViewData to finish the same job.
In the View, we only need one line code to render the menus.
Here is the result of running this code (All of the solutions have the same result).
Before following the solutions, we need to add a function to get the tree menus, mainly for recursion.
- private IList<MenuViewModel> GetMenu(IList<Menu> menuList, string parentId)
- {
- var children = GetChildrenMenu(menuList, parentId);
-
- if (!children.Any())
- {
- return new List<MenuViewModel>();
- }
-
- var vmList = new List<MenuViewModel>();
- foreach (var item in children)
- {
- var menu = GetMenuItem(menuList, item.ID);
-
- var vm = new MenuViewModel();
-
- vm.ID = menu.ID;
- vm.Content = menu.Content;
- vm.IconClass = menu.IconClass;
- vm.Href = menu.Href;
- vm.Children = GetMenu(menuList, menu.ID);
-
- vmList.Add(vm);
- }
-
- return vmList;
- }
Solution #2 Tree Menu Entity + PartialView
In this solution, we will return a tree menu entity to the View, and render the View through this entity.
In the below code we are defining a partial View named _MenuPartial.cshtml , and its content.
- @model System.Collections.Generic.IEnumerable<Catcher.CoreDemo.Controllers.Learn01.MenuViewModel>
-
- @foreach (var menu in Model)
- {
- if (menu.Children.Any())
- {
- <li class="treeview">
- <a href="#">
- <i class="@menu.IconClass"></i> <span>@menu.Content</span>
- <span class="pull-right-container">
- <i class="fa fa-angle-left pull-right"></i>
- </span>
- </a>
- <ul class="treeview-menu">
- @Html.Partial("_MenuPartial",menu.Children)
- </ul>
- </li>
- }
- else
- {
- <li class="treeview">
- <a href="@menu.Href">
- <i class="@menu.IconClass"></i> <span>@menu.Content</span>
- </a>
- </li>
- }
- }
Note - We need to include current partial View in the loop so that we can render all the nodes of menus, because we can not know how many nodes the menus have .
As you can see, we mainly deal with the LI tag in Partial View, so we need to add the UL tag when we use the partial View.
- <ul class="sidebar-menu">
- @Html.Partial("_MenuPartial",Model)
- </ul>
Turning to our Controller, we only need to return menu data to the View.
- [Route("model")]
- public IActionResult Model()
- {
- var menuItems = GetAllMenuItems();
- return View("Model",GetMenuData(menuItems,null));
- }
Solution #3 ViewComponent
ViewComponent is not a new feature in ASP.NET Core , but we can use this feture to finish many jobs. This solution is similar to the previous solution. Both of them return a Vie but with a minor difference that can be seen easily.
Defining a new ViewComponent named MenuViewComponent
- public class MenuViewComponent : ViewComponent
- {
- public async Task<IViewComponentResult> InvokeAsync()
- {
- var menuItems = await GetAllMenuItems();
- return View(GetMenuData(menuItems, null));
- }
- }
Note - When we do not specify the name of the View, we should use the Default.cshtml as our View's name. And, we put this file in the path "your project/Views/Shared/Components/Menu". The content of this View is same as in _MenuPartial.cshtml.
And how to use this component? - Just call Component.InvokeAsync to use !
Here is an example.
- <ul class="sidebar-menu">
- @await Component.InvokeAsync("Menu")
- </ul>
The above three solutions may be suitable for most of ASP.NET projects, such as WebForm, MVC. and Core. These solutions are mostly based on AJAX and JSON data so that most of the web projects can use.
Let's define a action to return the JSON data of the menus.
- [Route("getdata")]
- public IActionResult GetData()
- {
- return Json(GetMenuData());
- }
The above code uses the JsonResult to compile. Json.NET can also finish this job as well.
When calling this action in our View, we will get the JSON data.
- [
- {
- "id": "E3A80BB6-B6BA-4F51-8EF2-123D6CD21E7F",
- "content": "first level 01",
- "iconClass": "fa fa-book",
- "href": "/",
- "children": [
- {
- "id": "7034B962-C54C-4B2C-92D4-439C0320F04B",
- "content": "second level 01",
- "iconClass": "fa fa-circle-o text-aqua",
- "href": "/",
- "children": [
- {
- "id": "B819B800-484F-4947-A36E-9A5E34F8CCA9",
- "content": "third level 01",
- "iconClass": "fa fa-circle-o",
- "href": "/",
- "children": []
- }
- ]
- }
- ]
- }
- ]
Solution #4 JSON Data + DOM manipulation
This solution is based on DOM manipulation. After getting the JSON data through AJAX, we will build a HTML string. At last, using append or appentTo to add DOM to element.
Defining a UL element in the View.
- <ul class="sidebar-menu" id="menu"></ul>
Using the JavaScript code to deal with DOM.
- $(function(){
- $.ajax({
- url:"/menu/getdata",
- dataType:"json",
- method:"get",
- success:function(data){
- $(getMenusByConcat(data)).appendTo("#menu");
- }
- });
-
- function getMenusByConcat(data){
- var dom = '';
-
- for(var i = 0;i<data.length;i++){
- dom+='<li class="treeview">';
-
- if(data[i].children.length>0)
- {
- dom+='<a href="#">';
- dom+='<i class="' + data[i].iconClass + '"></i> <span>'+ data[i].content +'</span>';
- dom+='<span class="pull-right-container">';
- dom+='<i class="fa fa-angle-left pull-right"></i>';
- dom+='</span>';
- dom+='</a>';
-
- dom+='<ul class="treeview-menu">';
- dom+=getMenusByConcat(data[i].children);
- dom+='</ul>';
- }
- else
- {
- dom+='<a href="'+ data[i].href +'"><i class="' + data[i].iconClass + '"></i> <span>'+ data[i].content +'</span></a>';
- }
-
- dom+='</li>';
- }
-
- return dom;
- }
- });
Solution #5 JSON Data + Template Engine
This solution uses a Template Engine (ArtTemplate) to reduce the operation of DOM. We just define the template and tell it how to render the data.
Take a look at the View's code.
- <ul class="sidebar-menu" id="menu"></ul>
- <script id="demo" type="text/html">
- {{each}}
- <li class="treeview">
- {{if $value.children.length>0}}
- <a href="#">
- <i class="{{$value.iconClass}}"></i> <span>{{$value.content}}</span>
- <span class="pull-right-container">
- <i class="fa fa-angle-left pull-right"></i>
- </span>
- </a>
- <ul class="treeview-menu">
- {{include 'demo' $value.children}}
- </ul>
-
- {{else}}
- <a href="{{$value.href}}">
- <i class="{{$value.iconClass}}"></i> <span>{{$value.content}}</span>
- </a>
- {{/if}}
- </li>
- {{/each}}
- </script>
There are two parts of the above code -
The first part is to define an empty shell of the menu, while the second part is the template which will render the data.
The most important code of the template is the following code dealing with recursion.
- {{include 'demo' $value.children}}
Note - When using ArtTemplate, we need to define a template using the script tag with a text/html type.
And the JavaScript code is shown below.
- $(function(){
- $.ajax({
- url:"/menu/getdata",
- dataType:"json",
- method:"get",
- success:function(data){
- getMenuByArtTemplate(data);
- }
- });
-
- function getMenuByArtTemplate(data){
- $("#menu").empty();
- var html = template('demo', data);
- $("#menu").html(html);
- }
- });
You can try the below Template Engines as well. Both of them help us to finish some complex DOM manipulation.
Solution #6 JSON Data + VueJS
This solution uses VueJS to render the View, which is similar to AngularJS and React.
For more information about VueJS, you can visit its official website : <http://vuejs.org/>
Because a vue component needs a root element, so we need to make a little change on the menu structure.
Here is the code of the View.
- <div id="menu">
- <item :model="treeData" class="sidebar-menu"></item>
- </div>
- <script type="text/x-template" id="item-template">
- <ul>
- <li class="treeview" v-for="item in model">
- <a href="#" v-if="item.children.length>0">
- <i :class="item.iconClass"></i> <span>{{item.content}}</span>
- <span class="pull-right-container">
- <i class="fa fa-angle-left pull-right"></i>
- </span>
- </a>
- <item :model="item.children" class="treeview-menu" v-if="item.children.length>0">
- </item>
-
- <a :href="item.href" v-if="item.children.length<=0">
- <i :class="item.iconClass"></i> <span>{{item.content}}</span>
- </a>
- </li>
- </ul>
- </script>
The tag item is a component defined in the JS code.
- Vue.component('item', {
- template: '#item-template',
- props: {
- model: Object
- }
- })
-
- var demo = new Vue({
- el: '#menu',
- data: {
- treeData: null
- },
- methods:{
- getMenuData:function(){
- var vm = this;
- axios.get('/menu/getdata')
- .then(function(res){
- vm.treeData = res.data;
- });
- }
- },
- mounted:function(){
- this.getMenuData();
- }
- })
Note - I used a JavaScript library named Axios to send an HTTP request!
Summary
This article introduced Six regular solutions to deal with the menus. As for me, I will suggest that you not to use Solution #1 and Solution #4.
Here are the reasons:
- Need a lot of time to build a HTML string.
- More prone to make mistakes, such as escape character.
- If you have plenty of menus, it may not perform very well.
Solution #2 and Solution #3 may be the easier solutions for a .NET devoloper. Solution #5 and Solution #6 need a little more of JavaScript technologies.