2015年12月2日 星期三

[MVC] MvcSiteMap with Dependency Injection

 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.


l   Visual Studio 2015 Ent.
l   Microsoft Sql Server 2012
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 Cookieuse 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





沒有留言:

張貼留言