Skip to content

Advanced Node Visibility with ISiteMapNodeVisibilityProvider

Shad Storhaug edited this page Jul 15, 2016 · 8 revisions

In some situations, nodes should be visible in some HTML helpers but not others. For example, you may want a node to be visible in the SiteMapPath HTML Helper but not in the rest of the HTML Helpers or the sitemaps XML endpoint. This can be accomplished using concrete implementations of ISiteMapNodeVisibilityProvider that can be specified globally for every node in the SiteMap or individually on a specific SiteMap node.

Note: Some alternatives to visibility providers are:

  1. Security Trimming
  2. Editing the HTML Helper Templates in the \Views\Shared\DisplayTemplates\ folder
  3. Creating custom named HTML Helper Templates in the \Views\Shared\DisplayTemplates\ folder, and specifying the templateName in the HTML helper, for example @Html.MvcSiteMap().Menu(templateName: "MyCustomTemplate")
  4. Creating a custom HTML Helper

Built-in ISiteMapNodeVisibilityProvider Implementations

Type Usage
FilteredSiteMapNodeVisibilityProvider Allows you to use magic string conventions to make nodes visible/invisible based on the HTML helper and/or whether the current node is in selection path
TrimEmptyGroupingNodesVisibilityProvider Makes a parent node invisible automatically when all of its children are invisible
CompositeSiteMapNodeVisibilityProvider Allows usage of multiple ISiteMapNodeVisibilityProvider instances per node

It is also possible to implement your own ISiteMapNodeVisibilityProvider if you have complex visibility logic.

Setting the Default ISiteMapNodeVisibilityProvider Globally

Internal DI

Modify (or add) the MvcSiteMapProvider_DefaultSiteMapNodeVisibiltyProvider in the web.config file.

<appSettings>
	<add key="MvcSiteMapProvider_DefaultSiteMapNodeVisibiltyProvider" value="MvcSiteMapProvider.FilteredSiteMapNodeVisibilityProvider, MvcSiteMapProvider"/>
</appSettings>

External DI

Set the defaultProviderName constructor argument of SiteMapNodeVisibilityProviderStrategy in your MvcSiteMapProvider module.

Note: The name of the module varies depending on the DI container and can be changed after installing the NuGet package, but each module is located under the \DI\<containerName>\Modules\ folder in your project by default.

This is how that would look with Ninject:

this.Kernel.Bind<ISiteMapNodeVisibilityProviderStrategy>().To<SiteMapNodeVisibilityProviderStrategy>()
    .WithConstructorArgument("defaultProviderName", "MvcSiteMapProvider.FilteredSiteMapNodeVisibilityProvider, MvcSiteMapProvider");

Setting the ISiteMapNodeVisibilityProvider Locally

It is possible to override the default visibility provider with another one by specifying the visibility provider directly on the mvcSiteMapNode element. This means that each node can have a separate visibility provider if you have different complex logic that needs to be specified for a node or group of nodes.

XML

<mvcSiteMapNode title="Settings" area="Admin" visibility="Condition2" visibilityProvider="Namespace.MyCustomVisibilityProvider2, AssemblyName" />

MvcSiteMapNodeAttribute

[MvcSiteMapNode(Title = "My View", ParentKey = "ParentController", Key = "MyView", VisibilityProvider = "Namespace.MyCustomVisibilityProvider2, AssemblyName", Attributes = @"{ ""visibility"": ""Condition2"" }")]

Using Multiple ISiteMapNodeVisibilityProvider Implementations at a Time

You can use several different visibility providers simultaneously to implement individual rules without violating the Single Responsibility Principle. To do so, you can use the CompositeSiteMapNodeVisibilityProvider.

Internal DI

Since using visibility providers with internal DI requires a default constructor, you need to subclass the CompositeSiteMapNodeVisibilityProvider in order to use it with the internal DI container, and provide your configuration to the constructor of CompositeSiteMapNodeVisibilityProvider.

using MvcSiteMapProvider;
using MvcSiteMapProvider.Reflection;

public class MyCompositeVisibilityProvider : CompositeSiteMapNodeVisibilityProvider
{
    public MyCompositeVisibilityProvider()
        : base(
            typeof(MyCompositeVisibilityProvider).ShortAssemblyQualifiedName(), 

            // Note that the visibility providers are executed in
            // the order specified here, but execution stops when
            // the first visibility provider returns false.
            new FilteredSiteMapNodeVisibilityProvider(),
            new TrimEmptyGroupingNodesVisibilityProvider(),
            new CustomVisibilityProvider()
        )
    { }
}

Note: For internal DI the first parameter must be the name of the type of your custom class. The reason for this is that the name is also used to instantiate the class using Activator.CreateInstance. The ShortAssemblyQualifiedName is an extension method that returns the name of the type including namespace and assembly, but excluding the version, culture, and public key token. You must match this string exactly including casing and the space after the comma when configuring the type globally or in individual nodes.

