Introduction
I'm having a great start to a new job. I'm getting the chance to explore SharePoint in many new ways. Working in a corporate environment (contrary to a software company) has exposed me directly to the needs of the users. Quite a few of our users have gone through SharePoint training, and know how to create and add pages to their departmental sites. One thing that SharePoint lacks is a Web Part that will allow users to drop a navigation part onto each of their pages to allow them to move from one page to the next. I've created a solution to that problem by creating a navigation part that will query libraries within a site collection and display these links to them using a JavaScript menu bar.
Installation
Hopefully, most of you already know how to install Web Parts, but if you need help, follow these steps:
- GAC both Mullivan.Shared.dll and Mullivan.SharePoint.WebParts.dll.
- Register Mullivan.SharePoint.WebParts.dll in the SharePoint Web Config.
Go to C:\inetpub\wwwroot\wss\VirtualDirectories\<Port Number>\web.config. Add the following in between the SafeControls
node:
<SafeControl
Assembly="Mullivan.SharePoint.WebParts,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=c37a514ec27d3057"
Namespace="Mullivan.SharePoint.WebParts" TypeName="*" Safe="True" />
- Go to Site Settings -> Webparts -> click New on the menu.
Scroll to the bottom and check Mullivan.SharePoint.WebParts.LibraryNavigationWebPart, and then scroll back to the top and click Populate Gallery.
Done! You should see it in your Web Parts collection.
Configuration
After you've dropped the Web Part on the page, you'll want to modify its settings. You'll see a Navigation category at the top of the property pane. This is where you are going to configure each tab in the navigation control. Click the plus to add or double click any existing items to edit.
- Title - What you want to appear as the text for the tab. If you leave it empty, then it will default to the list title.
- List URL - Click Browse to point to the list you want to query.
- Display Field - The internal SharePoint name of the list field that you want to use as the display text for each item that is displayed when the user mouses over the tab.
- Query - The Camel query that will execute against that list to filter items. Make sure you use the internal name for the columns when building your query.
Example:
Click OK, and your first tab is created. You can then move it around and order the tabs appropriately.
Using the Code
We have three different environments at this company, and being able to easily deploy these Web Parts is a must. That means that there can be no resources that have to be dropped into the layouts directory in SharePoint such as images, pages, etc. So, there are three things we want to do when creating this Web Part. We want to embed our JavaScript, CSS, and images.
The first is to embed all JavaScript into this DLL and extract it using the ScriptManager. So, select your JS item (JS/LibraryNavigation.js) and go to the property pane and set the Build Action to Embedded Resource.
Now, we need to add a reference to the embedded resource to our AssemblyInfo.cs. Just append that line all the way at the bottom of the file. So, we need to give the WebResource the location of the resource and the mime type that it is. The location is formatted as "<DefaultNameSpace>.<Folder>.<FileName>". If you have multiple folders, then the format would be "<DefaultNameSpace>.<Folder1>.<Folder2>.<FileName>".
[assembly: ComVisible(false)]
[assembly: Guid("c0e90cc1-f501-4b02-97aa-fffccacfa00f")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: WebResource("Mullivan.SharePoint.WebParts.JS.Utility.js", "text/javascript")]
So now, we can get a URL to the JavaScript resource and register it to the page.
ClientScriptManager csm = this.Page.ClientScript;
Type lnType = this.GetType();
if (!csm.IsClientScriptIncludeRegistered(
@"Mullivan.SharePoint.WebParts.JS.LibraryNavigation.js"))
{
string url = csm.GetWebResourceUrl(lnType,
@"Mullivan.SharePoint.WebParts.JS.LibraryNavigation.js");
csm.RegisterClientScriptInclude(lnType,
"Mullivan.SharePoint.WebParts.JS.LibraryNavigation.js",
ResolveClientUrl(url));
}
Great! Now we have our JavaScript attached. Let's move on to registering our CSS. We go through the same steps to embed our CSS file as we did with our JavaScript file. That is to set the Build Action to Embedded Resource and add the WebResource
object to the AssemblyInfo.cs. Registering a CSS file can be tricky in SharePoint. This is because we can use the CSSRegistration
class, but there is one problem. SharePoint will always load the Core.css after our registered CSS file. You might ask why that matters? Well, in CSS, the last file to load in the page is the winner. Its CSS styles will override anything before it. In our case, we wanted to change the style to the hyperlinks in the Web Part. If we don't get our CSS to load last, then the Core.css styles for hyperlinks will override ours. So, here is the trick to getting our CSS file to load last:
protected override void OnPreRender(EventArgs e)
{
ClientScriptManager csm = this.Page.ClientScript;
Type lnType = this.GetType();
RegisterStyleSheet(ResolveUrl(csm.GetWebResourceUrl(lnType,
@"Mullivan.SharePoint.WebParts.CSS.LibraryNavigation.css")));
base.OnPreRender(e);
}
private void RegisterStyleSheet(string cssUrl)
{
HtmlHead header = FindControl<HtmlHead>(this.Page.Master.Controls);
if (header != null)
{
Literal literal = new Literal();
literal.Text = string.Format("<link href=\"{0}\" type" +
"=\"text/css\" rel=\"stylesheet\"/>", cssUrl);
header.Controls.Add(literal);
}
}
private T FindControl<T>(ControlCollection controlCollection)
where T : Control
{
foreach (Control c in controlCollection)
{
if (c.GetType() == typeof(T))
return (T)c;
else
{
T child = FindControl<T>(c.Controls);
if (child != null)
return child;
}
}
return null;
}
So now, you need to go and set your images and embedded resources and add the WebResource
object to the AssemblyInfo.cs. Now, we can render our HTML and set our image tags to point to our web resource URL.
ClientScriptManager csm = this.Page.ClientScript;
Type lnType = this.GetType();
string libNavLeftUrl = ResolveUrl(csm.GetWebResourceUrl(lnType,
"Mullivan.SharePoint.WebParts.Images.LibNav_Left.jpg"));
string libNavMidUrl = ResolveUrl(csm.GetWebResourceUrl(lnType,
"Mullivan.SharePoint.WebParts.Images.LibNav_Mid.jpg"));
string libNavRightUrl = ResolveUrl(csm.GetWebResourceUrl(lnType,
"Mullivan.SharePoint.WebParts.Images.LibNav_Right.jpg"));
string libNavOverUrl = ResolveUrl(csm.GetWebResourceUrl(lnType,
"Mullivan.SharePoint.WebParts.Images.LibNav_Over.jpg"));
string libNavSubItemMidUrl = ResolveUrl(csm.GetWebResourceUrl(lnType,
"Mullivan.SharePoint.WebParts.Images.LibNav_SubItem_Mid.jpg"));
string libNavSubItemOverUrl = ResolveUrl(csm.GetWebResourceUrl(lnType,
"Mullivan.SharePoint.WebParts.Images.LibNav_SubItem_Over.jpg"));
writer.WriteLine("<div class=\"lnwp_Container\">");
writer.WriteLine("<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\">");
writer.WriteLine(string.Format("<tr style=\"background" +
"-image:url('{0}')\">", libNavMidUrl));
writer.WriteLine("<td>");
writer.WriteLine(string.Format("<div class=\"lnwp_LeftImage\" style" +
"=\"background-image:url('{0}')\" />", libNavLeftUrl));
writer.WriteLine("</td>");
OK, now that we have all of our resources embedded, we can worry about how the users are going to configure this Web Part. It would have been really easy to just drop two text boxes into our Web Part config and make them give us a SPWeb URL and a list name. But, I figured it would be a lot cooler to let them use the List/Web selector that the Content Query Web Part uses! So, let's steal some JavaScript!
Here is our HTML for our Browse button in our configuration panel for the Web Part. The {n} tags are string replaces that I execute later. {0} is the ClientID for our web part. {1} is the ClientID for our View State textbox that holds the XML for our configuration. Finally, {7} is the RelativeServerUrl
for our SPSite.
<input type="button"
id="{0}_ListUrl_BUILDER"
value="Browse..."
title="Click to use list picker."
tabindex="0"
class="ms-PropGridBuilderButton"
style="display:inline;cursor:pointer;width:55px;text-align:center"
onclick="javascript:LPNW_LaunchListPicker('{1}','{0}', '{7}')" />
Here is the OnClick
method that we call:
function LPNW_LaunchListPicker(viewStateId, clientId, serverUrl) {
var callback = function(results) {
LPNW_SetList(clientId, results);
};
LaunchPickerTreeDialog('CbqPickerSelectListTitle', 'CbqPickerSelectListText',
'listsOnly', '', serverUrl, lastSelectedListId, '',
'', '/_layouts/images/smt_icon.gif', '', callback);
}
function LPNW_SetList(clientId, results) {
var listTextBox = document.getElementById(clientId + "_ListUrl_EDITOR");
if (results == null
|| results[1] == null
|| results[2] == null) return;
if (results[2] == "") {
alert("You must select a list!.");
return;
}
lastSelectedListId = results[0];
var listUrl = '';
if (listUrl.substring(listUrl.length - 1) != '/')
listUrl = listUrl + '/';
if (results[1].charAt(0) == '/')
results[1] = results[1].substring(1);
listUrl = listUrl + results[1];
if (listUrl.substring(listUrl.length - 1) != '/')
listUrl = listUrl + '/';
if (results[2].charAt(0) == '/')
results[2] = results[2].substring(1);
listUrl = listUrl + results[2];
listTextBox.value = listUrl;
}
So, what we are going to get out of this is a string
that looks like the following:<WebRelativeServerUrl>\<ListTitle>
. You might be wondering ListTitle!?!?! What if the administrator changes the name of the list? The URL to the list stays the same, but the title changes!! Well, we are going to need to do some parsing in the C# code-behind to be able to get our SPList
object form SharePoint. I'll go ahead and demonstrate.
private NavLink GetNavLink(LibNavLink link)
{
NavLink navLink = new NavLink();
SPSite site = SPContext.Current.Site;
using (SPWeb web = site.OpenWeb(GetWebUrl(link.ListUrl)))
{
string listName = GetListName(link.ListUrl);
SPList list = web.Lists[listName];
if (list == null)
throw new Exception(string.Format("List {0} could not be found.",
link.ListUrl));
if (string.IsNullOrEmpty(link.DisplayField))
{
if (list.BaseType == SPBaseType.DocumentLibrary)
link.DisplayField = "BaseName";
else
link.DisplayField = "Title";
}
navLink.Title = link.Title;
navLink.Url = MullivanUtility.GetServerUrl(this.Page.Request) +
list.DefaultViewUrl;
SPQuery query = new SPQuery();
if (!string.IsNullOrEmpty(link.Query))
query.Query = link.Query;
query.ViewAttributes = "Scope=\"Recursive\"";
query.ViewFields = string.Format("<FieldRef Name=\"EncodedAbsUrl\" />" +
"<FieldRef Name=\"{0}\" />", link.DisplayField);
SPListItemCollection items = list.GetItems(query);
foreach (SPItem item in items)
{
NavLink subLink = new NavLink();
subLink.Title = item[link.DisplayField].ToString();
subLink.Url = item["EncodedAbsUrl"].ToString();
navLink.SubLinks.Add(subLink);
}
}
return navLink;
}
private string GetWebUrl(string listUrl)
{
string webUrl = string.Empty;
listUrl = listUrl.TrimEnd('/');
int lastIndex = listUrl.LastIndexOf("/");
if (lastIndex > -1)
webUrl = listUrl.Substring(0, lastIndex);
return webUrl.TrimEnd('/');
}
private string GetListName(string listUrl)
{
string listName = string.Empty;
listUrl = listUrl.Trim('/');
int lastIndex = listUrl.LastIndexOf("/");
if (lastIndex > -1)
listName = listUrl.Substring(lastIndex + 1, listUrl.Length - lastIndex - 1);
return listName;
}
Points of Interest
There are two other Web Parts included in the project. The weather Web Part was taken from a blog made by the Mossman here. The other is a Web Part that I just blogged about. It's a really great Web Part that queries the Analytics database and pulls back the most used content either by a user or anonymously. I call it the Most Viewed Web Part. Check it out here.
Conclusion
Let me know what you think. If you have any ideas, then I'll be sure to take a look and see if I can provide an update. I hope you found this Web Part useful.
History
- 14th January, 2009: Initial post
- 19th March, 2009: Updated source code
- 24th March, 2009: Updated source code