November 14, 2013

How to make SPTreeView work on a custom page with 2 or more list views


The issue

I was reading the other day an article here on how to add an SPTreeView control to a list view page using SharePoint Designer 2010. This works fine on a library view page (for example: AllItems.aspx).
However, if you add another list view web part into the same list view page, or if you want to use the SPTreeView in a custom page then an issue appears. When user clicks on folder node the SPTreeView control will NOT stay in the same page but will redirect into the library root page (AllItems.apsx).



Analysis

I found very little info about this in the Internet, so decided to dig this up. I'll show here the details with an example. If you are interested keep reading, or you can go directly to solution section below.

The example setup:
  • Suppose we want a custom page to show 2 list views of the same library using different filters, and we want a tree view navigation to work for both of them whenever we click a folder.
  • I created a document library named 'Test Library Doc' and put some files and folders.


  • Created 2 list views for 'Test Doc Lib':
  1. Local view: Content Type is not Folder, and the Scope field is equal to Global
  2. Global view: Content Type is not Folder, and the Scope field is equal to Local
  • Added a custom web part page 'TestTreeView.aspx', added an SPTreeView control with its SPHierarchyDataScourceControl to the page using SPD2010. Make sure the RootListID and RootWebId are relevant.



Tests: (both on custom page TestTreeView.aspx)

Case 1:
Using SPTreeView with only 1 list view web part. Upon user click, each folder node will redirect to same page TestTreeView.aspx with corresponding folder level. Here is how 'Folder A' node is rendered:

<a title="Folder A" class="ctl00_PlaceHolderMain_doclibtreeview_0" id="ctl00_PlaceHolderMain_doclibtreeviewt1" href="javascript:_spNavigateHierarchy(this,'doclibDataSource','30:FolderNode:ef072b87-28a5-4db4-b82f-097c7ba329d3','\u002fTest Library Doc\u002fFolder A',true,'FolderNode')">

Case 2:
Using SPTreeView with 2 or more list view web parts. Upon user click, each folder node will redirect to the default page of library (example: AllItems.aspx) instead of our TestTreeView.aspx. Here is how 'Folder A'node is rendered:

<a title="Folder A" class="ctl00_PlaceHolderMain_doclibtreeview_0" id="ctl00_PlaceHolderMain_doclibtreeviewt1" href="javascript:_spNavigateHierarchy(this,'doclibDataSource','30:FolderNode:ef072b87-28a5-4db4-b82f-097c7ba329d3','\u002fTest Library Doc\u002fFolder A',false,'FolderNode')">



I tried to override _spNavigateHierarchy() method & inserted our custom page TestTreeView.aspx as the url parameter to force the redirect..but it didn't work. This is due to how the tree view nodes internally works as you will see below.


Digging on the front-end:


One of the arguments (highlighted above in test cases 1 & 2) caught my attention, it's called 'listInContext'. The _spNavigateHierarchy() definition is inserted by the SPHierarchyDataSourceControl in its onLoad event:

<script type=\"text/javascript\"> \r\n//<![CDATA[\r\nfunction _spNavigateHierarchy(nodeDiv, dataSourceId, dataPath, url, listInContext, type)  {\r\n   CoreInvoke('ProcessDefaultNavigateHierarchy', nodeDiv, dataSourceId, dataPath, url, listInContext, type, document.forms." + this.Page.Form.ClientID + ", \"" + SPEncode.ScriptEncode(this.ConstructSamePageQueryString()) + "\", \"" + SPEncode.ScriptEncode(str) + "\");\r\n\r\n}\r\n//]]>\r\n </script>

When we click a folder node,  the following call sequence starts:
_spNavigateHierarchy() -> CoreInvoke() -> ProcessDefaultNavigateHierarchy() in Core.js file