The above class would need to be referenced as:

<Fully Qualified Namespace>.MyCompositeVisibilityProvider, <Assembly Name>

For example:

<add key="MvcSiteMapProvider_DefaultSiteMapNodeVisibiltyProvider" value="Acme.MvcSiteMapExtensions.MyCompositeVisibilityProvider, Acme"/>

External DI

For external DI, you need to inject each CompositeSiteMapNodeVisibilityProvider configuration into the SiteMapNodeVisibilityProviderStrategy class constructor. This should be done inside of the MvcSiteMapProvider DI module. Here is an example using StructureMap:

// Visibility Providers

// Explicitly set the visibility providers, using CompositeSiteMapNodeVisibilityProvider to combine the AclModuleVisibilityProvider
// with all other ISiteMapNodeVisibilityProvider implementations.
this.For<ISiteMapNodeVisibilityProviderStrategy>().Use<SiteMapNodeVisibilityProviderStrategy>()
    .EnumerableOf<ISiteMapNodeVisibilityProvider>().Contains(x =>
        {
            x.Type<CompositeSiteMapNodeVisibilityProvider>()
                .Ctor<string>("instanceName").Is("filteredAndTrimmedAndCustom")
                .EnumerableOf<ISiteMapNodeVisibilityProvider>().Contains(y =>
                    {
						// Note that the visibility providers are executed in
                		// the order specified here, but execution stops when
            			// the first visibility provider returns false.
                        y.Type<FilteredSiteMapNodeVisibilityProvider>();
                        y.Type<TrimEmptyGroupingNodesVisibilityProvider>();
                        y.Type<CustomVisibilityProvider>();
                    });

            // TODO: Add additional combined visibility logic if using custom providers, as shown.
            //x.Type<CompositeSiteMapNodeVisibilityProvider>()
            //    .Ctor<string>("instanceName").Is("aclAndOtherCustom")
            //    .EnumerableOf<ISiteMapNodeVisibilityProvider>().Contains(y =>
            //    {
            //        y.Type<AclModuleVisibilityProvider>();
            //        y.Type<MyOtherCustomVisibilityProvider>();
            //    });
        })
    .Ctor<string>("defaultProviderName").Is("filteredAndTrimmedAndCustom");

Note: When using external DI, you don't need to use .NET type names to specify the visibility provider instance to use.

<mvcSiteMapNode title="Custom Combined Visibility" visibility="Condition2" visibilityProvider="aclAndOtherCustom" />

VisibilityAffectsDescendants Setting

You can control the setting of whether the visibility of the parent node will be inherited by its children using the VisibilityAffectsDescendants setting. This setting can be applied SiteMap wide or specified explicitly on by calling one of the overloads that accept the visibilityAffectsDescendants parameter on the Menu or SiteMap HTML helpers. Although it is not the default setting, we recommend setting this value to false for the most flexibility with visibility providers, which will allow you to make visible children of invisible parent nodes.

For internal DI you can set the property SiteMap wide using the following setting in web.config:

<appSettings>
    <add key="MvcSiteMapProvider_VisibilityAffectsDescendants" value="false"/>
</appSettings>

For external DI, you can just set the variable on at the top of the MvcSiteMapProvider DI module file, which is passed into the constructor of the SiteMapBuilderSet class.

bool visibilityAffectsDescendants = false;

Using FilteredSiteMapNodeVisibilityProvider

Register the FilteredSiteMapNodeVisibilityProvider either globally or on a specific node.

For every node, specify the visibility attribute. Here's an example in XML:

<mvcSiteMapNode title="Administration" area="Admin" clickable="false" visibility="SiteMapPathHelper,!*" />

And here is the same example using SiteMapNodeAttribute.

[MvcSiteMapNode(Title = "Administration", Clickable = false, Attributes = @"{ ""visibility"": ""SiteMapPathHelper,!*"" }")]

Note: Due to data type restrictions of .NET attributes, the Attributes must be declared as a JSON string. The key names are case sensitive, so you should use visibility rather than Visibility.

The visibility attribute can contain a comma-separated list of controls in which the SiteMap node should be rendered or not. These are processed left-to-right and can be inverted using an exclamation mark. Here's a break down of the above example:

Directive Meaning
SiteMapPathHelper The node is visible in the SiteMapPathHelper.
!* The node is invisible in any other control.

Note that an implicit * (The node is visible in any control) is added at the end of the list.

Here is a list of the types that can be specified in the FilteredSiteMapNodeVisibilityProvider.

Type What it Affects
CanonicalHelper The Canonical HTML Helper
MenuHelper The Menu HTML Helper
MetaRobotsHelper The Meta Robots HTML Helper
SiteMapHelper The SiteMap HTML Helper
SiteMapPathHelper The SiteMapPath HTML Helper
SiteMapTitleHelper The Title HTML Helper
XmlSiteMapResult The sitemaps XML output of the /sitemap.xml endpoint

