Search code examples
vb.netasp.net-mvc-3mvcsitemapprovider

Dynamic Url - MVCSiteMapProvider Not Working


I have the following code in mvc.sitemap file (using mvcsitemapprovider mvc3):

 <?xml version="1.0" encoding="utf-8" ?>
   <mvcSiteMap xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0"
        xsi:schemaLocation="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0 MvcSiteMapSchema.xsd">

  <mvcSiteMapNode title="Home" controller="Home" action="Index" key="Home">
<mvcSiteMapNode title="Internal Mail Delivery Days - Display" controller="InternalMailCalendar" action="Index" key="InternalMailCalendar"  />
<mvcSiteMapNode title="Internal Mail Delivery Days - Create" controller="InternalMailCalendar" action="Create" />
 </mvcSiteMapNode>

 </mvcSiteMap>

also i am calling this function to add the breadcrumb:

<MvcSiteMapNode(Title:="Edit", ParentKey:="InternalMailCalendar")> _
    Function Edit(ByVal id As Integer) As ActionResult
        If id > 0 Then
            Dim obj = Mapper.Map(Of InternalMailCalendarViewModel)(_repositoryForIntMailCalendar.FindSingleByCondition(Function(x) x.ID = id))
            If obj IsNot Nothing Then
                Return View(obj)
            End If
        End If
        Throw New HttpException(404, "Invalid Calendar ID")
    End Function

but nothing gets displayed in the breadcrumb only for this dynamic url. any ideas please?


