2016年2月13日 星期六

[MVC] MvcSiteMap - Multiple sitemaps in one application

 MVC    MvcSiteMap   Multiple SiteMaps    Dependency Injection 

Background


We completed the function of dynamic nodes with MvcSiteMap in the previous article.


And I have a new mission to put two SiteMaps in one website. One for the normal logon users, and the other is for the adminitrators. Sounds crazy, let’s see how to get this job done.

Environment


l   Visual Studio 2015 Ent.
l   Microsoft Sql Server 2012
l   MvcSiteMapProvider.MVC5 4.6.22
l   MvcSiteMapProvider.MVC5.DI.Unity 4.6.22


Goals


In this sample, I will create 2 SiteMaps in one application, the first one is for the normal pages, and the other is for call center, which is created with a new MVC Area.


The above screenshot shows that there are tow different SiteMaps in one website.


Implement : Create a second SiteMap


I would like to make a sample which is continuing on from the sample of this article :



Database : Extend

In order to separate the second Menu nodes from the first ones, it’s recommend to create a new Menu table to keep the information of Menu nodes.

So my DAOs are like the below picture. “CcMenus” and “CcRoleMenus” are the new tables for the second SiteMap.

PS. I am using Entity framework Code-first here.



Table Name
Used for
SmUsers
User information.
SmRoles
Role information.
SmUserRoles
User and Role information.
SmMenus
Menu Nodes for Normal pages.
CcMenus
Menu Nodes for Call Center
SmRoleMenus
Relations between roles and menus of normal pages.
CcRoleMenus
Relations between roles and menus of call center.


l   CcMenus

[Table("CcMenus")]
public class CcMenu : BaseEntity
{
        /// <summary>
        /// Menu ID
        /// </summary>
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int CcMenuId { get; set; }

        /// <summary>
        /// Menu title (zh-TW)
        /// </summary>
        [Required]
        [StringLength(200)]
        public string Name { get; set; }

        /// <summary>
        /// Menu title (zh-CN)
        /// </summary>
        [Required]
        [StringLength(200)]
        public string NameCn { get; set; }

        /// <summary>
        /// Menu title (en-US)
        /// </summary>
        [Required]
        [StringLength(200)]
        public string NameUs { get; set; }


        /// <summary>
        /// Area
        /// </summary>
        [StringLength(100)]
        public string Area { get; set; }

        /// <summary>
        /// Controller name
        /// </summary>
        [StringLength(100)]
        public string Controller { get; set; }

        /// <summary>
        /// Action name
        /// </summary>
        [StringLength(100)]
        public string Action { get; set; }

        /// <summary>
        /// the redirect Url
        /// </summary>
        public string Url { get; set; }

        /// <summary>
        /// Description
        /// </summary>
        [StringLength(500)]
        public string Description { get; set; }

        /// <summary>
        /// The parent id
        /// </summary>
        public int? ParentId { get; set; }

        /// <summary>
        /// The route values
        /// </summary>
        public string RouteValues { get; set; }

        /// <summary>
        /// Order serial number
        /// </summary>
        public int? OrderSn { get; set; }

        /// <summary>
        /// Is enabled?
        /// </summary>
        public bool IsEnabled { get; set; }

}


l   CcRoleMenus

[Table("CcRoleMenus")]
public class CcRoleMenu : UowEntity
{
        /// <summary>
        /// SysRole ID
        /// </summary>
        [Key]
        [Column(Order = 1)]
        public int SmRoleId { get; set; }
        /// <summary>
        /// SysMenu ID
        /// </summary>
        [Key]
        [Column(Order = 2)]
        public int CcMenuId { get; set; }

        //Foreign Key
        [ForeignKey("SmRoleId")]
        public virtual SmRole SmRole { get; set; }

        //Foreign Key
        [ForeignKey("CcMenuId")]
        public virtual CcMenu CcMenu { get; set; }
}

And of course we have to implement the CRUD repositories for the DAOs in the next step.
I will skip this step.

Update MvcSiteMap : Create a new physical Sitemap

The default Sitemp file is in the root folder of the website project.
We must create a new one for the second sitemap.



l   Center.sitemap

Notice that we have to use a new MenuNodeProvider : CcMenuNodeProvider,  for this new sitemap. We will implement the provider later.

<?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.CcMenuNodeProvider, JB.Sample.MvcSiteMap.Website" />
  </mvcSiteMapNode>

</mvcSiteMap>


Update MvcSiteMap : Validate Sitemap

Open \App_Start\MvcSiteMapProviderConfig.cs
And add the following code in the Register method.

validator.ValidateXml(HostingEnvironment.MapPath("~/Center.sitemap"));



Update MvcSiteMap : Create a new MenuNodeProvider

We will create a new MenuNodeProvider to provide the logic for the menu nodes for the second SiteMap. It’s very similar with the original one we created, the only different part is using the CRUD repositories of DAOs, “CcMenus” and “CcRoleMenus”.

CcMenuNodeProvider

public class CcMenuNodeProvider : DynamicNodeProviderBase
{
        public CcMenuNodeProvider() : base()
        {}