Named HTML Helper Instances

As of v4.4.8 you can use the FilteredSiteMapNodeVisibilityProvider to specify named instances of the Menu and other HTML helpers instead of specifying the type of control. This is helpful if for example you wanted to have a main menu and a footer menu.

To name an HTML helper, call one of the overrides that has a sourceMetadata argument and pass the name.

@Html.MvcSiteMap().Menu(new { name = "MainMenu" })
@Html.MvcSiteMap().Menu(new { name = "FooterMenu" })

You can then use the name instead of the type name to specify visible or not visible, as in the previous example.

<mvcSiteMapNode title="Home" controller="Home" action="Index" visibility="MenuHelper,!*" />
<mvcSiteMapNode title="Contact" controller="Home" action="Contact" visibility="MainMenu,!*" />
<mvcSiteMapNode title="About" controller="Home" action="About" visibility="FooterMenu,!*" />

This example will make the home page visible on all menus (but invisible everywhere else), make the contact page visible only on the main menu, and make the about page visible only on the footer menu.

Visible Only if Selected

You can add the suffix IfSelected to either the HTML helper type name or the named instance name to make the node only visible if it is in the current path (that is, between the current node and the root node, inclusive).

<mvcSiteMapNode title="Contact" controller="Home" action="Contact" visibility="MainMenuIfSelected,!*" />

You can also specify this behavior for all HTML helpers and the /sitemap.xml endpoint at once using IfSelected.

<mvcSiteMapNode title="Contact" controller="Home" action="Contact" visibility="IfSelected,!*" />

Creating a Custom ISiteMapNodeVisibilityProvider

In order to create a custom ISiteMapNodeVisibilityProvider, subclass the SiteMapNodeVisibilityProviderBase abstract class. This class saves you some work by implementing the type comparison logic for you.

Note: It is also possible to implement ISiteMapNodeVisibilityProvider directly.

Custom ISiteMapNodeVisibilityProvider Example

using System.Collections.Generic;
using System.Web;
using MvcSiteMapProvider;

namespace MyCompany
{
    public class MyCustomVisibilityProvider : SiteMapNodeVisibilityProviderBase
    {
        public bool IsVisible(ISiteMapNode node, IDictionary<string, object> sourceMetadata)
        {
            // Is a visibility attribute specified?
            string visibility = node.Attributes["visibility"];
            if (string.IsNullOrEmpty(visibility))
            {
                return true;
            }
            visibility = visibility.Trim();
            
            //process visibility
            switch (visibility)
            {
                case "Condition1":
                    //...
                    return false;

                case "Condition2":
                    //...
                    return false;
            }

            return true;
        }
    }
}

Custom ISiteMapNodeVisibilityProvider Usage

First, register the .NET type name either globally or locally.

Also, in this case we are using the visibility attribute (a custom attribute). So we need to set it on the nodes that the visibility provider affects. Here's an example in XML:

<mvcSiteMapNode title="Administration" area="Admin" visibility="Condition1" />
<mvcSiteMapNode title="Settings" area="Admin" visibility="Condition2" />

Note: The default behavior of MvcSiteMapProvider will automatically make all descendant nodes invisible when a node is made invisible. To change the behavior to allow visible nodes of invisible ancestor nodes, you should change the VisibilityAffectsDescendants Setting.

Passing Custom Information

Note that if you need to, you can pass custom information from the HTML helper declaration to your visibility provider by using the sourceMetadata parameter of the HTML helper (or the /sitemaps.xml endpoint) as shown in the section Named HTML Helper Instances above.

@Html.MvcSiteMap().SiteMapPath(new { someKey = "Some Value" })

It will then be available through the sourceMetadata parameter of the IsVisible method of your custom visibility provider.

public class MyCustomVisibilityProvider : SiteMapNodeVisibilityProviderBase
{
	public bool IsVisible(ISiteMapNode node, IDictionary<string, object> sourceMetadata)
	{
		// Retrieve the value named someKey
		var value = sourceMetadata["someKey"];
		
		// Value of value variable will be "Some Value"
	}
}

You can also use custom attributes on SiteMap nodes to pass information to your custom visibility providers on a per node basis. See Creating a Custom ISiteMapNodeVisibilityProvider for an example.


Want to contribute? See our [Contributing to MvcSiteMapProvider] (https://github.com/maartenba/MvcSiteMapProvider/blob/master/CONTRIBUTING.md) guide.



[Version 3.x Documentation] (Version-3.x-Documentation)


Unofficial Documentation and Resources

Other places around the web have some documentation that is helpful for getting started and finding answers that are not found here.

Tutorials and Demos

Version 4.x
Version 3.x

Forums and Q & A Sites

Other Blog Posts

Clone this wiki locally