Search code examples
c#asp.net-mvc-5mvcsitemapprovider

MvcSiteMapProvider Visibility directives in annotation doesn't work when implementing custom visibility provider


I'm configuring MvcSiteMapProvider with C# annotations and not through XML. I implemented a custom visibility provider based on the documentation. I derive my class from FilteredSiteMapNodeVisibilityProvider:

public class CustomVisibilityProvider: FilteredSiteMapNodeVisibilityProvider
{
    public override bool IsVisible(ISiteMapNode node, IDictionary<string, object> sourceMetadata)
    {
        if (node.Attributes.Keys.Contains("customVisibility"))
        {
            string customVisibility = (string)node.Attributes["customVisibility"];
            if (!string.IsNullOrEmpty(customVisibility))
            {
                customVisibility = customVisibility.Trim();
                ...
                var criteria = ...
                return criteria && base.IsVisible(node, sourceMetadata);
            }
        }

        return base.IsVisible(node, sourceMetadata);
    }
}

My Controller's view:

[MvcSiteMapNode(Title = "My View", ParentKey = "ParentController", Key = "MyView", Order = 922, PreservedRouteParameters = "id", Attributes = @"{ ""Visibility"": ""SiteMapPathHelper,!*"" }")]
public ActionResult MyView(int? id)
{
    return ViewForEntity(id);
}

As we can see I didn't use my own customVisibility attribute with this view, but I'd like to use the standard Visibility attributes. This particular view shouldn't appear in the menu or elsewhere, except in the SiteMap.

The problem is that when this view's SiteMapNode is examined for visibility in the menu (a.k.a. (string)sourceMetadata["HtmlHelper"] == "MvcSiteMapProvider.Web.Html.SiteMapPathHelper"), the base.IsVisible(node, sourceMetadata) returns true. I'd expect that the FilteredSiteMapNodeVisibilityProvider will handle the Visibility attribute and return false, seeing that this view should only appear in the SiteMap.

As a workaround I currently implemented my own check:

    private bool checkDefaultVisibility(ISiteMapNode node, IDictionary<string, object> sourceMetadata)
    {
        bool defaultVisibility = sourceMetadata["HtmlHelper"] == null || !node.Attributes.Keys.Contains("Visibility");
        if (sourceMetadata["HtmlHelper"] != null && node.Attributes.Keys.Contains("Visibility"))
        {
            var htmlHelper = (string)sourceMetadata["HtmlHelper"];  // Example: "MvcSiteMapProvider.Web.Html.SiteMapPathHelper"
            var helpersRules = ((string)node.Attributes["Visibility"]).Split(',');
            foreach(var helperRule in helpersRules)
            {
                if (helperRule != "!*" && htmlHelper.EndsWith("." + helperRule))
                {
                    defaultVisibility = true;
                    break;
                }
            }
        }
        return defaultVisibility;
    }

This is a method of my custom visibility provider. I hate it, because it's not universal, only handles specific cases. At the same time I don't want to reinvent the wheel here. I want Visibility to be handled by the MvcSiteMapProvider internals. How to achieve that?


Solution

  • Dictionary keys are case sensitive. The reason why FilteredSiteMapVisibilityProvider is not returning false is because you have set an attribute named Visibility instead of the expected name visibility.

    [MvcSiteMapNode(Title = "My View", ParentKey = "ParentController", Key = "MyView", Order = 922, PreservedRouteParameters = "id", Attributes = @"{ ""visibility"": ""SiteMapPathHelper,!*"" }")]
    

    As far as making visibility "universal", that would be difficult to do since everyone has different visibility requirements. The purpose of visibility providers is to implement your own visibility requirements. FilteredSiteMapNodeVisibilityProvider is about as generic as it can get, and if that doesn't meet your requirements you need to go custom.

    Do note you can override the default visibility provider for specific nodes by setting the VisibilityProvider to the type string of the custom visibility provider class.

    [MvcSiteMapNode(Title = "My View", ParentKey = "ParentController", Key = "MyView", Order = 922, PreservedRouteParameters = "id", VisibilityProvider = "MyNamespace.CustomVisibilityProvider, MyAssembly", Attributes = @"{ ""customVisibility"": ""foo"" }")]
    

    If you need to use more than one visibility provider on the same node, it can be done using external DI and the CompositeSiteMapNodeVisibilityProvider as shown here.

    Note that you can inherit this class for use with the internal DI container if you need to - but for internal DI you need a default constructor, so the types that it uses internally must be hard-coded into the constructor. But you can create as many of these classes as you need for your entire visibility configuration.

    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()
            )
        { }
    }
    

    And then calling it by using:

    [MvcSiteMapNode(Title = "My View", ParentKey = "ParentController", Key = "MyView", Order = 922, PreservedRouteParameters = "id", VisibilityProvider = "MyNamespace.MyCompositeVisibilityProvider, MyAssembly", Attributes = @"{ ""visibility"": ""SiteMapPathHelper,!*"", ""customVisibility"": ""foo"" }")]
    

    Also note there are many other ways to control visibility including security trimming, customizing the templates (or creating new templates and specifying the templateName in the HTML helper explicitly) in the /Views/Shared/DisplayTemplates/ folder, or even creating custom HTML helpers.