Search code examples
mvcsitemapprovider

Remembering Ancestors in MvcSiteMapProvider


I have a general need to maintain a reference to my ancestors as I traverse down the sitemap.

Mvc.sitemap

<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" >
    <mvcSiteMapNode title="Products" url="~/Home/Products" roles="*">
      <mvcSiteMapNode title="Harvest MAX" url="~/Home/Products/HarvestMAX" >
        <mvcSiteMapNode title="Policies" url="~/Home/Products/HarvestMAX/Policy/List" productType="HarvestMax" type="P" typeFullName="AACOBusinessModel.AACO.HarvestMax.Policy" roles="*">
          <mvcSiteMapNode title="Policy" controller="Object" action="Details" typeName="Policy" typeFullName="AACOBusinessModel.AACO.HarvestMax.Policy" preservedRouteParameters="id" roles="*">
            <mvcSiteMapNode title="Counties" controller="Object" action="List" collection="Counties" roles="*">
              <mvcSiteMapNode title="County" controller="Object" action="Details" typeName="County" typeFullName="*" preservedRouteParameters="id" roles="*">
                <mvcSiteMapNode title="Land Units" controller="Object" action="List" collection="LandUnits" roles="*">
                  <mvcSiteMapNode title="Land Unit" controller="Object" action="Details" typeName="LandUnit" typeFullName="AACOBusinessModel.AACO.LandUnit" preservedRouteParameters="id" roles="*">
                  </mvcSiteMapNode>
                </mvcSiteMapNode>
              </mvcSiteMapNode>    
            </mvcSiteMapNode>
          </mvcSiteMapNode>
        </mvcSiteMapNode>
      </mvcSiteMapNode>
    </mvcSiteMapNode>
  </mvcSiteMapNode>
</mvcSiteMap>

Controller

[SiteMapTitle("Label")]
public ActionResult Details(string typeFullName, decimal id)
{
  return View(AACOBusinessModel.AACO.VersionedObject.GetObject( typeFullName?.ToType() ?? Startup.CurrentType,
                                                                ApplicationSignInManager.UserCredentials.SessionId,
                                                                id));
}

There are many reasons I want this, but here are some specific examples.

Example 1: Vanishing ID's

Let's say the url that got me to the Policy node is http://localhost:36695/AACOAgentPortal/details/Policy/814861364767412.

Once I navigate down past that to the County node, my breadcrumbs looks like this: enter image description here

However if I hover over the Policy breadcrumb, the url given is http://localhost:36695/AACOAgentPortal/Object/Details?typeName=Policy&typeFullName=AACOBusinessModel.AACO.HarvestMax.Policy. As you can see, the id is gone.

Example 2: Vanishing Titles

As you can see in my controller, I'm telling mvc sitemap that I want to use the Label property to display the node title. It does that when it's the leaf node:

enter image description here

But once I go past that, it disappears: enter image description here

Both of these issues may have a common cause. There are other reasons why I may want a reference to an ancestor along the breadcrumb trail, but these are two concrete ones to exemplify the issue.


