In the first part of the article I introduced the matter, i.e. adding a Silverlight menu in Sharepoint. The first solution presented, defined as “the client approach”, has a major defect: it cannot be used for anonymous-enabled sites. In this second part we will see another way (that I called the “server approach”) in order to build a Silverlight menu for Sharepoint that can be used also by anonymous users. To make this new approach clearer I will introduce another few general concepts regarding the Sharepoint platform. As previously, they will be marked with a specific icon and Sharepoint developers can skip these parts.
The server approach
An alternative solution for the anonymous – enabled menu is illustrated in the picture below:
Why calling it “server approach”? Because the phase of retrieving the hierarchy of our Sharepoint site is made by code which is running on the server. And who is the owner of this code portion? The answer is: the web part hosting the Silverlight application. Let me take a while to recap some basic concepts I explained in my first article on this series Sharepoint and Silverlight working together. In that article we sectioned the structure of the Silverlight custom web part we are using here to inject the Silverlight application into a Sharepoint page. We have seen that it is a specialized Visual Studio template which helps us create a web part capable of hosting a Silverlight application. The Silverlight application itself is embedded in a <div> tag inside the custom control which represents the web part. This web part is defined as “sandboxed visual web part” i.e. a “Sharepoint 2010 Visual Web Part” with limited power.
A Silverlight developer could feel a bit bewildered when hearing about visual, sandboxed web parts. In the Sharepoint context, a Web Part is conceptually nothing other than an ASP.NET Web Part. It is an ASP.NET user control which acts as a black box that you can place in one of the zones allowed in the UI layout of a page. Although there is a specific namespace (Microsoft.WebPartPages.WebPart) for the web parts, it is used in rare cases. As usual you can build your own Web Part deriving from System.Web.Ui.WebControls.WebPart. Sharepoint 2010 introduced a few new project templates for Visual Studio 2010, one of these is called “Visual Web Part”. This template creates a ready-to-deploy Web Part with a built-in user control. Editing the user control in design mode or via code behind you can enrich your pages with new features.
The term “sandboxed” is used in Sharepoint 2010 to describe a restricted environment. A “sandboxed solution” is something that can have access only to a subset of the Sharepoint server object model and of the .NET 3.5 assemblies (btw Sharepoint 2010 does not use .NET 4.0) and that runs in a restricted code access security policy. Furthermore, server farm admins can regulate resource usage limits for these solutions. A Visual Web Part is not a sandboxed solution. That’s probably why in the Visual Studio Sharepoint Power Tools add-on a new project template called “Sandboxed Visual Web Part” has been added. A “Silverlight Custom Web Part” is a “Sandboxed Visual Web Part” hosting a Silverlight application, nothing more than that.
Coming back to the picture above, I put the retrieval of the hierarchy under the Sharepoint side because we will do it by invoking the Server Object Model in a code running on server side. After the hierarchy has been provided it is then put into a custom List (the basic data container of Sharepoint) using a nice technique called “Modified Preorder Tree Traversal” that we will see later. Finally, the custom List containing the menu hierarchy can be read by the Silverlight application using the Client Object model every time the cache expires. Why using a custom list? Because we can set anonymous access to this list. This allows a Silverlight application to read the List also in the case in which we are not logged in. Don’t forget that the Silverlight app uses the default credentials when it queries the server.
Retrieving the hierarchy of the site
There is more than one way to get the hierarchy of a Sharepoint site collection. You can use an existing Site Map provider or create your own or, as in this case, use a built-in Sharepoint control, i.e. the SPHierarchyDataSourceControl which provides hierarchical views of Sharepoint sites, lists, and folders. It is the same control used for the “tree view” navigation system mentioned in the first part of this article. Extracting the hierarchy from this control is a simple undertaking.
As shown in the code snippet below it is a matter of instantiating a new SPHierarchyDataSourceControl, setting some properties in order to define which categories of nodes to retrieve, obtaining a HierarchicalDataSourceView and performing a Select() over the view to finally have a IHierarchicalEnumerable collection of menu nodes.
private IHierarchicalEnumerable sitemapNodes;
…
SPHierarchyDataSourceControl HierarchyDataSC = new SPHierarchyDataSourceControl();
HierarchyDataSC.IncludeDiscussionFolders = true;
HierarchyDataSC.ShowDocLibChildren = true;
HierarchyDataSC.ShowFolderChildren = true;
HierarchyDataSC.ShowListChildren = true;
HierarchyDataSC.ShowWebChildren = true;
HierarchyDataSC.RootContextObject = "Web";
HierarchicalDataSourceView view = HierarchyDataSC.GetHierarchicalViewPublic("");
sitemapNodes = view.Select();
If you try to include the code above in your Silverlight Custom Web Part unfortunately it won’t compile. Why? Because as clarified in the previous paragraph the Web Part is a sandboxed We Part and this prevents you from using some assemblies, amongst them the one needed for the code above. You have two options to get out from the mud: one is to abandon the Silverlight Custom Web Part and use a normal Visual Web Part in the way I described in one of my previous articles entitled “Silverlight and Sharepoint 2010 a step forward: how to build a small Silverlight 4 utility to upload files in a List or Library of Sharepoint”, precisely in paragraph “Deploy the Silverlight application using a Visual WebPart”. The other is to modify the settings of the Silverlight Custom Web Part in an “unconventional” way, i.e. by making it become a not-sandboxed solution.
Deploy a Silverlight Custom Web Part as a not-sandboxed solution: a dirty trick
By default when using a Silverlight Custom Web Part, the property “Sandboxed Solution” of the project is set to true as in the picture below:
If you try to set this property to false and then to include the code above to retrieve the hierarchy, you will be able to build the solution but not to deploy it. The error message that will appear is the following:
where “MenuSCWP” is the name I used to replace the default name “SilverlightcustomWebPart1”. As you can see in the second-last picture this project item is in charge of transporting a “MasterpageGallery” module in Sharepoint. This module contains the xap file of our Silverlight application. If you open the file “Elements.xml” nested in the module you can confirm this statement:
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Module Name="Menu4U" Url="_catalogs/masterpage/ClientBin/$SharePoint.Package.Name$">
<File Path="Menu4U\Menu4UForSP.xap" Url="Menu4UForSP.xap" />
</Module>
</Elements>
And now comes the trick to remove this obstacle, and I would like to warn you: it is a very dirty trick.
In Visual Studio right-click on the Silverlight custom Web Part item (I renamed it “MenuSCWP) as in the picture below:
Click on “Open Folder in windows Explorer” to open the folder. You should see a couple of subdirectories and a file named “SharePointProjectItem.spdata” as in the picture below:
Open the file. It should have a content similar to this one:
<?xml version="1.0" encoding="utf-8"?>
<ProjectItem Type="MicrosoftSilverlightCustomWebPart" SupportedTrustLevels="Sandboxed" SupportedDeploymentScopes="Site" xmlns="http://schemas.microsoft.com/VisualStudio/2010/SharePointTools/SharePointProjectItemModel">
<Files />
</ProjectItem>
Take a look at the “SupportedTrustLevels” attribute of the projectitem node: actually it is set as “Sandboxed”. Now write “FullTrust” instead, and save the file. Come back to Visual Studio and try to deploy the solution. If all the steps have been followed correctly, the solution will be deployed. Pay attention to the fact that when you close the solution and open it again in Visual Studio, the file is overwritten and you have to set the attribute again. It is not so annoying if you make a batch file or a vbscript that performs the replacement for you. You can also launch the execution of the script from the pre-build event.
Saving the hierarchy in a Sharepoint custom list: the modified preorder traversal tree
In the previous paragraph I explained the reason why I want to put the hierarchy into a Custom Sharepoint List: it can be read also by anonymous users. The problem is that a List in Sharepoint is basically a table with rows and columns, while the IHierarchicalEnumerable object that we have obtained represents a tree hierarchy. How to save a tree-hierarchy in a table then? After a moment of despair I recalled an old article entitled Storing Hierarchical Data in a Database that illustrated an algorithm called Modified Preorder Tree Traversal. The principal assumption of the algorithm is that each node in the hierarchy tree must have a left value and a right value. Then the tree is traversed assigning opportune values for left and right according to the rules shown in the flowchart below:
At a first glance it may look a bit complicated but if you try to put on paper a simple tree and assign the left and right values following the flowchart above, it will be a fun exercise. The following picture shows a hierarchy tree, a tree already numbered with the indication of the direction of traversing:
The nice thing of this approach is that after the hierarchy is put into a table you can retrieve nodes with simple queries. With reference to the picture above, if you want to retrieve the subtree starting from the node title “SITE AA” you can launch this query:
“SELECT * FROM tree_table WHERE left BETWEEN 3 AND 8 ORDER BY left ASC;
If you want all the tree simply write:
“SELECT * FROM tree_table WHERE left BETWEEN 1 AND 22 ORDER BY left ASC;
As for the code implementation, I created a class “MpttItem” representing a single node as below:
class MpttItem
{
private string title;
public string Title
{
get { return title; }
set { title = value; }
}
private string type;
public string Type
{
get { return type; }
set { type = value; }
}
private string imageUrl;
public string ImageUrl
{
get { return imageUrl; }
set { imageUrl = value; }
}
private string serverRelativeUrl;
public string ServerRelativeUrl
{
get { return serverRelativeUrl; }
set { serverRelativeUrl = value; }
}
private string navigateUrl;
public string NavigateUrl
{
get { return navigateUrl; }
set { navigateUrl = value; }
}
private int left = 0;
public int Left
{
get { return left; }
set { left = value; }
}
private int right = 0;
public int Right
{
get { return right; }
set { right = value; }
}
private int id;
public int ID
{
get { return id; }
set { id = value; }
}
protected MpttItem parent;
public MpttItem Parent
{
get { return parent; }
set { parent = value; }
}
protected IHierarchyData iHierarchyItem;
public IHierarchyData IHierarchyItem
{
get { return iHierarchyItem; }
set { iHierarchyItem = value; }
}
}
And then in the code behind the sandboxed (not-sandboxed) Web Part I stored each node in a List of MpttItem using a recursive function as below:
List<MpttItem> menuRows;
[…]
menuRows = new List<MpttItem>();
MpttItem mpttRootItem = new MpttItem()
{
Parent = null,
IHierarchyItem = null,
Title = "root",
Left = 1,
Right = 0,
};
menuRows.Add(mpttRootItem);
foreach (object item in sitemapNodes)
{
IHierarchyData menuNode = (IHierarchyData)sitemapNodes.GetHierarchyData(item);
if (menuRows.Count == 1)
{
AddMenuRowsRecursive(null, menuNode, 2, 2);
}
else
{
int startIndex = menuRows[menuRows.Count - 1].Right + 1;
AddMenuRowsRecursive(null, menuNode, startIndex, startIndex);
}
}
menuRows.Sort(
delegate(MpttItem p1, MpttItem p2)
{
return p1.Right.CompareTo(p2.Right);
}
);
menuRows[0].Right = menuRows[menuRows.Count - 1].Right + 1;
menuRows.Sort(
delegate(MpttItem p1, MpttItem p2)
{
return p1.Left.CompareTo(p2.Left);
}
);
AddToSPList();
The recursive function is the following:
private int AddMenuRowsRecursive(MpttItem mpttParent, IHierarchyData currHIData, int leftIndex, int rootIndex)
{
if (mpttParent == null && leftIndex > rootIndex)
return 0; // elaboration completed
string title = GetPropertyDescriptorValue(currHIData, "Name");
Microsoft.SharePoint.Navigation.ISPHierarchyData currHIOtherData = (Microsoft.SharePoint.Navigation.ISPHierarchyData)currHIData.Item;
// look if the current mpttItem was already elaborated
MpttItem elaboratedItem = menuRows.Find(i => ( (i.IHierarchyItem == null || currHIData == null) ? false : i.IHierarchyItem.GetHashCode() == currHIData.GetHashCode()));
IHierarchyData childHiData = GetFirstAvailableChild(currHIData);
if (childHiData == null)
{
if (leftIndex > rootIndex && elaboratedItem == null /* && leftIndex < mpttParent.Left + 2*/)
{
// then it is a leaf
MpttItem mpttItem = new MpttItem()
{
Parent = mpttParent,
IHierarchyItem = currHIData,
Title = title,
Type = currHIData.Type,
ServerRelativeUrl = currHIOtherData.ServerRelativeUrl,
NavigateUrl = SPContext.Current.Web.Site.Url + currHIOtherData.ServerRelativeUrl,//currHIOtherData.NavigateUrl,
ImageUrl = currHIOtherData.ImageUrl,
Left = leftIndex,
Right = leftIndex + 1
};
menuRows.Add(mpttItem);
AddMenuRowsRecursive(mpttItem.Parent, mpttItem.Parent.IHierarchyItem, mpttItem.Right + 1, rootIndex);
return 0;
}
else
{
if (mpttParent == null)
return 0;
mpttParent.Right = leftIndex;
if (mpttParent.Parent == null)
return 0;
AddMenuRowsRecursive(mpttParent.Parent, mpttParent.Parent.IHierarchyItem, mpttParent.Right + 1, rootIndex);
return 0;
}
}
else
{
// it is a branch
if (elaboratedItem == null)
{
MpttItem mpttItem = new MpttItem()
{
Parent = mpttParent,
IHierarchyItem = currHIData,
Title = title,
Left = leftIndex,
Type = currHIData.Type,
ServerRelativeUrl = currHIOtherData.ServerRelativeUrl,
NavigateUrl = SPContext.Current.Web.Site.Url + currHIOtherData.ServerRelativeUrl,//currHIOtherData.NavigateUrl,
ImageUrl = currHIOtherData.ImageUrl,
};
menuRows.Add(mpttItem);
AddMenuRowsRecursive(mpttItem, childHiData, mpttItem.Left + 1, rootIndex);
return 0;
}
AddMenuRowsRecursive(elaboratedItem, childHiData, leftIndex, rootIndex);
return 0;
}
}
The AddToSPList() function performs the insertion of the nodes in a Sharepoint custom list which I called “MenuTable”:
private void AddToSPList()
{
SPWeb mySite = SPContext.Current.Web;
SPListItemCollection listItems = mySite.Lists["MenuTable"].Items;
mySite.AllowUnsafeUpdates = true;
// clean list
PurgeList(listItems);
foreach (MpttItem mpttItem in menuRows)
{
SPListItem item = listItems.Add();
item["Left"] = mpttItem.Left;
item["Right"] = mpttItem.Right;
item["Title"] = mpttItem.Title;
item["ImageUrl"] = mpttItem.ImageUrl;
item["NavigateUrl"] = mpttItem.NavigateUrl;
item["ServerRelativeUrl"] = mpttItem.ServerRelativeUrl;
if (mpttItem.Parent != null)
{
item["ParentLeft"] = mpttItem.Parent.Left;
item["ParentRight"] = mpttItem.Parent.Right;
}
else
{
item["ParentLeft"] = menuRows[0].Left;
item["ParentRight"] =menuRows[0].Right;
}
item.Update();
}
}
The source code
At this link you find the complete solution discussed in this article. It is not an optimized project but a starting point ready to be improved if you want to create your own Silverlight menu for Sharepoint. It does not contain any cache management either from server side or client side. It is left as an exercise for you. In order to put the menu in practice it is recommended to put it in a master page using Sharepoint Designer.
Summary
In this second part we have seen how to implement a Silverlight menu using a so called “server approach” where the retrieval of the hierarchy is made at the server side. We have learned a dirty trick to “un-sandbox” a Silverlight Custom Web Part and a way to retrieve the hierarchy. Then we have explored a fun method to save the hierarchy in a table so that it can be read by the Silverligt menu also in an anonymous context.