Search code examples
c#asp.net-mvc-4mefmvcsitemapprovider

MvcSiteMapProvider : How to load extra web.sitemap files from MEF-enabled module in MVC 4


I have a separate Module in a C# class library project which is loaded via MEF Imports. As an initial attempt at bringing sitemap information along with the Module I have added a web.sitemap file with the necessary mark-up but I can't seem to get a clear sense of how to load it and attach it to the Host MVC project sitemap in memory.

I also tried to use an MvcSiteMapNode attribute but haven't really been able to get this working yet.

Firstly, which is the easiest method to use, Attribute or SiteMap?

Secondly can anyone point me to guidance on how to do either of these please?

I have a preference for using the sitemap file because it should avoid dependencies on MvcSiteMapProvider in the MEF module.


Solution

  • You could embed XML into your modules and then somehow export them through MEF, I am pretty sure that is an option.

    To load the XML from memory, you will need to use an external DI container and implement the IXmlSource interface yourself.

    public class MyXmlSource : IXmlSource
    {
        public XDocument GetXml()
        {
            // Load the XML from wherever...
        }
    }
    

    Then the DI configuration would just need to swap out the default XmlSource.

    var excludeTypes = new Type[] { 
        typeof(IXmlSource)
    };
    
    // More DI config not shown...
    
    this.For<IXmlSource>()
        .Use<MyXmlSource>();
    

    Of course, if you have multiple files, you would need multiple registrations of IXmlSource and XmlSiteMapNodeProvider. The XmlSiteMapNodeProvider only has the ability to merge the nodes below the root node, so you can't just place them anywhere in the SiteMap.

    XML and Attributes are the least flexible options for configuring MvcSiteMapProvider.

    Another Approach

    The most flexible option is to use ISiteMapNodeProvider implementations to load your configuration data which also requires an external DI container. The second-best option is to use dynamic node provider implementations, but the limitation there is that they cannot contain the root node and require either an XML node or .NET attribute to host the provider on.

    However, if you don't want any 3rd party dependencies, you will need a custom abstraction (DTO) that is defined in your own base library that is exported via MEF, and then used by either ISiteMapNodeProvider or IDynamicNodeProvider to load the data from the abstraction.

    public class SiteMapNodeDto
    {
        public string Key { get; set; }
        public string ParentKey { get; set; }
        public string Title { get; set; }
        public IDictionary<string, object> RouteValues { get; set; }
    
        // Additional properties...
    }
    

    And then have an interface of some kind that your module can implement to provide nodes.

    public interface IModuleSiteMapNodeProvider
    {
        IEnumerable<SiteMapNodeDto> GetNodes();
    }
    

    My brief experience with MEF was several years ago, so I don't recall how you export a type and import it somewhere else, so you will need to work that out on your own.

    Then in your main application, you could create a method for converting from your DTO to the type that is expected by MvcSiteMapProvider (ISiteMapNodeToParentRelation or DynamicNode).

    public static SiteMapNodeProviderExtensions
    {
        public static ISiteMapToParentRelation CreateNode(this ISiteMapNodeHelper helper, SiteMapNodeDto dto, string sourceName)
        {
             string key = helper.CreateNodeKey(
                dto.ParentKey,
                dto.Key,
                dto.Url,
                dto.Title,
                dto.Area,
                dto.Controller, 
                dto.Action, 
                dto.HttpMethod,
                dto.Clickable);
    
            var nodeParentMap = helper.CreateNode(key, attribute.ParentKey, sourceName);
            var node = nodeParentMap.Node;
    
            node.Title = title;
    
            // Populate remaining properties...
    
            return nodeParentMap;
        }
    }
    

    And then in the ISiteMapNodeProvider you would simply need to call this method for each node.

    public IEnumerable<ISiteMapNodeToParentRelation> GetSiteMapNodes(ISiteMapNodeHelper helper)
    {
        string sourceName = typeof(SiteMapNodeDto).Name;
        IEnumerable<SiteMapNodeDto> dtos = someExternalSource.GetNodes();
    
        foreach (var dto in dtos)
        {
            yield return helper.CreateNode(dto, sourceName);
        }
    }
    

    If you want to make providing SiteMap nodes a first class feature for your module developers (and make it really easy to understand the hierarchy of nodes like you can with XML), you could create a Fluent API so you can express the nodes in code. Have a look at this pull request for one such approach.