How do I make MvcSiteMapProvider create 1 SiteMap per Area?

How do you make 1 SiteMap per MVC area and use MvcSiteMapNodeAttribute at the same time?

Answers


Please have a look at this answer for help with setting up MvcSiteMapProvider with areas. The routes have to be configured using the correct conventions or it won't work right.

However, that alone isn't going to address this requirement, because there is no default assumption made that you want to have a different SiteMap per area.

The behavior of the internal DI container assumes that there will be 1 SiteMap per domain name, and that all of the SiteMaps in the application will be built using the same configuration. There is no way to change this behavior unless you use an external DI container and follow the instructions in Multiple SiteMaps in One Application to override it.

Option 1

You could continue using the internal DI container and a single SiteMap for the entire website and you could create a custom ISiteMapNodeVisibilityProvider that hides everything that is not in the current area by reading the area from the current request.

public class AreaSiteMapNodeVisibilityProvider
    : SiteMapNodeVisibilityProviderBase
{
    public AreaSiteMapNodeVisibilityProvider()
    {
        // NOTE: Accept this as a constructor parameter if using external DI and 
        // use a guard clause to ensure it is not null.
        this.mvcContextFactory = new MvcSiteMapProvider.Web.Mvc.MvcContextFactory();
    }
    private readonly MvcSiteMapProvider.Web.Mvc.IMvcContextFactory mvcContextFactory;

    #region ISiteMapNodeVisibilityProvider Members

    public override bool IsVisible(ISiteMapNode node, IDictionary<string, object> sourceMetadata)
    {
        var requestContext = this.mvcContextFactory.CreateRequestContext();
        var area = requestContext.RouteData.DataTokens["area"];
        var areaName = area == null ? string.Empty : area.ToString();
        return string.Equals(node.Area, areaName, StringComparison.OrdinalIgnoreCase);
    }

    #endregion
}

Then set it up as the default visibility provider.

<add key="MvcSiteMapProvider_DefaultSiteMapNodeVisibiltyProvider" value="MyNameSpace.AreaSiteMapNodeVisibilityProvider, MyAssemblyName" />

Using external DI (StructureMap example shown):

// Visibility Providers
this.For<ISiteMapNodeVisibilityProviderStrategy>().Use<SiteMapNodeVisibilityProviderStrategy>()
    .Ctor<string>("defaultProviderName").Is("MyNameSpace.AreaSiteMapNodeVisibilityProvider, MyAssemblyName");

Do note that you will still need to nest your area nodes below the non-area part of the site if you do this, so it might not behave as you would like. You need to ensure you set the parent key of the Admin area to a key of a node in the non-area part - there can only be 1 root node per SiteMap.

Also, if you go this route, be sure to set the MvcSiteMapProvider_VisibilityAffectsDescendants setting to "false" so your area nodes are not affected by the visibility of the non-area nodes.

Option 2

Inject a custom ISiteMapCacheKeyGenerator that is based on area and use the SiteMapCacheKey property of [MvcSiteMapNode] attribute to control which area the node belongs to.

public class AreaSiteMapCacheKeyGenerator
    : ISiteMapCacheKeyGenerator
{
    public AreaSiteMapCacheKeyGenerator(
        IMvcContextFactory mvcContextFactory
        )
    {
        if (mvcContextFactory == null)
            throw new ArgumentNullException("mvcContextFactory");
        this.mvcContextFactory = mvcContextFactory;
    }

    protected readonly IMvcContextFactory mvcContextFactory;

    #region ISiteMapCacheKeyGenerator Members

    public virtual string GenerateKey()
    {
        var requestContext = this.mvcContextFactory.CreateRequestContext();
        var area = requestContext.RouteData.DataTokens["area"];
        return area == null ? "default" : area.ToString();
    }

    #endregion
}

You need to inject this using external DI (StructureMap example shown):

this.For<ISiteMapCacheKeyGenerator>().Use<AreaSiteMapCacheKeyGenerator>();

And then configure your [MvcSiteMapNode] attributes:

[MvcSiteMapNode(Title = "title", Description = "desc", Key = "root", ParentKey = null, ImageUrl = "fa-home", Order = 0, SiteMapCacheKey = "Admin")]
[MvcSiteMapNode(Title = "title", Description = "desc", Key = "root", ParentKey = null, ImageUrl = "fa-home", Order = 0, SiteMapCacheKey = "default")]
Option 3

Rather than setting SiteMapCacheKey on every [MvcSiteMapNode] attribute, you could put each area in a separate assembly and configure it to only scan the pertinent area assembly for [MvcSiteMapNode] attribute.