function ProcessDefaultNavigateHierarchy(g,e,d,b,c,j,i,h,f){
    ULSrLq:;
    if(typeof _spCustomNavigateHierarchy=="function")_spCustomNavigateHierarchy(g,e,d,b,c,j);
    else if(c==false)top.location=b;
    else{
        var a=document.createElement("INPUT");
        a.type="hidden";
        a.name="_spTreeNodeClicked";
        a.value=d;
        i.appendChild(a);
        var k="?RootFolder="+escapeProperly(b)+h+"&"+g_AdditionalNavigateHierarchyQString;
        _SubmitFormPost(f+k);
        return false
    }


Inside ProcessDefaultNavigateHierarchy(), the logic is as this:
If the 'islistInContent' flag is false then set the page location to the root page of current library and eventually redirect to the library root page (i.e AllItems.aspx). Otherwise, if the islistInContent flag is true, then eventually redirect to our custom page (TestTreeView.aspx).

Now we need to find out why listInContext parameter was set to false in test case 1.


Digging more on the back-end:


Let's have a look on how the SPTreeView is being internally rendered inside Microsoft.SharePoint.dll
In Microsoft.SharePoint.Navigation.SPHierarchyNode.cs I found NavigateUrl() method representing a tree node click. The highlighted portion is the important part.

public string NavigateUrl

        {

            {
                string str = "javascript:_spNavigateHierarchy(this,";
                return (((((str + "'" + this.DataSourceControlName + "','") + this.Path + "','") +
SPHttpUtility.EcmaScriptStringLiteralEncode("/" + this.m_url)) + "'," + ((this.m_ds.IsSingleListViewPage &&
this.IsContextListNode) ? "true" : "false")) + ",'" + this.m_type + "')");
            }
        }



According to internal code, 'IsSingleListViewPage' member returns True when there is only 1 web part (either data view or xslt list view) in current page. 'IsContextListNode' property returns True only within a list context and there is a valid list ID.
So, as long as we use 2 list view web parts or use a custom page, then listInContext parameter in NavigateUrl() will always get the value of 'False'.



Solution:

I Wrote a JQuery script to force change the listInContext flag value from 'False' to 'True' for every rendered tree view folder node, then insert this script into a CEWP in our custom TestTreeView.aspx page:

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>

<script>

_spBodyOnLoadFunctionNames.push("IbrTreeView");

function IbrTreeView() {

$("a[href*='javascript:_spNavigateHierarchy']")
.each(function()
{
this.href = this.href.replace("false", "true");
});
}

</script>


And it works!
When user clicks 'Folder A' folder node, the 2 view web parts will show underlying filtered contents on the same TestTreeView.aspx page. No more redirecting to AllItems.aspx.




Note:
Remember your page should only contain list view web parts of the same library. Otherwise user will witness inconsistencies/errors when clicking on tree nodes because SPTreeView may redirect to a folder that exist in library of 1st web part but does not exist in library of the 2nd web part.


I hope people find this useful :)

Ibraheem

November 6, 2013

Styling The Folders In Doc Lib Using JQuery and CSS


Objective:

In your SharePoint site, sometimes you need to stylize how the folders look like so that they no longer reflect the typical folder hierarchy layout. You can use a new image, color and font style in a specific document library view or in a web part page 


For example:
  • We want to replace folder icon with a custom image
  • We want the title font to be bold and with larger size


How to:


First let's prepare url to the new folder icon file. I uploaded "fact.gif" into Images library:
        http://{YourServerName}/publishingImages/fact.gif



Since we don't want to affect the entire SharePoint farm, we will use JQuery script to target specific page. You can use the script by registering the script file using ScriptLink control, or by putting the follwoing script inside a Content Editor Web part (CEWP).



1. To change the folder type icon only:


<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>

<script>
_spBodyOnLoadFunctionNames.push("changeFolderUI");

function changeFolderUI() {
jQuery("a[href*='?RootFolder=']").find("img").attr("src", "http://{YourServerName}/publishingImages/fact.gif");
}

</script>





2. To change the folder name and add icon to it:

(This can be useful when hiding the Type field, allowing only folders to have icons in Name field)

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script><script>

_spBodyOnLoadFunctionNames.push("changeFolderUI");

function changeFolderUI() {
jQuery("a[href*='?RootFolder=']:not(:has(img))")
.css("font-weight", "bold")
.css("font-size", "16px")
.before("<img alt='arrow' src='{YourServerName}/publishingImages/fact.gif'/>");

}</script>




You can also step forward and apply multiple custom styling into folders depending on a folder name.
Have fun!