MVC MvcSiteMap
▌Introduction
MvcSiteMap
is an extension for showing the Site Menus and Site Map path.
However,
the release of MvcSiteMap is so fast that I encountered many problems during
using the MvcSiteMap Provider and its DI extension.
So
I write this article to memorize the following functions of it.
l Dynamic nodes from
database
l User’ Role based
dependency injection to MvcSiteMap.
l Localization.
▋Related articles
l Visual Studio 2015
Ent.
l
MvcSiteMapProvider.MVC5 4.6.22
l
MvcSiteMapProvider.MVC5.DI.Unity
4.6.22
▌Implement : Dynamic
nodes from database
▋Create database
Needless
to say, we have to create the database to support a dynamic site maps.
The
following code-first POCOs are for reference.
*
In order to implement the Localization later, we prepare
the 3 properties (“Name”, “NameCn”,
“NameUs”) to keep the wording of zh-TW (Traditional Chinese), zh-CN (Simplified
Chinese) and en-US (Default - English).
[Table("SysMenus")]
public class SysMenu : BaseEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int SysMenuId { get; set; }
[Required]
[StringLength(200)]
public string Name { get; set; }
[Required]
[StringLength(200)]
public string NameCn { get; set; }
[Required]
[StringLength(200)]
public string NameUs { get; set; }
[StringLength(100)]
public string Area { get; set; }
[StringLength(100)]
public string Controller { get; set; }
[StringLength(100)]
public string Action { get; set; }
public string Url { get; set; }
[StringLength(500)]
public string Description { get; set; }
public int? ParentId { get; set; }
public string RouteValues { get; set; }
public int? OrderSn { get; set; }
public bool IsEnabled { get; set; }
}
|
[Table("SysRoles")]
public class SysRole : BaseEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int SysRoleId { get; set; }
[StringLength(500)]
public string Name { get; set; }
public bool IsEnabled { get; set; }
}
|
[Table("SmRoleMenus")]
public class SmRoleMenu : UowEntity
{
[Key]
[Column(Order = 1)]
public int SmRoleId { get; set; }
[Key]
[Column(Order = 2)]
public int SmMenuId { get; set; }
//Foreign Key
[ForeignKey("SmRoleId")]
public virtual SmRole SmRole { get; set; }
//Foreign Key
[ForeignKey("SmMenuId")]
public virtual SmMenu SmMenu { get; set; }
}
|
▋Install
MvcSiteMapProvider.MVC5
After
installing, some Display Templates are included into the project.
▋Specify the Display
Templates for Bootstrap
Now
in the DisplayTemplates folder, add the 2 new display-templates for bootstrap,
just copy the codes below.
l BootstrapMenuHelperModel.cshtml
@model MvcSiteMapProvider.Web.Html.Models.MenuHelperModel
@using
System.Web.Mvc.Html
@using
MvcSiteMapProvider.Web.Html.Models
@helper TopMenu(List<SiteMapNodeModel>
nodeList)
{
<ul class="nav navbar-nav">
@foreach (SiteMapNodeModel node in nodeList)
{
string url = node.IsClickable ? node.Url
: "#";
if (!node.Children.Any())
{
<li><a href="@url">@node.Title</a></li>
}
else
{
<li class="dropdown"><a class="dropdown-toggle" data-toggle="dropdown" href="@url">@node.Title <b class="caret"></b></a>@DropDownMenu(node.Children)</li>
}
if (node != nodeList.Last())
{
<li class="divider-vertical"></li>
}
}
</ul>
}
@helper DropDownMenu(SiteMapNodeModelList nodeList)
{
<ul class="dropdown-menu">
@foreach (SiteMapNodeModel node in nodeList)
{
if (node.Title == "Separator")
{
@:
<li class="divider"></li>
continue;
}
string url = node.IsClickable ? node.Url
: "#";
if (!node.Children.Any())
{
@:
<li><a href="@url">@node.Title</a></li>
}
else
{
@:
<li class="dropdown-submenu"><a href="@url">@node.Title</a>@DropDownMenu(node.Children)</li>
}
}
</ul>
}
@TopMenu(Model.Nodes)
|
l BootstrapSiteMapPathHelperModel.cshtml
@model MvcSiteMapProvider.Web.Html.Models.SiteMapPathHelperModel
@using
System.Web.Mvc.Html
@using System.Linq
@using
MvcSiteMapProvider.Web.Html.Models
@if
(Model.Nodes.Count != 1)
{
@:<ul class="breadcrumb">
foreach (var node in Model.Nodes)
{
if (node == Model.Nodes.First())
{
continue;
}
if (node != Model.Nodes.Last())
{
string url = node.IsClickable ? node.Url
: "#";
@:<li><a href="@url">@node.Title</a><span class="divider">></span></li>
}
else
{
@:<li class="active">@node.Title</li>
}
}
@:</ul>
}
|
Now
you can use them with MvcSiteMap in the Mater page (default : _Layout.cshtml)
<div class="navbar-collapse
collapse">
@Html.MvcSiteMap().Menu("BootstrapMenuHelperModel", false)
</div>
<div class="navbar-collapse
collapse">
@Html.MvcSiteMap().SiteMapPath("BootstrapSiteMapPathHelperModel")
</div>
<div class="container
body-content">
@RenderBody()
<hr />
</div>
|
▋Create a dynamic Menu
provider
Okay,
we had set the UI. It’s time to implement a Menu Provider, which can get the
menus’ information from database, and put them into the MvcSiteMap.
To
achieve that, We have to create a class and make it inherit MvcSiteMapProvider.DynamicNodeProviderBase
public class MenuNodeProvider : DynamicNodeProviderBase
{
public MenuNodeProvider() : base() {}
public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
{
var returnValue = new List<DynamicNode>();
try
{
using (var menuService = new SmMenuService<SmMenu>(new SmDbContext()))
{
// 取出所有Menu項
var menus = menuService.GetAll().ToList();
foreach (var menu in menus)
{
DynamicNode dynamicNode = new DynamicNode()
{
Title =
menu.Name,
ParentKey =
menu.ParentId.HasValue ? menu.ParentId.Value.ToString() : "",
Key =
menu.SmMenuId.ToString(),
Action =
menu.Action,
Controller =
menu.Controller,
Area = menu.Area,
Url = menu.Url
};
if (!string.IsNullOrWhiteSpace(menu.RouteValues))
{
var keyVals =
Serializer.FromJson<List<KeyValuePair<String, String>>>(menu.RouteValues);
dynamicNode.RouteValues = keyVals.ToDictionary(x => x.Key, x =>
(object)x.Value);
}
returnValue.Add(dynamicNode);
}
}
return returnValue;
}
catch (Exception ex)
{
return null;
}
}
}
|
▋Inject the Menu provider
into MvcSiteMap
Open
Mvc.sitemap, use the following codes to inject the menu provider.
You have to specify the menu provider’s full class name and from
which assembly.
<?xml version="1.0" encoding="utf-8" ?>
<mvcSiteMap xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0"
xsi:schemaLocation="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0
MvcSiteMapSchema.xsd">
<mvcSiteMapNode title="Home" clickable="false" visibility="SiteMapPathHelper">
<mvcSiteMapNode title="About" dynamicNodeProvider="JB.Sample.MvcSiteMap.Website.Utility.SiteMap.MenuNodeProvider, JB.Sample.MvcSiteMap.Website" />
</mvcSiteMapNode>
</mvcSiteMap>
|
▋Got it!
Congrats!
We got the dynamic nodes.
▌Implement : Role
based DI to MvcSiteMap
Since
we finish implementing the MvcSiteMap with dynamic nodes, we are thinking that
it will be more practical if the Site Map can show or hide the nodes by the Role of a logon user.
So
in this chapter, we are going to make an unique SiteMap cache for every connect
session and inject the Role information into the SiteMap.
▋Install
MvcSiteMapProvider.MVC5.DI.Unity
▋Cache Key Generator
Base
on Shad Storhaug‘s answer in the question of StackFlow, a SiteMap is always
generated with a key.
So
we will make and inject a new “Cache Key Generator” for every session for
MvcSiteMap.
l SessionBasedSiteMapCacheKeyGenerator
public class SessionBasedSiteMapCacheKeyGenerator : ISiteMapCacheKeyGenerator
{
// fields
protected readonly IMvcContextFactory
mvcContextFactory;
// constructor
public
SessionBasedSiteMapCacheKeyGenerator(IMvcContextFactory
mvcContextFactory)
{
if (mvcContextFactory == null)
throw new ArgumentNullException("mvcContextFactory");
this.mvcContextFactory =
mvcContextFactory;
}
// methods -
ISiteMapCacheKeyGenerator Members
public virtual string GenerateKey()
{
var context = mvcContextFactory.CreateHttpContext();
var builder = new StringBuilder();
builder.Append("sitemap://");
builder.Append(context.Request.Url.DnsSafeHost);
builder.Append("/?sessionId=");
builder.Append(context.Session.SessionID);
LogUtility.Logger.Debug($"key = {builder.ToString()}");
return builder.ToString();
}
}
|
A
cache key will be looked like this
sitemap://localhost/?sessionId=eqtiep2wy1fvvaqvoprvgws3
Open
DI\Unity\ContainerExtensions\MvcSiteMapProviderContainerExtension.cs
Write
the injection code inside it.
// CacheKey
Generator
this.Container.RegisterType<ISiteMapCacheKeyGenerator, SessionBasedSiteMapCacheKeyGenerator>();
|
Besides,
I recommend to check the injection settings below.
▋Role Management (Security
Trimming)
Now
we are going to make the MvcSiteMap recognize the role of a logon user. To do
this, first we must have a role provider (management) process in our website.
Once you have your security settings in place and have enabled
security trimming, MvcSiteMapProvider will automatically hide the nodes the
current user doesn't have access to.
|
*
However, before automatically hiding the nodes, you have
to do 3 things :
*
1. Have Role based authentication in our application.
*
2. Enable Security Trimming.
*
3. Update Menu Node Provider to recognize the relation
between Roles and Menus.
1.
Role based authentication
So you can either use
Windows or OWIN authentication.
In my sample, I am
using Windows authentication. You can take a look at
2.
Enable Security Trimming
Open DI\Unity\ContainerExtensions\MvcSiteMapProviderContainerExtension.cs ,
and enabling Security
Trimming.
bool
securityTrimmingEnabled = true;
|
3.
Update Menu Node Provider
The principle here is setting Roles to
every dynamic node.
l
MenuNodeProvider
foreach (var menu in menus)
{
//What
Roles are matched for this Menu
IList<String> roles =
roleMenuService.Get(x => x.SmMenuId == menu.SmMenuId).ToList().Select(x
=> x.SmRole.Name).ToList();
DynamicNode dynamicNode = new DynamicNode()
{
Title = menu.Name,
ParentKey = menu.ParentId.HasValue ?
menu.ParentId.Value.ToString() : "",
Key = menu.SmMenuId.ToString(),
Action = menu.Action,
Controller = menu.Controller,
Area = menu.Area,
Url = menu.Url,
Roles
= roles
};
……
|
▋Test
In
the first test, I login as a “Admin”, and Admin can access all the parent
nodes.
(Database
settings)
And
the below picture shows that I can access all the parent nodes.
In
the second test, I login as an “User”, and an User can only access “Movies” and
“Main Roles”, the “Products” is not allowed.
▌Implement : Localization
▋Enable Localization in the
application
There
are many ways to enable localization in application.
Here
is my method.
l Global.asax - Application_BeginRequest
HttpCookie cultureCookie =
Request.Cookies["_culture"];
CultureInfo ci = null;
try
{
if (cultureCookie != null) //Use Cookie
{
ci = new CultureInfo(cultureCookie.Value);
}
else //No Cookie,use HttpRequest’s
information
{
var userLanguages =
Request.UserLanguages;
if (userLanguages.Length > 0)
{
try
{
ci = new CultureInfo(userLanguages[0]);
}
catch (CultureNotFoundException)
{
ci = CultureInfo.InvariantCulture;
}
}
else
{
ci = CultureInfo.InvariantCulture;
}
}
}
catch (Exception ex) { }
finally
{
System.Threading.Thread.CurrentThread.CurrentUICulture
= ci;
System.Threading.Thread.CurrentThread.CurrentCulture
= ci;
}
|
Be
sure that the “enaleLocalization” flag is set to “true” (Default) in MvcSiteMapProviderContainerExtension.cs.
▋Update Menu Node Provider …
again
The base idea is
1.
Getting the localization
setting of user’s browser
2.
Set the mapping wording to
the nodes’ (menus’) title.
private CultureEnum
getCurrentCulture()
{
CultureEnum cultureEnum = CultureEnum.enUS; //default : en-US
CultureInfo ci = System.Threading.Thread.CurrentThread.CurrentCulture;
if (ci != null)
{
switch (ci.Name.ToLower())
{
case "zh-tw":
cultureEnum = CultureEnum.zhTW;
break;
case "zh-cn":
cultureEnum = CultureEnum.zhCN;
break;
default: //Default : en-US
cultureEnum = CultureEnum.enUS;
break;
}
}
return cultureEnum;
}
|
private string getLocalizeTitle(SmMenu menu, CultureEnum cultureEnum)
{
if (cultureEnum.Equals(CultureEnum.zhTW))
{
return menu.Name;
}
else if (cultureEnum.Equals(CultureEnum.zhCN))
{
return menu.NameCn;
}
else
return menu.NameUs;
}
|
The
final version of MenuNodeProvider!
public class MenuNodeProvider : DynamicNodeProviderBase
{
public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
{
var returnValue = new List<DynamicNode>();
CultureEnum cultureEnum = this.getCurrentCulture();
try
{
using (var menuService = new SmMenuService<SmMenu>(new SmDbContext()))
using (var roleMenuService = new SmRoleMenuService<SmRoleMenu>(new SmDbContext()))
{
var menus =
menuService.GetAll().ToList();
foreach (var menu in menus)
{
IList<String> roles = roleMenuService.Get(x
=> x.SmMenuId == menu.SmMenuId).ToList().Select(x =>
x.SmRole.Name).ToList();
DynamicNode dynamicNode = new DynamicNode()
{
Title = this.getLocalizeTitle(menu,
cultureEnum),
ParentKey =
menu.ParentId.HasValue ? menu.ParentId.Value.ToString() : "",
Key =
menu.SmMenuId.ToString(),
Action = menu.Action,
Controller =
menu.Controller,
Area = menu.Area,
Url = menu.Url,
Roles = roles
};
if (!string.IsNullOrWhiteSpace(menu.RouteValues))
{
var keyVals =
Serializer.FromJson<List<KeyValuePair<String, String>>>(menu.RouteValues);
dynamicNode.RouteValues
= keyVals.ToDictionary(x => x.Key, x => (object)x.Value);
}
returnValue.Add(dynamicNode);
}
}
return returnValue;
}
catch (Exception ex)
{
return null;
}
}
}
|
▋Final Result
Now
our Site Map supports Traditional Chinese, Simplified Chinese and English now.
▌Reference
沒有留言:
張貼留言