public class AreaSiteMapCacheKeyGenerator
    : ISiteMapCacheKeyGenerator
{
    public AreaSiteMapCacheKeyGenerator(
        IMvcContextFactory mvcContextFactory
        )
    {
        if (mvcContextFactory == null)
            throw new ArgumentNullException("mvcContextFactory");
        this.mvcContextFactory = mvcContextFactory;
    }

    protected readonly IMvcContextFactory mvcContextFactory;

    #region ISiteMapCacheKeyGenerator Members

    public virtual string GenerateKey()
    {
        var requestContext = this.mvcContextFactory.CreateRequestContext();
        var area = requestContext.RouteData.DataTokens["area"];
        return area == null ? "default" : area.ToString();
    }

    #endregion
}

public class OneToOneSiteMapCacheKeyToBuilderSetMapper
    : ISiteMapCacheKeyToBuilderSetMapper
{
    public virtual string GetBuilderSetName(string cacheKey)
    {
        return cacheKey;
    }
}

In the external DI module (StructureMap example shown):

// Setup the cache
var cacheDependency = this.For<ICacheDependency>().Use<NullCacheDependency>();

var cacheDetails = this.For<ICacheDetails>().Use<CacheDetails>()
    .Ctor<TimeSpan>("absoluteCacheExpiration").Is(absoluteCacheExpiration)
    .Ctor<TimeSpan>("slidingCacheExpiration").Is(TimeSpan.MinValue)
    .Ctor<ICacheDependency>().Is(cacheDependency);

// Register the ISiteMapNodeProvider instances
var defaultNodeProvider = this.For<ISiteMapNodeProvider>().Use<ReflectionSiteMapNodeProvider>()
    .Ctor<bool>("includeAssemblies").Is(new string[] { "dllmain" });

var adminNodeProvider = this.For<ISiteMapNodeProvider>().Use<ReflectionSiteMapNodeProvider>()
    .Ctor<bool>("includeAssemblies").Is(new string[] { "dll2" });

// Register the ISiteMapBuilder instances
var defaultBuilder = this.For<ISiteMapBuilder>().Use<SiteMapBuilder>()
    .Ctor<ISiteMapNodeProvider>().Is(defaultNodeProvider);

var adminBuilder = this.For<ISiteMapBuilder>().Use<SiteMapBuilder>()
    .Ctor<ISiteMapNodeProvider>().Is(adminNodeProvider);

// Register the builder sets
this.For<ISiteMapBuilderSetStrategy>().Use<SiteMapBuilderSetStrategy>()
    .EnumerableOf<ISiteMapBuilderSet>().Contains(x =>
    {
        // SiteMap builder for the non-area part of the site
        x.Type<SiteMapBuilderSet>()
            .Ctor<string>("instanceName").Is("default")
            .Ctor<bool>("securityTrimmingEnabled").Is(false)
            .Ctor<bool>("enableLocalization").Is(false)
            .Ctor<bool>("visibilityAffectsDescendants").Is(false)
            .Ctor<bool>("useTitleIfDescriptionNotProvided").Is(true)
            .Ctor<ISiteMapBuilder>().Is(defaultBuilder)
            .Ctor<ICacheDetails>().Is(cacheDetails);

        // SiteMap builder for the Admin area of the site
        x.Type<SiteMapBuilderSet>()
            .Ctor<string>("instanceName").Is("Admin")
            .Ctor<bool>("securityTrimmingEnabled").Is(false)
            .Ctor<bool>("enableLocalization").Is(false)
            .Ctor<bool>("visibilityAffectsDescendants").Is(false)
            .Ctor<bool>("useTitleIfDescriptionNotProvided").Is(true)
            .Ctor<ISiteMapBuilder>().Is(adminBuilder)
            .Ctor<ICacheDetails>().Is(cacheDetails);
    });

// Register the custom ISiteMapCacheKeyGenerator and ISiteMapCacheKeyToBuilderSetMapper
this.For<ISiteMapCacheKeyGenerator>().Use<AreaSiteMapCacheKeyGenerator>();
this.For<ISiteMapCacheKeyToBuilderSetMapper>().Use<OneToOneSiteMapCacheKeyToBuilderSetMapper>();

Need Your Help

How do I parse timezones with UTC offsets in Python?

python datetime

Let's say I have a timezone like "2009-08-18 13:52:54-04". I can parse most of it using a line like this:

Adding Constraints to Nested Eloquent Relationships in Laravel 4

php laravel-4 constraints relationship eloquent

I have 3 related tables as follows Sections --> AgeGroups --> Teams.