I am using
MVC4, MvcSiteMapProvider v3.2.1 (able to upgrade to v4 is needed).
My problem is the application is huge. And I want to modularize the application and make the module pluggable.
Since the sitemap is already huge, I want to make the sitemap also pluggalbe.
Is there a way to structure the sitemap with a root sitemap loading nodes from multiple xml files when the application start?
Here is the example to explain:
The original sitemap:
<?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-3.0"
xsi:schemaLocation="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-3.0 MvcSiteMapSchema.xsd"
enableLocalization="true">
<mvcSiteMapNode title="Home" controller="Home" action="Index">
<mvcSiteMapNode title="Staff List" controller="Staff" action="List">
<mvcSiteMapNode title="Create Staff" controller="Staff" action="Create"/>
<mvcSiteMapNode title="Edit Staff" controller="Staff" action="Edit"/>
<mvcSiteMapNode title="View Staff" controller="Staff" action="Details">
<mvcSiteMapNode >
...
</mvcSiteMapNode>
</mvcSiteMapNode>
<mvcSiteMapNode title="Client List" controller="Client" action="List">
<mvcSiteMapNode title="Create Client" controller="Client" action="Create"/>
<mvcSiteMapNode title="Edit Client" controller="Client" action="Edit"/>
<mvcSiteMapNode title="View Client" controller="Client" action="Details">
<mvcSiteMapNode >
...
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMap>
And I want to split the sitemap to be:
Root sitemap:
<mvcSiteMapNode title="Home" controller="Home" action="Index">
<subsitemap file="StaffSiteMap">// something like this.
<subsitemap file="ClientSiteMap">// something like this.
</mvcSiteMapNode>
</mvcSiteMap>
StaffSiteMap:
<mvcSiteMapNode title="Staff List" controller="Staff" action="List">
<mvcSiteMapNode title="Create Staff" controller="Staff" action="Create"/>
<mvcSiteMapNode title="Edit Staff" controller="Staff" action="Edit"/>
<mvcSiteMapNode title="View Staff" controller="Staff" action="Details">
<mvcSiteMapNode />
...
</mvcSiteMapNode>
</mvcSiteMapNode>
ClientSiteMap:
<mvcSiteMapNode title="Client List" controller="Client" action="List">
<mvcSiteMapNode title="Create Client" controller="Client" action="Create"/>
<mvcSiteMapNode title="Edit Client" controller="Client" action="Edit"/>
<mvcSiteMapNode title="View Client" controller="Client" action="Details">
<mvcSiteMapNode >
...
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMapNode>
Your approach is likely causing you to create more nodes than you need. Unless it is important to index your CRUD operations in search engines, there is a shortcut. You can use a single set of nodes for "Index", "Create", "Edit", "Delete", "Details" and use preservedRouteParameters
to force them to match every possible "id".
First of all, you need to nest the nodes properly so they will show the correct breadcrumb trail in each case.
<mvcSiteMapNode title="Staff" controller="Staff" action="List">
<mvcSiteMapNode title="Create New" action="Create" />
<mvcSiteMapNode title="Details" action="Details" preservedRouteParameters="id">
<mvcSiteMapNode title="Edit" action="Edit" preservedRouteParameters="id"/>
<mvcSiteMapNode title="Delete" action="Delete" preservedRouteParameters="id"/>
</mvcSiteMapNode>
</mvcSiteMapNode>
None of the "Edit", "Delete", and "Details" nodes will be in the menu or other controls, so you will need to use FilteredSiteMapNodeVisibilityProvider
to make them invisible in that case. You can set the default visibility provider to FilteredSiteMapNodeVisibilityProvider
in the configuration so you don't have to set it on every node. You should also set the VisibilityAffectsDescendants
property to false to ensure each node will always toggle on and off instead of being invisible when its parent node is invisible.
Internal DI (web.config):
<appSettings>
<add key="MvcSiteMapProvider_DefaultSiteMapNodeVisibiltyProvider" value="MvcSiteMapProvider.FilteredSiteMapNodeVisibilityProvider, MvcSiteMapProvider"/>
<add key="MvcSiteMapProvider_VisibilityAffectsDescendants" value="false"/>
</appSettings>
External DI (in DI module, StructureMap sample shown):
bool visibilityAffectsDescendants = false;
// Module code omitted here...
// Visibility Providers
this.For<ISiteMapNodeVisibilityProviderStrategy>().Use<SiteMapNodeVisibilityProviderStrategy>()
.Ctor<string>("defaultProviderName").Is("MvcSiteMapProvider.FilteredSiteMapNodeVisibilityProvider, MvcSiteMapProvider");
To finish up the visibility, you need to set the visibility
attribute of each node.
<mvcSiteMapNode title="Staff" controller="Staff" action="List">
<mvcSiteMapNode title="Create New" action="Create" visibility="SiteMapPathHelper,!*" />
<mvcSiteMapNode title="Details" action="Details" preservedRouteParameters="id" visibility="SiteMapPathHelper,!*">
<mvcSiteMapNode title="Edit" action="Edit" preservedRouteParameters="id" visibility="SiteMapPathHelper,!*"/>
<mvcSiteMapNode title="Delete" action="Delete" preservedRouteParameters="id" visibility="SiteMapPathHelper,!*"/>
</mvcSiteMapNode>
</mvcSiteMapNode>
Also, you will likely want to set the title of the "Index" node so it will show the title of the current record. You can do that with the SiteMapTitleAttribute
in each of your action methods.
[SiteMapTitle("Name")]
public ActionResult Details(int id)
{
using (var db = new EntityContext())
{
var model = (from staff in db.Staff
where staff.Id == id
select staff).FirstOrDefault();
return View(model);
}
}
This assumes there is a field named "Name" in your Staff table. You will also need to set this in the edit and delete methods (both get and post). But you also need to make sure you set the attribute target to ParentNode
so it will override the title of the "Details" node.
[SiteMapTitle("Name", Target = AttributeTarget.ParentNode)]
public ActionResult Edit(int id)
{
using (var db = new EntityContext())
{
var model = (from staff in db.Staff
where staff.Id == id
select staff).FirstOrDefault();
return View(model);
}
}
[HttpPost]
[SiteMapTitle("Name", Target = AttributeTarget.ParentNode)]
public ActionResult Edit(int id, Staff staff)
{
try
{
using (var db = new EntityContext())
{
var model = (from s in db.Staff
where s.Id == id
select s).FirstOrDefault();
if (model != null)
{
model.Name = staff.Name;
db.SaveChanges();
}
}
return RedirectToAction("Index");
}
catch
{
return View();
}
}
The result is that you will have fake breadcrumbs that change depending on which record is selected.
Home > Staff
Home > Staff > Create New
Home > Staff > John Doe
Home > Staff > John Doe > Edit
Home > Staff > John Doe > Delete
For a downloadable working demo, see the Forcing-A-Match
project in the code download of How to Make MvcSiteMapProvider Remember a User's Position.
Note that it is also possible to do this in MvcSiteMapProvider version 3.x, you just need to set the default visibility provider in the siteMap/providers/add
tag and ignore the part about VisibilityAffectsDescendants
.
<siteMap defaultProvider="MvcSiteMapProvider" enabled="true">
<providers>
<clear/>
<add name="MvcSiteMapProvider"
type="MvcSiteMapProvider.DefaultSiteMapProvider, MvcSiteMapProvider"
siteMapFile="~/Mvc.Sitemap"
securityTrimmingEnabled="true"
cacheDuration="5"
enableLocalization="true"
scanAssembliesForSiteMapNodes="true"
excludeAssembliesForScan=""
includeAssembliesForScan=""
attributesToIgnore="bling,visibility"
nodeKeyGenerator="MvcSiteMapProvider.DefaultNodeKeyGenerator, MvcSiteMapProvider"
controllerTypeResolver="MvcSiteMapProvider.DefaultControllerTypeResolver, MvcSiteMapProvider"
actionMethodParameterResolver="MvcSiteMapProvider.DefaultActionMethodParameterResolver, MvcSiteMapProvider"
aclModule="MvcSiteMapProvider.DefaultAclModule, MvcSiteMapProvider"
routeMethod=""
siteMapNodeUrlResolver="MvcSiteMapProvider.DefaultSiteMapNodeUrlResolver, MvcSiteMapProvider"
siteMapNodeVisibilityProvider="MvcSiteMapProvider.FilteredSiteMapNodeVisibilityProvider, MvcSiteMapProvider"
siteMapProviderEventHandler="MvcSiteMapProvider.DefaultSiteMapProviderEventHandler, MvcSiteMapProvider"/>
</providers>
</siteMap>
If you still think it is necessary to organize your SiteMap into smaller files, it is not possible in version 3.x. It is possible in version 4.x to use multiple XML files in your configuration if you use external DI and repeat the XmlSiteMapNodeProvider
multiple times for the same SiteMap. Here is how you would do that using StructureMap.
// Prepare for our node providers
var rootXmlSource = this.For<IXmlSource>().Use<FileXmlSource>()
.Ctor<string>("fileName").Is(HostingEnvironment.MapPath("~/Root.sitemap"));
var staffXmlSource = this.For<IXmlSource>().Use<FileXmlSource>()
.Ctor<string>("fileName").Is(HostingEnvironment.MapPath("~/Staff.sitemap"));
var clientXmlSource = this.For<IXmlSource>().Use<FileXmlSource>()
.Ctor<string>("fileName").Is(HostingEnvironment.MapPath("~/Client.sitemap"));
// Register the sitemap node providers
var siteMapNodeProvider = this.For<ISiteMapNodeProvider>().Use<CompositeSiteMapNodeProvider>()
.EnumerableOf<ISiteMapNodeProvider>().Contains(x =>
{
x.Type<XmlSiteMapNodeProvider>()
.Ctor<bool>("includeRootNode").Is(true)
.Ctor<bool>("useNestedDynamicNodeRecursion").Is(false)
.Ctor<IXmlSource>().Is(rootXmlSource);
x.Type<XmlSiteMapNodeProvider>()
.Ctor<bool>("includeRootNode").Is(false)
.Ctor<bool>("useNestedDynamicNodeRecursion").Is(false)
.Ctor<IXmlSource>().Is(staffXmlSource);
x.Type<XmlSiteMapNodeProvider>()
.Ctor<bool>("includeRootNode").Is(false)
.Ctor<bool>("useNestedDynamicNodeRecursion").Is(false)
.Ctor<IXmlSource>().Is(clientXmlSource);
x.Type<ReflectionSiteMapNodeProvider>()
.Ctor<IEnumerable<string>>("includeAssemblies").Is(includeAssembliesForScan)
.Ctor<IEnumerable<string>>("excludeAssemblies").Is(new string[0]);
});
Note that each XML file will still need a root node, but it will not be parsed into the SiteMap if the includeRootNode argument is false. Effectively this is the same as nesting the nodes of the Staff.sitemap
and Client.sitemap
files below the home page of Root.sitemap
.
Root.sitemap
<?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" controller="Home" action="Index" key="Home"/>
</mvcSiteMap>
Staff.sitemap
<?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" controller="Home" action="Index" key="Home">
<mvcSiteMapNode title="Staff List" controller="Staff" action="List">
<mvcSiteMapNode title="Create Staff" controller="Staff" action="Create"/>
<mvcSiteMapNode title="Edit Staff" controller="Staff" action="Edit"/>
<mvcSiteMapNode title="View Staff" controller="Staff" action="Details">
<mvcSiteMapNode >
...
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMap>
Note that you need to ensure the root node has the same key in every XML file - the best way to do that is to set the key explicitly to the same value in each file. Also note that with XML it is not possible to attach the nodes in different files any deeper than the home page node. Although you can nest nodes within each other within the extra files, they all must attach to the home page.
However, if you use IDynamicNodeProvider
, ISiteMapNodeProvider
, or [MvcSiteMapNode]
attribute, you can nest the nodes of each provider wherever you need to.