        public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
        {
            var returnValue = new List<DynamicNode>();
            CultureEnum cultureEnum = this.getCurrentCulture();

            try
            {

                using (var menuService = new CcMenuService<CcMenu>(new SmDbContext()))
                using (var roleMenuService = new CcRoleMenuService<CcRoleMenu>(new SmDbContext()))
                {
                    var menus = menuService.GetAll().ToList();

                    foreach (var menu in menus)
                    {
                        //取出該Menu對應那些Roles
                        IList<String> roles = roleMenuService.Get(x => x.CcMenuId == menu.CcMenuId).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.CcMenuId.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)
            {
                LogUtility.Logger.Error(ex, ex.Message);
                return null;
            }

        }

        private string getLocalizeTitle(CcMenu menu, CultureEnum cultureEnum)
        {
            if (cultureEnum.Equals(CultureEnum.zhTW))
            {
                return menu.Name;
            }
            else if (cultureEnum.Equals(CultureEnum.zhCN))
            {
                return menu.NameCn;
            }
            else
                return menu.NameUs;
        }

        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;
        }
}



Create a new master page layout and Area

Now we can make a master page layout for the Call Center pages.


In this sample, I simply used the default layout style of MVC 5 project template and made the changes in the following picture.



Also, I would like to create an Area to apply the new master page layout.



Finish the second SiteMap

If we are not using dynamic nodes from database and DI with MvcSiteMap, then the last step will be setting up the Webconfig like this.

<system.web>
    <!--Multiple SiteMaps configuration-->
    <siteMap defaultProvider="MenuNodeProvider" enabled="true">
      <providers>
        <clear />
        <add name="MenuNodeProvider" type="MvcSiteMapProvider.DefaultSiteMapProvider, MvcSiteMapProvider" siteMapFile="~/Mvc.sitemap" securityTrimmingEnabled="true" cacheDuration="5" enableLocalization="true" scanAssembliesForSiteMapNodes="true" includeAssembliesForScan="" excludeAssembliesForScan="" attributesToIgnore="visibility" nodeKeyGenerator="MvcSiteMapProvider.DefaultNodeKeyGenerator, MvcSiteMapProvider" controllerTypeResolver="MvcSiteMapProvider.DefaultControllerTypeResolver, MvcSiteMapProvider" actionMethodParameterResolver="MvcSiteMapProvider.DefaultActionMethodParameterResolver, MvcSiteMapProvider" aclModule="MvcSiteMapProvider.DefaultAclModule, MvcSiteMapProvider" siteMapNodeUrlResolver="MvcSiteMapProvider.DefaultSiteMapNodeUrlResolver, MvcSiteMapProvider" siteMapNodeVisibilityProvider="MvcSiteMapProvider.DefaultSiteMapNodeVisibilityProvider, MvcSiteMapProvider" siteMapProviderEventHandler="MvcSiteMapProvider.DefaultSiteMapProviderEventHandler, MvcSiteMapProvider" />
        <add name="CcMenuNodeProvider" type="MvcSiteMapProvider.DefaultSiteMapProvider, MvcSiteMapProvider" siteMapFile="~/Center.sitemap" securityTrimmingEnabled="true" cacheDuration="5" enableLocalization="true" scanAssembliesForSiteMapNodes="true" includeAssembliesForScan="" excludeAssembliesForScan="" attributesToIgnore="visibility" nodeKeyGenerator="MvcSiteMapProvider.DefaultNodeKeyGenerator, MvcSiteMapProvider" controllerTypeResolver="MvcSiteMapProvider.DefaultControllerTypeResolver, MvcSiteMapProvider" actionMethodParameterResolver="MvcSiteMapProvider.DefaultActionMethodParameterResolver, MvcSiteMapProvider" aclModule="MvcSiteMapProvider.DefaultAclModule, MvcSiteMapProvider" siteMapNodeUrlResolver="MvcSiteMapProvider.DefaultSiteMapNodeUrlResolver, MvcSiteMapProvider" siteMapNodeVisibilityProvider="MvcSiteMapProvider.DefaultSiteMapNodeVisibilityProvider, MvcSiteMapProvider" siteMapProviderEventHandler="MvcSiteMapProvider.DefaultSiteMapProviderEventHandler, MvcSiteMapProvider" />
      </providers>
    </siteMap>
</system.web>





Implement : DI with the new SiteMap


This part will shows how to inject our new SiteMap into the master page.

Update MvcSiteMap : Inject! Inject! Inject!

Open \DI\Unity\ContainerExtensions\MvcSiteMapProviderContainerExtension.cs


What we are going to inject :
l   RuntimeFileCacheDependency
l   CacheDetails
l   FileXmlSource
l   SiteMapNodeProvider
l   SiteMapBuilder
l   SiteMapBuilderSet
l   SiteMapCacheKeyToBuilderSetMapper


ü   Define the SiteMap path

string absoluteCcFileName = HostingEnvironment.MapPath("~/Center.sitemap");


ü   RuntimeFileCacheDependency

this.Container.RegisterType<ICacheDependency, RuntimeFileCacheDependency>(
                "cacheDependencyCc", new InjectionConstructor(absoluteCcFileName));


ü   CacheDetails

this.Container.RegisterType<ICacheDetails, CacheDetails>(
"cacheDetailsCc",
                new InjectionConstructor(
absoluteCacheExpiration,
TimeSpan.MinValue,
new ResolvedParameter<ICacheDependency>("cacheDependencyCc")));


ü   FileXmlSource

this.Container.RegisterType<IXmlSource, FileXmlSource>(
"file2XmlSource", new InjectionConstructor(absoluteCcFileName));


ü   SiteMapNodeProvider

this.Container.RegisterInstance<ISiteMapNodeProvider>(
"xmlSiteMapNodeProvider2",             
this.Container.Resolve<XmlSiteMapNodeProviderFactory>().Create(
this.Container.Resolve<IXmlSource>("file2XmlSource")),
new ContainerControlledLifetimeManager());



ü   SiteMapBuilder

this.Container.RegisterInstance<ISiteMapNodeProvider>(
"reflectionSiteMapNodeProvider2",             
this.Container.Resolve<ReflectionSiteMapNodeProviderFactory>().Create(
includeAssembliesForScan),
new ContainerControlledLifetimeManager());

this.Container.RegisterType<ISiteMapNodeProvider, CompositeSiteMapNodeProvider>(
new InjectionConstructor(
new ResolvedArrayParameter<ISiteMapNodeProvider>(
                new ResolvedParameter<ISiteMapNodeProvider>("xmlSiteMapNodeProvider2"),
                new ResolvedParameter<ISiteMapNodeProvider>("reflectionSiteMapNodeProvider2"))));


ü   SiteMapBuilderSet

// Configure the builder sets
this.Container.RegisterType<ISiteMapBuilderSet, SiteMapBuilderSet>("normal",
                new InjectionConstructor(
                    "normal",
                    securityTrimmingEnabled,
                    enableLocalization,
                    visibilityAffectsDescendants,
                    useTitleIfDescriptionNotProvided,
                    new ResolvedParameter<ISiteMapBuilder>("normalSiteMapBuilder"),
                    new ResolvedParameter<ICacheDetails>("cacheDetails")));

this.Container.RegisterType<ISiteMapBuilderSet, SiteMapBuilderSet>("callCenter",
               new InjectionConstructor(
                   "callCenter",
                   securityTrimmingEnabled,
                   enableLocalization,
                   visibilityAffectsDescendants,
                   useTitleIfDescriptionNotProvided,
                   new ResolvedParameter<ISiteMapBuilder>("callCenterSiteMapBuilder"),
                   new ResolvedParameter<ICacheDetails>("cacheDetailsCc")));

this.Container.RegisterType<ISiteMapBuilderSetStrategy, SiteMapBuilderSetStrategy>(
                new InjectionConstructor(
new ResolvedArrayParameter<ISiteMapBuilderSet>(
                     new ResolvedParameter<ISiteMapBuilderSet>("normal"),
new ResolvedParameter<ISiteMapBuilderSet>("callCenter"))));


ü   SiteMapCacheKeyToBuilderSetMapper

// Configure our custom builder set mapper
this.Container.RegisterType<ISiteMapCacheKeyToBuilderSetMapper, CustomSiteMapCacheKeyToBuilderSetMapper>();


Yes, I know. It’s really a huge works here.


Create Custom builder set mapper

In the previous step, we injected a custom builder set mapper, CustomSiteMapCacheKeyToBuilderSetMapper.

We need to have this builder set mapper in order to inject the correct builder set* to the SiteMap.

PS. Inject the correct builder set: in other words, the correct Menu Node Provider.


l   CustomSiteMapCacheKeyToBuilderSetMapper

public class CustomSiteMapCacheKeyToBuilderSetMapper
    : ISiteMapCacheKeyToBuilderSetMapper
{
        public virtual string GetBuilderSetName(string cacheKey)
        {
            string builderSetName = string.Empty;
            switch (cacheKey)
            {
                case "CcMenuNodeProvider":
                    builderSetName = "callCenter";
                    break;
                case "MenuNodeProvider":
                default:
                    builderSetName = "normal";
                    break;
            }

            return builderSetName;
        }
}

So how to know which Cache key are we using?
Remember that we created the new Master page layout with the Html helper extensions*:

@Html.MvcSiteMap("CcMenuNodeProvider").Menu("BootstrapMenuHelperModel", false)
@Html.MvcSiteMap("CcMenuNodeProvider").SiteMapPath("BootstrapSiteMapPathHelperModel")

CcMenuNodeProvider” is the name of Cache key.

PS. You may want to take a look at the metadata of the extensions.






Test it!

Now we can put some data into the database tables for the Menu nodes of Call Center.


The Menu nodes of normal pages.



The Menu nodes of Call Center. Notice that I am using the same logon user.




Conclusion

The essential idea of multiple SiteMaps in one application is using the dependency injection to apply the SiteMap to the mapping Layout.

There are also some samples in maartenba’s GitHub. You can use Autofac or other DI frameworks, or use other data source such like XML.



Reference



沒有留言:

張貼留言