Solution

  • To use custom parameters (such as "id"), you must configure MvcSiteMapProvider to take them into consideration. There are 2 options - you can either configure 1 node per controller action (as you have done) or you can configure 1 node per route value combination (in your case 1 node per id).

    There is a detailed article on this subject with downloadable demos called How to Make MvcSiteMapProvider Remember a User's Position. These 2 StackOverflow answers might also be helpful.

    A CRUD Example

    But here is the reader's digest version. For CRUD operations that are only available to logged-in users (that no search engine will ever see), the best way is to use preservedRouteParameters to force a single set of nodes to match every record.

    Structuring the Nodes

    First things first. Let's do a complete example with Index, Details, Create, Edit, and Delete just to outline where they would go should you use them. You can of course limit your implementation to the features you need and you can change this to using MvcSiteMapNodeAttribute if you like. But I am showing them here in XML so they are easier to picture in your mind. The first thing is to arrange the nodes appropriately so your breadcrumb trail will correctly show the current position.

    <mvcSiteMapNode title="Home" controller="Home" action="Index" key="Home">
        <mvcSiteMapNode title="Internal Mail Delivery Days" controller="InternalMailCalendar" action="Index" key="InternalMailCalendar">
            <mvcSiteMapNode title="Create" controller="InternalMailCalendar" action="Create" visibility="SiteMapPathHelper,!*"/>
            <mvcSiteMapNode title="Details" controller="InternalMailCalendar" action="Details" visibility="SiteMapPathHelper,!*" preservedRouteParameters="id">
                <mvcSiteMapNode title="Edit" controller="InternalMailCalendar" action="Edit" visibility="SiteMapPathHelper,!*" preservedRouteParameters="id"/>
                <mvcSiteMapNode title="Delete" controller="InternalMailCalendar" action="Delete" visibility="SiteMapPathHelper,!*" preservedRouteParameters="id"/>
            </mvcSiteMapNode>
        </mvcSiteMapNode>
    </mvcSiteMapNode>
    

    So navigating your nodes would look something like this in the breadcrumb trail.

    • Home > Internal Mail Delivery Days (A list showing all records)
    • Home > Internal Mail Delivery Days > Create (The page to create a new record)
    • Home > Internal Mail Delivery Days > Record 1 (The page to view a record - read only)
    • Home > Internal Mail Delivery Days > Record 1 > Edit (The page to edit the record)
    • Home > Internal Mail Delivery Days > Record 1 > Delete (The page to delete the record from)

    That is what we are going for, but we are not there yet. With preservedRouteParameters="id" on our nodes, you can now see that they will match the current request when navigating the pages. Do note that you must provide navigation from record to record in your site's content when using preservedRouteParameters because the menu will not display the records. Usually this is done by displaying a grid of all of the records with links to the appropriate commands (Details, Edit, Delete, etc).

    There are 2 additional things to take care of - setting the title of the node dynamically and hiding the nodes that pertain to specific records from the menu. Why? Because when using preservedRouteParameters, it is not possible to show a list of all of the records in MvcSiteMapProvider - you only get the current record. Therefore, the only thing we get is a breadcrumb trail. It is possible to list everything, but only if you add a node for each record to MvcSiteMapProvider and set the "id" for each record explicitly, but that is not what we are doing in this example.

    Visibility

    Next, lets focus on the visibility. As you can see, I have already set up the filters in the visibility attribute as visibility="SiteMapPathHelper,!*". This will tell the FilteredSiteMapNodeVisibility provider to only show the node on the SiteMapPath HTML helper, but hide it everywhere else.

    But there are 2 more things we need to get visibility working. We need to set the FilteredSiteMapNodeVisibilityProvider as the default and we need to turn off the Visibility Affects Descendants setting. If using internal DI, these can both be done in the web.config appSettings as follows.

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

    If using external DI, you can configure these settings on the constructors of the SiteMapNodeVisibilityProviderStrategy and SiteMapBuilderSet, respectively.

    // Visibility Providers
    this.For<ISiteMapNodeVisibilityProviderStrategy>().Use<SiteMapNodeVisibilityProviderStrategy>()
        .Ctor<string>("defaultProviderName").Is("MvcSiteMapProvider.FilteredSiteMapNodeVisibilityProvider, MvcSiteMapProvider");
    
    // Configure the builder sets
    this.For<ISiteMapBuilderSetStrategy>().Use<SiteMapBuilderSetStrategy>()
        .EnumerableOf<ISiteMapBuilderSet>().Contains(x =>
        {
            x.Type<SiteMapBuilderSet>()
                .Ctor<string>("instanceName").Is("default")
                .Ctor<bool>("securityTrimmingEnabled").Is(securityTrimmingEnabled)
                .Ctor<bool>("enableLocalization").Is(enableLocalization)
                .Ctor<bool>("visibilityAffectsDescendants").Is(false)
                .Ctor<bool>("useTitleIfDescriptionNotProvided").Is(useTitleIfDescriptionNotProvided)
                .Ctor<ISiteMapBuilder>().Is(builder)
                .Ctor<ICacheDetails>().Is(cacheDetails);
        });
    

    Title

    Last but not least, we need to fix the title of the node so it changes each time we switch records. This will ensure we see the name or title of the record we are on in the breadcrumb instead of what is set in the title attribute of the XML.

    The easiest way to accomplish this is with the SiteMapTitleAttribute. This attribute can just be added onto your action method.

    <SiteMapTitle("Name", Target = AttributeTarget.ParentNode)> _
    Function Edit(ByVal id As Integer) As ActionResult
        If id > 0 Then
            Dim obj = Mapper.Map(Of InternalMailCalendarViewModel)(_repositoryForIntMailCalendar.FindSingleByCondition(Function(x) x.ID = id))
            If obj IsNot Nothing Then
                Return View(obj)
            End If
        End If
        Throw New HttpException(404, "Invalid Calendar ID")
    End Function
    

    This example assumes your model (the variable obj) has a property named "Name". The property can be named whatever you want, it just needs to match the configuration of the SiteMapTitle attribute. The second argument indicates that we want the parent node of the current node to change title. Remember, in our example above we want the Edit breadcrumb to look like this:

    Home > Internal Mail Delivery Days > Record 1 > Edit

    In other words, we don't want the title of the Edit node to change, we want the one that says "Record 1" to change to the name of the current record. Just be sure to leave the Target argument off of the Details node because on that node we need the current title to change dynamically, not the parent node.

    Important Note

    Using preservedRouteParameters has a side-effect you need to be aware of - it is copying the "id" from the current request into all of the visible nodes that it is configured on. It is fine to reuse the route value "id" so long as it always means the same thing to all of the nodes that are visible. If you have another level of nodes you want to display at the same time that require a parameter, it must be named something different than the original parameter (in this case "id") so the values can be kept separate.

    Also, you must ensure (manually) that all of the required values to build all of the visible URLs are available in the URL of each request, even if it means sticking extra information in the URL that your application doesn't require.