ASP.NET MVC SiteMap

Note: This code does not support caching or security trimming. It is simply a way to build something that compares to the .NET TreeView control using the MVC pattern.

Recently one of my clients had the "perfect storm" of requirements, restrictions, deadlines and policies that lead to the need to develop a MVC TreeView(ish) control.

 

Requirements:

  1. Database storage – Site administrations needed a simple interface into editing and I was not about to write something to update XML files.
  2. NOT use the SQL Service Broker for easy SQL caching support – Security concerns….I assume…I was never told why NOT but just NO.
  3. Support the MVC pattern – new projects where being developed using ASP.NET MVC
  4. Easy to implement and maintain

Design:

  1. With MS shipping jQuery it would be nice to use a jQuery plugin to render the tree
  2. Leverage existing technology – Exising project are using the SQL SiteMap Provider, hence, accompanying tables and stored procedures.
  3. Extend the ASP.NET MVC HtmlHelpers class

Note: I assume your application is currently setup to use the SQL SiteMap Provider…or at least the Table for the provider exists.

Step 1: Create a Stored Procedure that uses a CTE

Most of the heavy lifting happens here. As you can see, when viewing the full-size image, the results are ordered by "thePath". That makes parsing the results into a ordered list very easy.

siteMap_CTE

 

   1:  WITH siteMapCTE(SYSID, ID, Parent,title,description,url,roles, level, thePath) AS (
   2:      SELECT SYSID, ID, Parent,title,description,url,roles, 1 as level, CONVERT(VARBINARY(MAX), ID) AS thePath
   3:      FROM aspnet_SiteMap WHERE Parent is NULL
   4:      UNION ALL
   5:      SELECT e.SYSID, e.ID, e.Parent,e.title,e.description,e.url,e.roles, c.level + 1, c.thePath + CONVERT(VARBINARY(MAX), e.ID) AS thePath
   6:      FROM aspnet_SiteMap e
   7:      INNER JOIN siteMapCTE c
   8:      ON e.Parent = c.ID
   9:  )
  10:  SELECT * FROM siteMapCTE ORDER BY thePath, title

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Step 2: Create new HtmlHelper method

First off..Stephen Walther’s first ASP.NET MVC article was on writing html helpers using Extension Methods. I have his sample in a current project so it was a great place to drop in the new method.

<Extension()> _
        Public Function ListBasedSiteMap(ByVal HtmlHelper As HtmlHelper, ByVal items As Object) As String
            If items Is Nothing Then
                Throw New ArgumentNullException("items")
            End If
            Dim builder As New StringBuilder()
            Dim mPreviousLevel As Integer
            Dim mUrl As String = String.Empty
 
            builder.Append("<div class=""demo"" id=""testMap""><ul>")
            For Each row As DataRow In items.Rows
                If row.Item("url").ToString.Contains("~/") = True Then
                    mUrl = "/" & System.Configuration.ConfigurationManager.AppSettings("application.name") & "" & row.Item("url").ToString.Split("~/")(1)
                Else
                    mUrl = row.Item("url").ToString
                End If
 
                If row.Item("level") = 1 Then
                    builder.AppendLine("<li id=""" & row.Item("id") & """ class=""open""><a href=""" & mUrl & """>" & row.Item("title") & "</a>")
                ElseIf mPreviousLevel < row.Item("level") Then
                    'child 
                    builder.AppendLine("<ul>")
                    builder.AppendLine("<li id=""" & row.Item("id") & """><a href=""" & mUrl & """>" & row.Item("title") & "</a>")
                ElseIf mPreviousLevel = row.Item("level") Then
                    builder.AppendLine("<li id=""" & row.Item("id") & """><a href=""" & mUrl & """>" & row.Item("title") & "</a>")
                ElseIf mPreviousLevel > row.Item("level") Then
                    'close level
                    builder.AppendLine("</li>")
                    builder.AppendLine("</ul>")
                    builder.AppendLine("<li id=""" & row.Item("id") & """><a href=""" & mUrl & """>" & row.Item("title") & "</a>")
                Else
                    'parent ?
                End If
                added.Add(row.Item("id"))
                mPreviousLevel = row.Item("level")
            Next
            builder.AppendLine("</ul></div>")
            Return builder.ToString()
        End Function

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Oh…I pass the results in as a data table….you can change that to be whatever your passing….

Step 3: Get results to your view

How ever you like, get the data to the view. Again, I am using my own DAL and pass the results in as a Data Table

Step 4: Put the unordered list on the view

   1:  <%=Html.ListBasedSiteMap(ViewData("siteMap"))%>

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Don’t forget to import the HtmlHelpers namespace…in your paper (I presume its a master page or the web.config)

   1:  <%@ Import Namespace="yourNameSpace.HtmlHelpers" %>
 

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Break

Ok, at this point you should have an non-styled unordered list of SiteMap nodes that looks something like. All we have to do now is style, wire and go. As a matter of fact, the sitemap should work too!…it just looks like crap.

sitemap_unstyled

 

Step E: jsTree!

I’m not going to walk you through the process of wiring up your site to use jsTree but that is the next step. download, and put the files into your project, add the script and link tags for the javascript and css…modify the CSS as needed…

So maybe I did walk you through it…anyway.

Step VI: onload event….um…

I’m using dojo for this project with the exception of the TreeView so I leveraged dojo’s OOP and onload….this should work but its not tested.

   1:  var first = null;
   2:  $(function() { 
   3:  tree1 = new tree_component();
   4:              tree1.init(jQuery("#testMap"), {
   5:                  cookies: {
   6:                      prefix: "sitemap",
   7:                      opts: { path: '/' }
   8:                  },
   9:                  callback: {
  10:                      onchange: function(NODE) {
  11:                          if (!first) {
  12:                              first = true;
  13:                          } else { document.location.href = jQuery(NODE).children("a:eq(0)").attr("href"); }
  14:                      }
  15:                  }
  16:              });
  17:   });

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Finish

Here is what I ended up with…

sitemap_styled

Again, I know…no caching…no security trimming but it works and you can allow management of the nodes from a web interface (of course you have to code that) … If anyone adds trimming please let me know…I don’t have a need now but I’d like to keep this all together in one place.

  • Gugulethu

    Hi,

    I’m tasked with implement something similar to this.

    Do you mind sharing as to how you populate the table and also an incite into the Stored Procedure?

    Thanks

    • http://eric.polerecky.com Eric Polerecky

      In my case we allow the administrators of the web application to manage the sitemap thru views in the admin controller.

      the code for the SP, while not well formatted in this theme, is right below the image of the table. It’s a common table expression (CTE) that uses hashes to track the depth of the link items.

  • Gugulethu

    Great.

    Do you have a C# version for the function ListBasedSiteMap?

    • http://eric.polerecky.com Eric Polerecky

      Sorry nope. you could run it through an online vb -> c# converter.