SharePoint v3: Recreating the Quick Launch Menu

UPDATE: 2007 May 2nd - I've updated this article to include a lot of bug fixes that I have encountered along the way. I will maintain some type of versioning at the end of this post so readers can follow code changes that have occurred over time.

Microsoft's SharePoint Services 2007 (v3) provides so many additional features and bug fixes to their previous version that I was extremely anxious to upgrade our company's intranet to the newer code. However, as with any upgrade, you eventually run into stumbling blocks that will make your life miserable as a programmer/designer.

I recently came across one such issue with SharePoint Services 2007 - when you create a new page through the front-end, the Quick Launch menu is lost. Huh?

The Quick Launch menu is the standard left side navigation module that holds link information. In addition to the site specific links that you add to the Quick Launch, a static "View All Site Content" link is docked at the top of this list (which allows users to see a listing of all the Lists, Document Libraries, Picture Libraries, Surveys, Discussions and more. The items displayed in the quick launch are managed through the "Site Settings" option underneath the "Site Actions" button.

However, there is one major problem with the default SharePoint implementaion - when a user creates a new page (whether a basic or web part page), the Quick Launch menu does not attach itself to that new page. For some unknown reason, a major navigational module is dropped from secondary pages created through the SharePoint front-end. I would say that this decision is dubious at best. Since our company wanted to dock our Quick Launch onto every new page created on every site we maintain, we needed to programmatically come up with a solution. I decided to solve this problem by recreating the Quick Launch as a custom web part.

How do you reconstruct the Quick Launch from scratch? First, as with most advanced custom SharePoint tasks, you need to understand how the database is structured. Many SharePoint admins will tell you to "leave the SharePoint database alone", but simple reads to the database will not cause a problem (in fact, as long as you understand the database structure, (although not supported) I have created procedures that update the SharePoint database directly by pulling information from other data sources). The Quick Launch information is stored in two tables - Webs and NavNodes. The Webs table stores information concerning the different sites that have been created. It's important to first pull the 'id' for the for the site the user is currently browsing.

select id, fullurl from webs where siteid = 'root_site_id' and fullurl = 'site_user_is_on'

How do you tell what site a user is on? One such method would be to get the URL through Page.Request.Url.AbsoluteUri, then get the array(3) value and use that value for the 'fullurl' where clause(and if your dataset = 0 rows, you are on the default site, otherwise you are on a subsite). One problem with this method is that you cannot have a Document Library that shares the same name as a subsite (however, this is the option we implemented for now until I develop something better). Next, you take that 'id' value and query the NavNodes table for the record containing the root Quick Launch information for that site. You'll grab the 'eid' value for that record and then use that value to grab all the records where that eid value is found in the 'eidparent' column (for that root site and subsite).

select eid, numchildren, rankchild, url, name from navnodes where eidparent in (select eid from navnodes where webid = 'subsite_id' and siteid = 'root_site_id' and name = 'Quick Launch') and webid = 'subsite_id' and siteid = 'root_site_id' order by rankchild

This will give you all the "Link Headings" for that site's Quick Launch. Read these values into an array. First, render these headings using the ms-navheader class. Before moving onto the next "Link Heading", we must check to see if there are any "children" or "Link Items" for this heading. That would be done by running the following query:

select eid, numchildren, rankchild, url, name from navnodes where eidparent in (select eid from navnodes where eid = 'link_heading_eid' and webid = 'subsite_id' and siteid = 'root_site_id') and webid = 'subsite_id' and siteid = 'root_site_id' order by rankchild

If your dataset count is > 0, then read these "Link Items" into an array and render them all out while applying the ms-navitem class. Once all the "Link Items" are printed out, move to the next "Link Heading" until you have read and rendered all headings. So, there's the logic behind recreating the Quick Launch. Here's a snippet of some of the code I used to render my Quick Launch recreation (GUIDs changed to protect the innocent):

   41         Try

   42             strURL = Page.Request.Url.AbsoluteUri

   43             arrURL = Split(strURL, "/", -1, CompareMethod.Text)

   44 

   45 #If PROD = "Intranet" Then

   46             strRootGUID = "{035AA977-25EF-4B20-AFF4-3BBF028CCE6C}"

   47             strRootURL = "intranet.domain.com"

   48             SQLConnection.ConnectionString = db.GetDBShare3Intranet

   49 #ElseIf PROD = "Extranet" Then

   50             strRootGUID = "{6B052A1E-F7FE-4A42-849F-A55079A49E3A}"

   51             strRootURL = "portal.domain.com"

   52             SQLConnection.ConnectionString = db.GetDBShare3Extranet

   53 #ElseIf PROD = "Development" Then

   54             strRootGUID = ""

   55             strRootURL = "development:16542"

   56             SQLConnection.ConnectionString = ""

   57 #End If

   58 

   59             SQLConnection.Open()

   60             SQLQuery = "select id, fullurl from webs where siteid = '" & strRootGUID & "' and fullurl = '" & arrURL(3) & "'"

   61             Dim DataAdapter As SqlClient.SqlDataAdapter = New SqlClient.SqlDataAdapter(SQLQuery, SQLConnection)

   62             DataAdapter.Fill(DataSet)

   63 

   64             If DataSet.Tables(0).Rows.Count > 0 Then

   65                 ' Match made - we are on a subsite

   66                 GUID = DataSet.Tables(0).Rows(i).Item(0)

   67                 strGUID = GUID.ToString

   68                 arrURL(3) = arrURL(3)

   69             Else

   70                 ' No match made - we are on the Root Site

   71 

   72 #If PROD = "Intranet" Then

   73                 strGUID = "{01872F73-68B8-44EC-AE6E-E0FCCF231AB5}"

   74 #ElseIf PROD = "Extranet" Then

   75                 strGUID = "{E3DB7959-A1A7-4619-AC7D-F0B4E67D2DC6}"

   76 #ElseIf PROD = "Development" Then

   77                 strGUID = ""

   78 #End If

   79 

   80                 arrURL(3) = ""

   81             End If

   82 

   83             DataSet = New DataSet

   84             SQLQuery = "select eid, numchildren, rankchild, url, name, siteid from navnodes where eidparent in (select eid from navnodes where webid = '" & strGUID & "' and siteid = '" & strRootGUID & "' and name = 'Quick Launch') and webid = '" & strGUID & "' and siteid = '" & strRootGUID & "' order by rankchild"

   85             DataAdapter = New SqlClient.SqlDataAdapter(SQLQuery, SQLConnection)

   86             DataAdapter.Fill(DataSet)

   87 

   88             If DataSet.Tables(0).Rows.Count > 0 Then

   89                 output.RenderBeginTag("div class=ms-quicklaunchouter")

   90                 output.RenderBeginTag("div class=ms-quicklaunch style=" & Chr(&H22) & "width:100%; border: none;" & Chr(&H22))

   91                 output.RenderBeginTag("table cellpadding=0 cellspacing=0 class=ms-quicklaunch width=100%")

   92                 output.RenderBeginTag("tr")

   93                 output.RenderBeginTag("td class=ms-quicklaunchheader style=" & Chr(&H22) & "border: none;" & Chr(&H22))

   94                 output.Write("Navigation")

   95                 output.RenderEndTag()

   96                 output.RenderEndTag()

   97 

   98                 For i = 0 To DataSet.Tables(0).Rows.Count - 1

   99                     output.RenderBeginTag("tr")

  100                     output.RenderBeginTag("td class=ql-navheader")

  101                     ListGUID = DataSet.Tables(0).Rows(i).Item(5)

  102                     strListGUID = ListGUID.ToString

  103 

  104                     ' Start adding exception handling ...

  105                     ' 1) Links that start with http:// shouldn't append to the root.

  106 

  107                     If Left(DataSet.Tables(0).Rows(i).Item(3), 7) = "http://" Then

  108                         output.RenderBeginTag("a class=ms-navheader href=" & System.Web.HttpUtility.UrlPathEncode(DataSet.Tables(0).Rows(i).Item(3)))

  109                     Else

  110                         ' Write the "nomral" quick launch URL scheme

  111                         output.RenderBeginTag("a class=ms-navheader href=http://" & strRootURL & System.Web.HttpUtility.UrlPathEncode(DataSet.Tables(0).Rows(i).Item(3)))

  112                     End If

  113                     output.RenderBeginTag("b")

  114                     output.Write(DataSet.Tables(0).Rows(i).Item(4))

  115                     output.RenderEndTag()

  116                     output.RenderEndTag()

  117                     output.RenderEndTag()

  118                     output.RenderEndTag()

  119 

  120                     If DataSet.Tables(0).Rows(i).Item(1) > 0 Then

  121                         ' There are child items for this menu item

  122 

  123                         DataSet2 = New DataSet

  124                         SQLQuery = "select eid, numchildren, rankchild, url, name, DocID from navnodes where eidparent in (select eid from navnodes where eid = '" & DataSet.Tables(0).Rows(i).Item(0) & "' and webid = '" & strGUID & "' and siteid = '{035AA977-25EF-4B20-AFF4-3BBF028CCE6C}') and webid = '" & strGUID & "' and siteid = '{035AA977-25EF-4B20-AFF4-3BBF028CCE6C}' order by rankchild"

  125                         DataAdapter = New SqlClient.SqlDataAdapter(SQLQuery, SQLConnection)

  126                         DataAdapter.Fill(DataSet2)

  127 

  128                         For x = 0 To DataSet2.Tables(0).Rows.Count - 1

  129                             output.RenderBeginTag("tr")

  130                             output.RenderBeginTag("td")

  131                             output.RenderBeginTag("table class=ms-navitem width=100% border=0 cellpadding=0 cellspacing=0")

  132                             output.RenderBeginTag("tr")

  133                             output.RenderBeginTag("td width=100% style=" & Chr(&H22) & "border: none;" & Chr(&H22))

  134 

  135                             If IsDBNull(DataSet2.Tables(0).Rows(x).Item(3)) = False Then

  136                                 ' URL field in NavNodes has a value

  137                                 output.RenderBeginTag("a class=ms-navitem href=http://" & strRootURL & System.Web.HttpUtility.UrlPathEncode(DataSet2.Tables(0).Rows(x).Item(3)))

  138                             Else

  139                                 ' URL field is Null, now, have to use DocID value to obtain path

  140                                 DataSet3 = New DataSet

  141                                 DocGUID = DataSet2.Tables(0).Rows(x).Item(5)

  142                                 strDocGUID = DocGUID.ToString

  143                                 SQLQuery = "select DirName, LeafName from AllDocs where id = '" & strDocGUID & "'"

  144                                 DataAdapter = New SqlClient.SqlDataAdapter(SQLQuery, SQLConnection)

  145                                 DataAdapter.Fill(DataSet3)

  146                                 output.RenderBeginTag("a class=ms-navitem href=http://" & strRootURL & "/" & System.Web.HttpUtility.UrlPathEncode(DataSet3.Tables(0).Rows(0).Item(0)) & "/" & System.Web.HttpUtility.UrlPathEncode(DataSet3.Tables(0).Rows(0).Item(1)))

  147                             End If

  148 

  149                             output.Write(DataSet2.Tables(0).Rows(x).Item(4))

  150                             output.RenderEndTag()

  151                             output.RenderEndTag()

  152                             output.RenderEndTag()

  153                             output.RenderEndTag()

  154                             output.RenderEndTag()

  155                             output.RenderEndTag()

  156                         Next

  157                     End If

  158                 Next

  159 

  160                 output.RenderEndTag()

  161                 output.RenderEndTag()

  162                 output.RenderEndTag()

  163             End If

  164 

  165             DataSet = Nothing

  166             SQLConnection.Close()

  167         Catch ex As Exception

  168             strError = ex.Message.ToString

  169             output.Write(strError)

  170         Finally

  171 

  172         End Try

If you see any improvements that I can make, let me know by leaving me a comment. Hope this helps someone.

Versioning

  1. Version 1.1 - (2007 May 2) - Bug fixes which include:
    • Applying Quick Launch logic that is used in the SharePoint WSS 40 Free Application templates.
    • Wrapping data retreived from the database and other sources in System.Web.HttpUtility.UrlPathEncode() to prepare for URL formation and usage.
    • Colorized the comments (I'm looking for a Drupal module to automatically do this for me)
  2. Version 1.0 - (2007 Feb 7) - Original Article on recreating the SharePoint v3 Quick Launch through a Web Part

Comments

C# version with styles closer to the real

using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Navigation;

namespace InfoViewer
{
public class SQLQuickLaunch : System.Web.UI.WebControls.WebParts.WebPart
{
protected override void RenderContents(System.Web.UI.HtmlTextWriter output)
{

base.RenderContents(output);
try
{
string strURL = Page.Request.Url.AbsoluteUri;
string[] arrURL = strURL.Split("/".ToCharArray());
SqlConnection SQLConnection = new SqlConnection();
DataSet DataSet1 = new DataSet();
DataSet DataSet2 = new DataSet();
DataSet DataSet3 = new DataSet();

string strGUID = "";
string strRootGUID = "<";
string strListGUID = "";
string strRootURL = "<";
SQLConnection.ConnectionString = "Data Source=RMSSERVER\\OFFICESERVERS;Initial Catalog=WSS_Content;Integrated Security=SSPI;";
SQLConnection.Open();
string SQLQuery = "select id, fullurl from webs where siteid = '" + strRootGUID + "' and fullurl = '" + arrURL[3] + "'";
SqlDataAdapter DataAdapter = new SqlDataAdapter(SQLQuery, SQLConnection);
DataAdapter.Fill(DataSet1);
if (DataSet1.Tables[0].Rows.Count > 0)
{
strGUID = DataSet1.Tables[0].Rows[0][0].ToString();
arrURL[3] = arrURL[3];
}
else
{
strGUID = "";
arrURL[3] = "";
}
DataSet1 = new DataSet();
SQLQuery = "select eid, numchildren, rankchild, url, name, siteid from navnodes where eidparent in (select eid from navnodes where webid = '" + strGUID + "' and siteid = '" + strRootGUID + "' and name = 'Quick Launch') and webid = '" + strGUID + "' and siteid = '" + strRootGUID + "' order by rankchild";
DataAdapter = new SqlDataAdapter(SQLQuery, SQLConnection);
DataAdapter.Fill(DataSet1);
if (DataSet1.Tables[0].Rows.Count > 0)
{
output.RenderBeginTag("div class=ms-quicklaunchouter");
output.RenderBeginTag("div class=ms-quicklaunch style=\"width:100%; border: none;\"");
output.RenderBeginTag("table cellpadding=0 cellspacing=0 class=ms-quicklaunch width=100%");
output.RenderBeginTag("tr");
output.RenderBeginTag("td class=ms-quicklaunchheader style=\"border: none;\"");
output.Write("Navigation");
output.RenderEndTag();
output.RenderEndTag();
for (int i = 0; i <= DataSet1.Tables[0].Rows.Count - 1; i++)
{
output.RenderBeginTag("tr");
output.RenderBeginTag("td");
strListGUID = DataSet1.Tables[0].Rows[i][5].ToString();
output.RenderBeginTag("table class=ms-navheader cellpadding=0 cellspacing=0 border=0 width=100%");
output.RenderBeginTag("tr");
output.RenderBeginTag("td style=width:100%;");
string strTD = "a class=ms-navheader style=\"border-style:none;font-size:1em;\" href=";
if (DataSet1.Tables[0].Rows[i][3].ToString().Substring(0, 7) == "http://")
{
output.RenderBeginTag(strTD + System.Web.HttpUtility.UrlPathEncode(DataSet1.Tables[0].Rows[i][3].ToString()));
}
else
{
output.RenderBeginTag(strTD + "http://" + strRootURL + System.Web.HttpUtility.UrlPathEncode(DataSet1.Tables[0].Rows[i][3].ToString()));
}
output.RenderBeginTag("b");
output.Write(DataSet1.Tables[0].Rows[i][4]);
output.RenderEndTag();
output.RenderEndTag();
output.RenderEndTag();
output.RenderEndTag();
output.RenderEndTag();
output.RenderEndTag();
output.RenderEndTag();
if (int.Parse(DataSet1.Tables[0].Rows[i][1].ToString()) > 0)
{
DataSet2 = new DataSet();
SQLQuery = "select eid, numchildren, rankchild, url, name, DocID from navnodes where eidparent in (select eid from navnodes where eid = '" + DataSet1.Tables[0].Rows[i][0].ToString() + "' and webid = '" + strGUID + "' and siteid = '" + strRootGUID + "') and webid = '" + strGUID + "' and siteid = '" + strRootGUID + "' order by rankchild";
DataAdapter = new SqlDataAdapter(SQLQuery, SQLConnection);
DataAdapter.Fill(DataSet2);
for (int x = 0; x <= DataSet2.Tables[0].Rows.Count - 1; x++)
{
output.RenderBeginTag("tr");
output.RenderBeginTag("td");
output.RenderBeginTag("table class=ms-navitem width=100% border=0 cellpadding=0 cellspacing=0");
output.RenderBeginTag("tr");
output.RenderBeginTag("td style=\"width:100%;\"");
if ((DataSet2.Tables[0].Rows[x][3]) != DBNull.Value)
{
output.RenderBeginTag("a class=ms-navitem style=\"border-style:none;font-size:1em;\" href=http://" + strRootURL + System.Web.HttpUtility.UrlPathEncode(DataSet2.Tables[0].Rows[x][3].ToString()));
}
else
{
DataSet3 = new DataSet();
string strDocGUID = DataSet2.Tables[0].Rows[x][5].ToString();
SQLQuery = "select DirName, LeafName from AllDocs where id = '" + strDocGUID + "'";
DataAdapter = new SqlDataAdapter(SQLQuery, SQLConnection);
DataAdapter.Fill(DataSet3);
output.RenderBeginTag("a class=ms-navitem style=\"border-style:none;font-size:1em;\" href=http://" + strRootURL + "/" + System.Web.HttpUtility.UrlPathEncode(DataSet3.Tables[0].Rows[0][0].ToString()) + "/" + System.Web.HttpUtility.UrlPathEncode(DataSet3.Tables[0].Rows[0][1].ToString()));
}
output.Write(DataSet2.Tables[0].Rows[x][4]);
output.RenderEndTag();
output.RenderEndTag();
output.RenderEndTag();
output.RenderEndTag();
output.RenderEndTag();
output.RenderEndTag();
}
}
}
output.RenderEndTag();
output.RenderEndTag();
output.RenderEndTag();
}
DataSet1 = null;
SQLConnection.Close();
}
catch (Exception ex)
{
output.Write(ex.Message);
}
finally
{
}
}
}
}

Quick Launch Navigation

Have you attempted, or do you know, if it possible to create a similar .ascx control and add to a site through a feature?

Quick Launch as a Feature

@Steve -

Thanks for the comment. Although I haven't tried it, if you think about features and how they work in theory, I would have to say that the answer is yes that it would most definitely work. I might give this approach a shot since it seems recreating the quick launch has interest to the SharePoint community and I'm willing to dive deeper into more elegant solutions. I'll post here what I find probably next week.

C# Quick Launch

Well done bala! Thank you for the C# code and improved stylization. In fact, I haven't really checked pixel by pixel so I should definitely do that with my code snippet above.

Btw, instead of some of the other functions I used, do you know of any WSS 3.0 SDK integration that I could use to improve the web part? I'm open to any improvements! Thanks again for contributing.

Create a quick launch links based on user login

Hi
I want to create a quick launch links based on user login.

By adding code in default.master, I can create links for a user A.

When User B logs in, it shows the links of user A. When i refresh the browser of B, it shows the links of User B.

When i refresh User B browser, it shows links of User B.

I want to retain the links of each user on their login, till they logout.

If this is possible what are the things, i need to keep in mind when i'm developing this.

Awaiting your reply.

Thanks
Prasad

Quick Launch Item Security

Prasad -

Great question and one that I have had to deal with here at work. The Quick Launch works off a completely different system than the List system, whih to me, seems like a major error on Microsoft's part. I don't see why the Quick Launch had to be structured on the backend differently and really should have just been made a list.

Why should it have been made a list? So you could perform list item security and only display/render links that users had rights to see and access.

Out of the box, SharePoint's Quick Launch doesn't seem to support item level security (there might be a feature you could add/write, but I am not aware of one currently). So, you could either look into writing a custom feature or ripping out the Quick Launch and converting the Quick Launch info into a list, applying list item security and displaying those links only if the user has the proper permissions. Those are some of the ideas I have had for handling this same problem.

Thanks hagrin. For us IT

Thanks hagrin. For us IT types who aren't as familiar with C#, would it be possible to post the code project for this? Not knowing much about making Custom Web Parts, I'm not exactly sure what to do with this cost specifically :)

No Problem

Np anonymous. I like being able to share pieces of code that took me a while to figure out to help others.

As for how exactly to use all of this code, yikes that could take a while. I'll try and list out the steps simply and see if that can at least get you started -

1) Open up Visual Studio and create a Web Part Library project.
2) Add a .dwp file and change the info inside to accomodate your web part.
3) Add a .vb web part file and add the code I listed in my article.
4) Build the project.
5) After building the project, there will be a new .dll file in your project's bin directory. Copy that file into the bin directory of your SharePoint server.
6) Make necessary changes to your web.config file and trust file as necessary.
7) Upload .dwp file through the SharePoint web interface into the Web Part gallery.
8) In SharePoint Designer, insert the web part and drag it onto your page in Design view. Save and refresh the page in a browser.

Those are the very, very, very basic steps to using this code. If you need any more help, let me know what you're trying to do and where you are at. You can also email me (contact info on Contact page).

Complicated

Hi,

This is a very detailed write up and at the same time utterly complicated. Does the same apply for MOSS? How could MS make such a mistake? I'm hopeing to find another solution, I’ll post if I do.

Thank you,

Necessary Evil

@Anon -

Yeah, although the article is complicated, it's the only way I know of to recreate the Quick Launch. However, there may be a way that's explained in the SDK, but not that I know of. Actually, thinking about it now, maybe you could create a content placeholder on the left hand side and then add the Quick Launch into that placeholder on every created page, but that doesn't seem like a good solution. To be honest, unless there is an easier way outlined in the SDK, I can't see any other way of accomplishing this.

I actually need to make a few updates to this post - I have had to change the code some to accomodate the templates released by the SharePoint team since they don't follow their own link building structure (sigh).

Syndicate content