Solution

  • I solve this by keeping my objects in a hierarchy in session, and each object has the same key that it's node has so that it can find the node upon processing each request.

    MenuItems.cs

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using AtlasKernelBusinessModel;
    using MvcSiteMapProvider;
    using CommonBusinessModel.Commands;
    using CommonBusinessModel.Extensions;
    using CommonBusinessModel.Security;
    
    namespace AtlasMvcWebsite.Code
    {
      [Serializable]
      public class MenuItems : Dictionary<string, MenuItem>
      {
        #region Properties
    
        public IEnumerable<Command> AvailableCommands
        {
          get
          {
            return CurrentItem?.Commandable?.AvailableCommands() ?? new List<Command>();
          }
        }
    
        /// <summary>
        /// Each User has his own copy because it has to track his travel through the hierarchy
        /// </summary>
        public static MenuItems Current
        {
          get
          {
            return (MenuItems)(HttpContext.Current.Session["MenuItems"] =
                                HttpContext.Current.Session["MenuItems"] ??
                                new MenuItems());
          }
        }
    
        private MenuItem currentItem;
        public MenuItem CurrentItem
        {
          get
          {
            return currentItem =
                    CurrentNode == null ?
                    null :
                    this[CurrentNode.Key] =
                      ContainsKey(CurrentNode.Key) ?
                      this[CurrentNode.Key] :
                      new MenuItem(CurrentNode,
                                   CurrentNode.ParentNode != null ? this[CurrentNode.ParentNode.Key] : null);
          }
        }
    
        public ISiteMapNode CurrentNode { get; private set; }
    
        #endregion
    
        #region Methods
        private void Build()
        {
          Build(SiteMaps.Current.RootNode);
        }
    
        private void Build(ISiteMapNode node)
        {
          foreach (var childNode in node.ChildNodes)
          {
            foreach (var att in node.Attributes.Where(a => !childNode.Attributes.Any(na => na.Key == a.Key)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value))
            {
              switch (att.Key)
              {
                case "productType":
                case "typeFullName":
                case "typeName":
                  childNode.Attributes[att.Key] = att.Value;
                  childNode.RouteValues[att.Key] = att.Value;
                  break;
              }
            }
            Build(childNode);
          }
        }
    
        /// <summary>
        /// We finally have an object from the details controller and we want to set it to the current menu item
        /// </summary>
        /// <param name="versionedObject"></param>
        /// <returns></returns>
        public AtlasKernelBusinessModel.VersionedObject Set(AtlasKernelBusinessModel.VersionedObject versionedObject)
        {
          ((ICommandable)versionedObject).UserAccess = User.Credentials.BusinessObjectAccessFor(versionedObject.ObjectType());
          if (CurrentItem != null)
            this[CurrentItem.Node.Key].Object = versionedObject;
          //else
          // for commands
          //SiteMapNodeObjects[SiteMapNodeObjects.Last().Key] = versionedObject;
          return versionedObject;
        }
    
        public void Sync()
        {
          //Build();
          CurrentNode = SiteMaps.Current.CurrentNode;//cache value of current node
          Values.ToList().ForEach(m => m.Sync());
        }
        #endregion
    
      }
    
    
    }
    

    MenuItem.cs

    using AtlasKernelBusinessModel;
    using MvcSiteMapProvider;
    using System;
    using CommonBusinessModel.Commands;
    
    
    namespace AtlasMvcWebsite.Code
    {
      [Serializable]
      public class MenuItem
      {
        #region Constructors
        public MenuItem(ISiteMapNode node, MenuItem parent)
        {
          Node = node;
          Parent = parent;
        }
        #endregion
    
        public ISiteMapNode Node;
    
        public readonly MenuItem Parent;
    
        private ICommandable commandable;
        public ICommandable Commandable
        {
          get
          {
            return commandable;
          }
        }
    
        private VersionedObject @object;
        public VersionedObject Object
        {
          get
          {
            return @object;
          }
    
          set
          {
            @object = value;
            Type = @object.GetType();
            commandable = (ICommandable)@object;
    
            Sync();
          }
        }
    
        public Type Type;
    
        public void Sync()
        {
          // sync the node to the object
          if (Object == null) return;
          Node = SiteMaps.Current.FindSiteMapNodeFromKey(Node.Key);
          Node.Title = Type.GetProperty("Label").GetValue(Object).ToString();
          Node.RouteValues["id"] = Object.Id;
        }
    
        public override string ToString()
        {
          return $"{Parent?.Node?.Title}.{Node.Title}";
        }
      }
    }
    

    Usage Example

          var menuItem = MenuItems.Current.CurrentItem; // ensure current item exists
          if (menuItem != null)
          {
          <!-- CHILD ITEM MENU -->
            @Html.MvcSiteMap().Menu(menuItem.Node, true, false, 1)
    
          <!-- COMMAND BUTTONS -->
            if (!viewModel.ReadOnly)
            {
              @Html.DisplayFor(m => menuItem.Commandable.BusinessOperations.Commands)
            }