I have a dynamic node provider which is copied below along with my site map configuration. When I go to my url, /Account/Edit/1475, the breadcrumb shows "Home > Accounts > [Incorrect Account Name] > Edit". It is showing an account name that is different then the 'accountId' in the url, 1475. I assume it is due to the 'preservedRouteParameter=accountId' that is causing it to match the wrong node. Is this right?
Do I need to create another DynamicNodeProvider for the Account Edit node in my sitemap? I started going down this path, but I needed to create a separate Dynamic Node Providers for most of the nodes so I thought I must be doing something wrong. Is there something I am missing in the configuration?
public class AccountDynamicNodeProvider : DynamicNodeProviderBase
{
public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
{
using (var entities = new Entities())
{
foreach (var account in entities.TAccounts)
{
var dynamicNode = new DynamicNode("Account_" + account.AccountId, account.AccountName);
dynamicNode.RouteValues.Add("accountId", account.AccountId);
yield return dynamicNode;
}
}
}
}
Mvc.sitemap:
<mvcSiteMapNode title="Home" controller="Home" action="Index">
<mvcSiteMapNode title="Accounts" controller="Account" action="Index">
<mvcSiteMapNode title="Detail" controller="Account" action="Details" dynamicNodeProvider="my.test.namespace.AccountDynamicNodeProvider, my.assembly">
<mvcSiteMapNode title="Edit" controller="Account" action="Edit" preservedRouteParameters="accountId" />
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMap>
This is the route that is being used:
routes.MapRoute(
name: "Account",
url: "Account/{action}/{accountId}",
defaults: new { controller = "Account", action = "Details" }
);
Use the SiteMapTitleAttribute to dynamically change the title when using preservedRouteParameters.
[SiteMapTitle("MyTitle", Target = AttributeTarget.ParentNode)]
public ViewResult Edit(int accountId) {
var account = _repository.Find(accountId);
// MyTitle is a string property of
// the account model object.
return View(account);
}
Or
[SiteMapTitle("MyTitle", Target = AttributeTarget.ParentNode)]
public ViewResult Edit(int accountId) {
ViewData["MyTitle"] = "This will be the title";
var account = _repository.Find(accountId);
return View(account);
}
In general, when configuring CRUD operations it is best to use preservedRouteParameters all the way. But going that route comes with the caveat that you need to fix the title and node visibility manually.
CRUD operations (other than perhaps Add New) generally don't appear in the Menu or SiteMap, instead a list or table is usually made on the page to dynamically generate commands for each record. So the only thing you typically have to worry about is the SiteMapPath, and for that you can use preservedRouteParameters.
Have a look at the Forcing a Match demo in How to Make MvcSiteMapProvider Remember a User's Position.
Related
I have a <s:select list="myList" /> being populated from an List<String> that is found in the Action class alongside its associated getters & setters...
As per this simple example
https://www.mkyong.com/struts2/struts-2-sselect-drop-down-box-example/
However I want to know if it's possible not to create a List within the action class but to store a List object on the request
List<String> myList = new ArrayList<>();
myList.add("value1"); myList.add("value2"); etc...
request.setAttribute("myList", myList);
and use that to populate <s:select list="myList" />
I have attempted this but I can't get it to work. I want to know if it was something I was doing wrong or it just can't be done ?
My belief was that all data within the form was added to the request anyhow on load of the JSP so shouldn't matter if it was manually added as per above...
Yes is possible like below
List<String> myList = new ArrayList<>();
myList.add("value1"); myList.add("value2"); //etc...
ActionContext actionContext = ActionContext.getContext();
if(null != actionContext) {
ValueStack stack = actionContext.getValueStack();
stack.setValue("#request['myList']", myList);
}
then
<s:select list="#request['myList']" />
Hi have a sitemap on my mvc 4 application like this:
<?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="Users" controller="User" action="Index" area="" preservedRouteParameters="culture,projectid">
<mvcSiteMapNode title="New" controller="User" action="Create" area="" preservedRouteParameters="culture,projectid"/>
<mvcSiteMapNode title="Edit" controller="User" action="Edit" area="" preservedRouteParameters="culture,projectid,id"/>
<mvcSiteMapNode title="Profile" controller="User" action="Details" area="" preservedRouteParameters="culture,projectid,id"/>
</mvcSiteMapNode>
</mvcSiteMap>
I have few resources files in another project that i use for globalize my app, I need the resources files in a separate project because its used in few projects like ddl.
How can i implement globalization for my sitemap?
The approach I would take would be to switch to external DI and then implement a custom IStringLocalizer class that can read the resources from another assembly. Here is a working example. I have created a demo application on GitHub as well.
using System;
using System.Collections.Specialized;
using System.Resources;
namespace MvcSiteMapProvider.Globalization
{
public class ResourceManagerStringLocalizer
: IStringLocalizer
{
public ResourceManagerStringLocalizer(
ResourceManager resourceManager
)
{
if (resourceManager == null)
throw new ArgumentNullException("resourceManager");
this.resourceManager = resourceManager;
}
protected readonly ResourceManager resourceManager;
/// <summary>
/// Gets the localized text for the supplied attributeName.
/// </summary>
/// <param name="attributeName">The name of the attribute (as if it were in the original XML file).</param>
/// <param name="value">The current object's value of the attribute.</param>
/// <param name="enableLocalization">True if localization has been enabled, otherwise false.</param>
/// <param name="classKey">The resource key from the ISiteMap class.</param>
/// <param name="implicitResourceKey">The implicit resource key.</param>
/// <param name="explicitResourceKeys">A <see cref="T:System.Collections.Specialized.NameValueCollection"/> containing the explicit resource keys.</param>
/// <returns></returns>
public virtual string GetResourceString(string attributeName, string value, bool enableLocalization, string classKey, string implicitResourceKey, NameValueCollection explicitResourceKeys)
{
if (attributeName == null)
{
throw new ArgumentNullException("attributeName");
}
if (enableLocalization)
{
string result = string.Empty;
if (explicitResourceKeys != null)
{
string[] values = explicitResourceKeys.GetValues(attributeName);
if ((values == null) || (values.Length <= 1))
{
result = value;
}
else if (this.resourceManager.BaseName.Equals(values[0]))
{
try
{
result = this.resourceManager.GetString(values[1]);
}
catch (MissingManifestResourceException)
{
if (!string.IsNullOrEmpty(value))
{
result = value;
}
}
}
}
if (!string.IsNullOrEmpty(result))
{
return result;
}
}
if (!string.IsNullOrEmpty(value))
{
return value;
}
return string.Empty;
}
}
}
Then you can inject it into your DI configuration module (StructureMap example shown, but any DI container will do).
First of all, you need to specify not to register the IStringLocalizer interface automatically by adding it to the excludeTypes variable.
var excludeTypes = new Type[] {
// Use this array to add types you wish to explicitly exclude from convention-based
// auto-registration. By default all types that either match I[TypeName] = [TypeName] or
// I[TypeName] = [TypeName]Adapter will be automatically wired up as long as they don't
// have the [ExcludeFromAutoRegistrationAttribute].
//
// If you want to override a type that follows the convention, you should add the name
// of either the implementation name or the interface that it inherits to this list and
// add your manual registration code below. This will prevent duplicate registrations
// of the types from occurring.
// Example:
// typeof(SiteMap),
// typeof(SiteMapNodeVisibilityProviderStrategy)
typeof(IStringLocalizer)
};
Then provide an explicit registration of the ResourceManagerStringLocalizer (and its dependencies) instead.
// Configure localization
// Fully qualified namespace.resourcefile (.resx) name without the extension
string resourceBaseName = "SomeAssembly.Resources.Resource1";
// A reference to the assembly where your resources reside.
Assembly resourceAssembly = typeof(SomeAssembly.Class1).Assembly;
// Register the ResourceManager (note that this is application wide - if you are
// using ResourceManager in your DI setup already you may need to use a named
// instance or SmartInstance to specify a specific object to inject)
this.For<ResourceManager>().Use(() => new ResourceManager(resourceBaseName, resourceAssembly));
// Register the ResourceManagerStringLocalizer (uses the ResourceManger)
this.For<IStringLocalizer>().Use<ResourceManagerStringLocalizer>();
Then it is just a matter of specifying the resources appropriately. You need to start them with the Base Name (in this case SomeAssembly.Resources.Resource1), and then specify the key of the resource as the second argument.
<mvcSiteMapNode title="$resources:SomeAssembly.Resources.Resource1,ContactTitle" controller="Home" action="Contact"/>
Or
[MvcSiteMapNode(Title = "$resources:SomeAssembly.Resources.Resource1,ContactTitle", Controller = "Home", Action = "Contact)]
Note that getting the BaseName right is the key to making it work. See the following MSDN documentation: http://msdn.microsoft.com/en-us/library/yfsz7ac5(v=vs.110).aspx
I have controller Account and its method LogOn. How to create sitemap like this:
-Account // non clickable, just grouping
--Log On // clickable
? If I use site map in the form of XML-file (mvc.sitemap) I can write like this:
<mvcSiteMapNode
title="Account"
controller="Account"
action="LogOn"
clickable="false"
key="AccountGroup" >
<mvcSiteMapNode
title="Log On"
controller="Account"
action="LogOn"
key="LogOn" />
</mvcSiteMapNode>
But I want to do it with only MvcSiteMapNodeAttribute attribute. However, only one such attribute can be applied to the method. I also don't want to use some dummy method to create just grouping node.
For now I have only one approach - create grouping nodes in the XML mvc.sitemap file, and clickable nodes - with MvcSiteMapNodeAttribute attribute. But I want to escape to write site map by hands as far as possible. Can it be done without grouping nodes in the XML?
If using v4, you can apply multiple MvcSiteMapNodeAttributes to a single action method.
//
// GET: /Account/LogOn
[MvcSiteMapNodeAttribute(Title = "Account", ParentKey = "Home", Key = "AccountGroup", Clickable = false)]
[MvcSiteMapNodeAttribute(Title = "Log On", ParentKey = "AccountGroup", Key = "LogOn")]
public ActionResult LogOn()
{
return View();
}
You can also put the grouping node on the controller class if that is what you prefer (even in v3).
[MvcSiteMapNodeAttribute(Title = "Account", ParentKey = "Home", Action = "LogOn", Key = "AccountGroup", Clickable = false)]
public class AccountController
{
// Implementation here
}
I have an MVC site and we are using MVCSiteMapProvider 4.4.3 with Autofac. We construct our site using a mixture of XML and Attributes. We have a few hundred dynamic nodes and we have security trimming enabled. Site has been getting larger over the last year with approximately 120 controllers. All controllers are secured using authorize attributes that vary per role etc.
In our layout we call #Html.MvcSiteMap().SiteMapPath() this adds approximately 950ms onto page load time. If we remove the line our pages load almost instantly.
Our menu used to take a second to load - however we put it in a RenderAction and simply cached the result which largely fixed that issue.
Is this common performance? Is there any obvious way to enhance performance for SiteMapPath or things that may cause the performance to be so poor
If we reload the page it takes as long the second time as the first
Just browsing about ten pages and profiling but about 70% of CPU cyles seem to have gone to:
MvcSiteMapProvider.Caching.RequestCache.GetValue(String)
MvcSiteMapProvider.RequestCacheableSiteMapNode.GetCacheKey(String)
MvcSiteMapProvider.Collections.Specialized.RouteValueDictionary.GetCacheKey()
MvcSiteMapProvider.RequestCacheableSiteMap.GetCacheKey(String)
MvcSiteMapProvider.Web.Mvc.MvcContextFactory.CreateHttpContext(ISiteMapNode)
MvcSiteMapProvider.RequestCacheableSiteMapNode.get_AreRouteParametersPreserved()
MvcSiteMapProvider.SiteMap.GetChildNodes(ISiteMapNode)
MvcSiteMapProvider.SiteMap.FindSiteMapNodeFromControllerAction(ISiteMapNode, IDictionary[StringObject], RoutMvcSiteMapProvider.Collections.CacheableDictionary`2.ContainsKey(TKey)
eBase)
MvcSiteMapProvider.RequestCacheableSiteMap.IsAccessibleToUser(ISiteMapNode)
MvcSiteMapProvider.Collections.CacheableDictionary`2.get_ReadOperationDictionary()
The total calls to the MVCSiteMapProvder Namespace were 434 million vs 1 million for all of our own code Namespaces.
Our Autofac module is:
public class MvcSiteMapProviderModule : global::Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
const bool SecurityTrimmingEnabled = false;
const bool EnableLocalization = false;
var absoluteFileName = HostingEnvironment.MapPath("~/Mvc.sitemap");
var absoluteCacheExpiration = TimeSpan.FromMinutes(60);
var includeAssembliesForScan = new[] { "OnboardWeb" };
var currentAssembly = this.GetType().Assembly;
var siteMapProviderAssembly = typeof(SiteMaps).Assembly;
var allAssemblies = new Assembly[] { currentAssembly, siteMapProviderAssembly };
var excludeTypes = new Type[]
{
typeof(SiteMapNodeVisibilityProviderStrategy),
typeof(SiteMapXmlReservedAttributeNameProvider),
typeof(SiteMapBuilderSetStrategy)
};
var multipleImplementationTypes = new Type[]
{
typeof(ISiteMapNodeUrlResolver),
typeof(ISiteMapNodeVisibilityProvider),
typeof(IDynamicNodeProvider)
};
// Single implementations of interface with matching name (minus the "I").
CommonConventions.RegisterDefaultConventions(
(interfaceType, implementationType) => builder.RegisterType(implementationType).As(interfaceType).SingleInstance(),
new Assembly[] { siteMapProviderAssembly },
allAssemblies,
excludeTypes,
string.Empty);
// Multiple implementations of strategy based extension points
CommonConventions.RegisterAllImplementationsOfInterface(
(interfaceType, implementationType) => builder.RegisterType(implementationType).As(interfaceType).SingleInstance(),
multipleImplementationTypes,
allAssemblies,
excludeTypes,
"^Composite");
// Registration of internal controllers
CommonConventions.RegisterAllImplementationsOfInterface(
(interfaceType, implementationType) => builder.RegisterType(implementationType).As(interfaceType).AsSelf().InstancePerDependency(),
new Type[] { typeof(IController) },
new Assembly[] { siteMapProviderAssembly },
new Type[0],
string.Empty);
// Visibility Providers
builder.RegisterType<SiteMapNodeVisibilityProviderStrategy>()
.As<ISiteMapNodeVisibilityProviderStrategy>()
.WithParameter("defaultProviderName", string.Empty);
//.WithParameter("defaultProviderName", "MvcSiteMapProvider.FilteredSiteMapNodeVisibilityProvider, MvcSiteMapProvider");
//builder.RegisterType<BreadCrumbOnlyVisibilityProvider>()
// .As<ISiteMapNodeVisibilityProvider>().InstancePerLifetimeScope();
// Pass in the global controllerBuilder reference
builder.RegisterInstance(ControllerBuilder.Current)
.As<ControllerBuilder>();
builder.RegisterType<BuildManagerAdaptor>()
.As<IBuildManager>();
builder.RegisterType<ControllerBuilderAdaptor>()
.As<IControllerBuilder>();
builder.RegisterType<ControllerTypeResolverFactory>()
.As<IControllerTypeResolverFactory>()
.WithParameter("areaNamespacesToIgnore", new string[0]);
// Configure Security
builder.RegisterType<AuthorizeAttributeAclModule>()
.Named<IAclModule>("authorizeAttributeAclModule");
builder.RegisterType<XmlRolesAclModule>()
.Named<IAclModule>("xmlRolesAclModule");
builder.RegisterType<CompositeAclModule>()
.As<IAclModule>()
.WithParameter(
(p, c) => p.Name == "aclModules",
(p, c) => new[]
{
c.ResolveNamed<IAclModule>("authorizeAttributeAclModule"),
c.ResolveNamed<IAclModule>("xmlRolesAclModule")
});
builder.RegisterInstance(System.Runtime.Caching.MemoryCache.Default)
.As<System.Runtime.Caching.ObjectCache>();
builder.RegisterGeneric(typeof(RuntimeCacheProvider<>))
.As(typeof(ICacheProvider<>));
builder.RegisterType<RuntimeFileCacheDependency>()
.Named<ICacheDependency>("cacheDependency1")
.WithParameter("fileName", absoluteFileName);
builder.RegisterType<CacheDetails>()
.Named<ICacheDetails>("cacheDetails1")
.WithParameter("absoluteCacheExpiration", absoluteCacheExpiration)
.WithParameter("slidingCacheExpiration", TimeSpan.MinValue)
.WithParameter(
(p, c) => p.Name == "cacheDependency",
(p, c) => c.ResolveNamed<ICacheDependency>("cacheDependency1"));
// Configure the visitors
builder.RegisterType<UrlResolvingSiteMapNodeVisitor>()
.As<ISiteMapNodeVisitor>();
// Prepare for our node providers
builder.RegisterType<FileXmlSource>()
.Named<IXmlSource>("xmlSource1")
.WithParameter("fileName", absoluteFileName);
builder.RegisterType<SiteMapXmlReservedAttributeNameProvider>()
.As<ISiteMapXmlReservedAttributeNameProvider>()
.WithParameter("attributesToIgnore", new string[0]);
// Register the sitemap node providers
builder.RegisterType<XmlSiteMapNodeProvider>()
.Named<ISiteMapNodeProvider>("xmlSiteMapNodeProvider1")
.WithParameter("includeRootNode", true)
.WithParameter("useNestedDynamicNodeRecursion", false)
.WithParameter(
(p, c) => p.Name == "xmlSource",
(p, c) => c.ResolveNamed<IXmlSource>("xmlSource1"));
builder.RegisterType<ReflectionSiteMapNodeProvider>()
.Named<ISiteMapNodeProvider>("reflectionSiteMapNodeProvider1")
.WithParameter("includeAssemblies", includeAssembliesForScan)
.WithParameter("excludeAssemblies", new string[0]);
builder.RegisterType<CompositeSiteMapNodeProvider>()
.Named<ISiteMapNodeProvider>("siteMapNodeProvider1")
.WithParameter(
(p, c) => p.Name == "siteMapNodeProviders",
(p, c) => new[]
{
c.ResolveNamed<ISiteMapNodeProvider>("xmlSiteMapNodeProvider1"),
c.ResolveNamed<ISiteMapNodeProvider>("reflectionSiteMapNodeProvider1")
});
// Register the sitemap builders
builder.RegisterType<SiteMapBuilder>()
.Named<ISiteMapBuilder>("siteMapBuilder1")
.WithParameter(
(p, c) => p.Name == "siteMapNodeProvider",
(p, c) => c.ResolveNamed<ISiteMapNodeProvider>("siteMapNodeProvider1"));
// Configure the builder sets
builder.RegisterType<SiteMapBuilderSet>()
.Named<ISiteMapBuilderSet>("builderSet1")
.WithParameter("instanceName", "default")
.WithParameter("securityTrimmingEnabled", SecurityTrimmingEnabled)
.WithParameter("enableLocalization", EnableLocalization)
.WithParameter(
(p, c) => p.Name == "siteMapBuilder",
(p, c) => c.ResolveNamed<ISiteMapBuilder>("siteMapBuilder1"))
.WithParameter(
(p, c) => p.Name == "cacheDetails",
(p, c) => c.ResolveNamed<ICacheDetails>("cacheDetails1"));
builder.RegisterType<SiteMapBuilderSetStrategy>()
.As<ISiteMapBuilderSetStrategy>()
.WithParameter(
(p, c) => p.Name == "siteMapBuilderSets",
(p, c) => c.ResolveNamed<IEnumerable<ISiteMapBuilderSet>>("builderSet1"));
}
}
}
We have one dynamic node provider adding a few hundred nodes (if we turn it off it is faster but not significantly so)
public class LocationsDynamicNodeProvider : DynamicNodeProviderBase
{
private List<Country> countries;
/// <summary>
/// Lazy loading of countries. Only create the graph when we actually need it.
/// Previously it was in the constructor, but for lightweight object composition we must
/// not do any work in the constructor.
/// </summary>
/// <returns></returns>
private List<Country> GetCountries()
{
if (countries == null)
{
var countryRepository = DependencyResolver.Current.GetService<ICountryRepository>();
countries = countryRepository.AllWithLocations().ToList();
}
return countries;
}
public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
{
countries = GetCountries();
foreach (var country in countries)
{
var countrynode = new DynamicNode
{
Title = country.Name,
Controller = "Assets",
Action = "Index",
Area = "OnboardAsset",
RouteValues = new RouteValueDictionary
{
{ "countryname", country.Name },
{ "locationname", "" },
{ "sitename", "" }
},
ParentKey = "All Assets",
Key = "countrynode_" + country.CountryId
};
yield return countrynode;
foreach (var site in country.Sites)
{
var sitenode = new DynamicNode
{
Title = site.Name,
Controller = "Assets",
Action = "Index",
Area = "OnboardAsset",
RouteValues =
new RouteValueDictionary()
{
{ "countryname", country.Name },
{ "sitename", site.Name },
{ "locationname", "" }
},
ParentKey = "countrynode_" + country.CountryId,
Key = "sitenode_" + site.SiteId
};
yield return sitenode;
foreach (var location in site.Locations)
{
var locationNode = new DynamicNode
{
Title = location.Name,
Controller = "Assets",
Action = "Index",
Area = "OnboardAsset",
RouteValues =
new RouteValueDictionary
{
{ "countryname", country.Name },
{ "sitename", site.Name },
{ "locationname", location.Name }
},
ParentKey = "sitenode_" + site.SiteId
};
yield return locationNode;
}
}
}
}
}
}
SiteMap config:
<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="HomePage" action="Index" key="Home">
<mvcSiteMapNode title="People" key="PeopleTop" controller="People" action="Index" area="OnboardTeam" >
<mvcSiteMapNode title="All People" key="PeopleIndex" controller="People" action="Index" area="OnboardTeam" visibility="hideChildren" />
</mvcSiteMapNode>
<mvcSiteMapNode title="Assets" key="Assets" controller="Home" action="Index" area="OnboardAsset">
<mvcSiteMapNode title="All Assets" key="All Assets" controller="Assets" action="Index" route="AllAssets">
<mvcSiteMapNode title="LocationNodes" dynamicNodeProvider="Onboard.Web.Infrastructure.Menu.LocationsDynamicNodeProvider, OnboardWeb" />
</mvcSiteMapNode>
</mvcSiteMapNode>
<mvcSiteMapNode title="Jobs" controller="Jobs" action="Index" area="Core" key="Jobs" visibility="hideChildren" />
<mvcSiteMapNode title="Reports" key="Report" clickable="false">
<mvcSiteMapNode title="Certifications" key="Report_Certifications" clickable="false" />
</mvcSiteMapNode>
<mvcSiteMapNode title="CRM" controller= "CRM" area="CRM" key="CRM" action="Index">
</mvcSiteMapNode>
<mvcSiteMapNode title="PO" key="PO" action="GeneralList" controller= "PurchaseOrders">
<mvcSiteMapNode title="Purchase Orders" action="GeneralList" controller= "PurchaseOrders" area="PO" key="PO_List" />
</mvcSiteMapNode>
<mvcSiteMapNode title="Training" key="OnboardTraining" controller="PersonTrainingBookings" action="Index" />
<mvcSiteMapNode title="Document Store" key="Documents" area="Documents" controller="DocumentStore" action="Browse" />
<mvcSiteMapNode title="Admin" key="Admin" roles="Administrator" clickable="false">
<mvcSiteMapNode title="Competence" key="Competences" area="OnboardTeam" controller="Competences" action="Index" />
<mvcSiteMapNode title="Certification" key="Certifications" area="OnboardTeam" controller="Certification" action="Index" />
<mvcSiteMapNode title="Supporting Entities" key="LookupTable" clickable="false" />
<mvcSiteMapNode title="Entity Types" key="LookupTypes" clickable="false" />
<mvcSiteMapNode title="Users and Teams" key="UsersAndTeams" area="Core" controller="UserManagement" action="Index" clickable="false" />
<mvcSiteMapNode title="Companies" key="Organisations" area="Core" controller="Companies" action="Index" clickable="false" />
<mvcSiteMapNode title="Geographic Data" key="Geographic" area="Core" controller="Countries" action="Index" clickable="false" />
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMap>
Rest of nodes are adding using attributes on controller actions
We are running in release mode
I am pretty sure this was caused by issues with our sitemap config/setup. We have been relying on I think a bug in SiteMap that was previosuly preserving route data. However in v4 this was fixed.
Performance issue was caused when we visited actions which had a node attribute placed on but did not contain required preserved route data. Sitemap provider seemed to go a bit nuts I think trying to locate an appropriate node/route.
We are now placing a lot of preserveRouteData throughout code and it fixes the issues. We ideally would like to now create dynamic nodes (as our whole sitemap is based on various major entities e.g. People with lots of child nodes hung off them) to avoid this. However we have issue as we also want to use attributes to add children to dynamic nodes see here
If your site is running in debug mode it will cause performance degradation with the html helpers. Note that this is for V3 and V4 should have improved this.
from
http://mvcsitemap.codeplex.com/wikipage?title=HtmlHelper%20functions
Known performance issues and the solution A performance degradation
may be noticed working with HtmlHelper functions from Visual Studio.
This is because during debugging, no caching occurs internally in
ASP.NET MVC regarding view rendering. The solution to this is running
the application in release mode or changing Web.config to run under
release mode:
<compilation debug="false">
In my MVC3 project, I have installed Maartenba's MvcSiteMapProvider v.3.2.1 and I have a very simple, static, two-level menu that I have created. Below is the conceptual map structure.
- Home
- Member Center
- Member Listing [SELECTED]
- Event Calendar
- Documents
- Administration
Now, there are many sub-pages under Member Listing (e.g. Detail, Edit, etc.), but I don't want these displayed as 3rd level menu items (mainly because they are tied to a specific member ID). However, I do want all these third level pages to be "tied" to the Member Listing menu node so that it shows as selected when on these pages.
I have the following code in my Mvc.SiteMap file:
<mvcSiteMapNode title="Home" controller="Home" action="Index">
<mvcSiteMapNode title="Member Center" area="Members" controller="Home" action="Index" roles="Approved Member" >
<mvcSiteMapNode title="Member Listing" area="Members" controller="Member" action="List" />
<mvcSiteMapNode title="Event Calendar" area="Members" controller="Event" action="List" />
<mvcSiteMapNode title="Documents" area="Members" controller="Document" action="List" />
</mvcSiteMapNode>
<mvcSiteMapNode title="Administration" area="Admin" controller="Home" action="Index" roles="Site Administrator" >
</mvcSiteMapNode>
</mvcSiteMapNode>
To render the menu, I am using the following code in my _Layout.cshtml file:
#Html.MvcSiteMap().Menu(1, true, true, 1, true, true)
Finally, I modified the SiteMapNodeModel.cshtml file so that it adds a "selectedMenuItem" class to the node that correlates to the page the user is viewing. Here's the snippit that renders the menu node.
#model SiteMapNodeModel
#Model.Title
The display and navigation of the map works just fine, until I navigate further into the members area. For example, if I go past Members/Member/List (which displays the menu correctly) and to a page like Members/Member/Detail/1, the child nodes under Member Center ("Member Listing", "Event Calendar", etc.) disappear. Therefore, here are my two issues that I have with my current code:
I want to specify that any given page is part of the "Member Center" parent menu node, so that the child menu nodes of "Member Center" will be displayed, regardless of whether the given page is defined as a specific node in the menu structure.
I want to specify (possibly in the view or controller action) that a specific page should be tied to a specific menu node. For example, when the user is at Members/Member/Detail/1, I simply want the "Member Listing" child node to be specified as IsCurrentNode so that the SiteMapNodeModel.cshtml file properly decorates it with the "selectedMenuItem" class.
Any suggestions?
You can add 3rd level nodes to the sitemap XML and specify visibility to hide them from the menu. Here is the node declaration to display it only in breadcrumbs:
<mvcSiteMapNode area="Members"
controller="Member"
action="Detail"
visibility="SiteMapPathHelper,!*"
title="Member details" />
Edit:
As far as I know, you can not set IsCurrentNode. But you can check if menu node is currently selected with the following code (I use it in SiteMapNodeModel display template):
IList<string> classes = new List<string> ();
if (Model.IsCurrentNode || Model.IsInCurrentPath && !Model.Children.Any ())
{
classes.Add ("menu-current");
}
Adding to Max's answer I would also create an extension method for SiteMapNodeModel. Which you could use to implement all the custom logic needed to do this:
public static class SiteMapNodeModelExtender
{
public static bool IsRealCurrentNode(this SiteMapNodeModel node)
{
// Logic to determine the "real" current node...
// A naive implementation could be:
var currentPath = HttpContext.Current.Request.Url.AbsolutePath;
return currentPath.StartsWith("Members/Member/") && node.Title.Equals("Member Center")
}
}
and change the display template accordingly:
/* Also check IsRealCurrentNode, depending on the use case maybe only
IsRealCurrentNode */
#if ((Model.IsCurrentNode || Model.IsRealCurrentNode()) && Model.SourceMetadata["HtmlHelper"].ToString() != "MvcSiteMapProvider.Web.Html.MenuHelper") {
<text>#Model.Title</text>
} else if (Model.IsClickable) {
#Model.Title
} else {
<text>#Model.Title</text>
}
Further to Max Kiselev's answer, if you want to use that technique but be able to use attributes on your controller actions, I did the following:
Define a custom visibility provider:
public class AlwaysInvisibleVisibilityProvider : ISiteMapNodeVisibilityProvider
{
public bool IsVisible(SiteMapNode node, HttpContext context, IDictionary<string, object> sourceMetadata)
{
return false;
}
}
Then subclass MvcSiteMapNodeAttribute:
public class InvisibleMvcSiteMapNodeAttribute : MvcSiteMapNodeAttribute
{
public InvisibleMvcSiteMapNodeAttribute(string key, string parentKey)
{
Key = key;
ParentKey = parentKey;
VisibilityProvider = typeof (AlwaysInvisibleVisibilityProvider).AssemblyQualifiedName;
}
}
And you can then use it on your controller actions:
[HttpGet]
[InvisibleMvcSiteMapNodeAttribute("ThisNodeKey", "ParentNodeKey")]
public ViewResult OrderTimeout()
{
return View("Timeout");
}