asp.net-mvcasp.net-mvc-5asp.net-mvc-routingmvcsitemapprovider

Using MvcSiteMapProvider with Attributes and Attribute Routing


I am trying to use MVCSiteMapProvider in my ASP.Net MVC 5 application. Lots of resources and tutorials can be found but most of them is about XML based configuration.

In my application attribute routing is already used and i want to use MvcSiteMapProvider with attributes But there aren't enough resource about this mix and i have some problems.

For example i have got three actions like below:

//HomeController    
    [Route(@"~/home", Name = "CustomerHomeIndex")]
        [MvcSiteMapNode(Title = "Home Page",  Key = "Home")] 
        public ActionResult Index() {
            return View()
        }

//AccountController       
        [Route(@"~/account", Name = "AccountIndex")]
        [MvcSiteMapNode(Title = "Accounts", ParentKey = "Home", Key = "AccountIndex")] 
        public ActionResult Index() {
        // fetching records from database
            return View();
        }

        [Route(@"~/account-management/{id:int}/{domain:regex(^([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$)}", Name = "AccountDetail")]
        [MvcSiteMapNode(Title = "Account Detail", ParentKey = "AccountIndex", Key = "AccountDetail")] 
        public ActionResult Details(string domain, int id) {
        // fetching record from database by parameters
            return View();
        }

I also added SiteMapPath code to my view

//Details.cshtml
    @Html.MvcSiteMap().SiteMapPath()

But nothing is showing as a result. In my opinion it is about preservedRouteParameters but i couldn't find anything about this parameter used in MvcSiteMapNode attribute with attribute routing.

Actually i have got another question about localization, i want to get titles from resource file, everything is already exists in global resource file. I read something about localization support but they are related with XML based configuration, too.


Solution

  • By default, both the providers for XML and .NET attributes are enabled. In this configuration, the root node (the one with no parent key) must be placed in the XML file. To use .NET attributes exclusively without any configuration in XML, you need to remove the XML node provider from the configuration.

    Internal DI:

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

    External DI (StructureMap example shown):

    // Register the sitemap node providers
    var siteMapNodeProvider = this.For<ISiteMapNodeProvider>().Use<CompositeSiteMapNodeProvider>()
        .EnumerableOf<ISiteMapNodeProvider>().Contains(x =>
        {
            //Remove the XmlSiteMapNodeProvider
            //x.Type<XmlSiteMapNodeProvider>()
            //    .Ctor<bool>("includeRootNode").Is(true)
            //    .Ctor<bool>("useNestedDynamicNodeRecursion").Is(false)
            //    .Ctor<IXmlSource>().Is(xmlSource);
            x.Type<ReflectionSiteMapNodeProvider>()
                .Ctor<IEnumerable<string>>("includeAssemblies").Is(includeAssembliesForScan)
                .Ctor<IEnumerable<string>>("excludeAssemblies").Is(new string[0]);
        });
    

    You also need to make sure the assembly with your controllers is included in the IncludeAssembliesForScan configuration setting. Note that the NuGet package automatically includes the assembly that you install MvcSiteMapProvider into, so if your controllers are all in your main MVC project you don't need to touch this.

    Internal DI:

    <appSettings>
        <add key="MvcSiteMapProvider_IncludeAssembliesForScan" value="MyAssembly,MyOtherAssembly"/>
    </appSettings>
    

    External DI:

    string[] includeAssembliesForScan = new string[] { "MyAssembly", "MyOtherAssembly" };
    
    ... Other code omitted ...
    
    // Register the sitemap node providers
    var siteMapNodeProvider = this.For<ISiteMapNodeProvider>().Use<CompositeSiteMapNodeProvider>()
        .EnumerableOf<ISiteMapNodeProvider>().Contains(x =>
        {
            //Remove the XmlSiteMapNodeProvider
            //x.Type<XmlSiteMapNodeProvider>()
            //    .Ctor<bool>("includeRootNode").Is(true)
            //    .Ctor<bool>("useNestedDynamicNodeRecursion").Is(false)
            //    .Ctor<IXmlSource>().Is(xmlSource);
            x.Type<ReflectionSiteMapNodeProvider>()
                .Ctor<IEnumerable<string>>("includeAssemblies").Is(includeAssembliesForScan) // <- Setting is injected here
                .Ctor<IEnumerable<string>>("excludeAssemblies").Is(new string[0]);
        });
    

    There is nothing special you need to do to make it work with AttributeRouting - MvcSiteMapProvider automatically picks up those routes so as long as you have them configured correctly and working in MVC it should just work.

    Yes, you probably do need to use PreservedRouteParameters for your actions that contain custom parameters, like so.

    [Route(@"~/account-management/{id:int}/{domain:regex(^([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$)}", Name = "AccountDetail")]
    [MvcSiteMapNode(Title = "Account Detail", ParentKey = "AccountIndex", Key = "AccountDetail", PreservedRouteParameters="domain,id")] 
    public ActionResult Details(string domain, int id) {
    // fetching record from database by parameters
        return View();
    }
    

    In your simple example, this will work fine. However, you need to be fully aware of how preservedRouteParameters work to use them correctly when nesting levels of nodes beyond the first one. You cannot use parameters with the same key name with different meanings that are both visible in the same request because MvcSiteMapProvider always inserts the value from the current request into all node with matching keys names. You must also provide any keys required by ancestor nodes in the request (of the child node) so the navigation will work. See How to Make MvcSiteMapProvider Remember a User's Position and demo code for complete details.

    See this for reading localization from an external assembly. However, do note that the only way this is possible as of v4.6.15 is to use an external DI container to inject a custom IStringLocalizer.

    The default localization implementation can only support files put into the App_GlobalResources folder. Note that this is problematic with MVC because the default settings when adding these files make them compile in a way that is not accessible from MVC. We are currently gathering requirements to make a new extension point that allows configuring resources from alternate locations.