MVC MvcSiteMap Multiple
SiteMaps Dependency
Injection
▌Background
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.
▋Related articles
▌Environment
l
Visual Studio 2015 Ent.
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*:
“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
沒有留言:
張貼留言