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

3 comments:

  1. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. Hello Shrikant,

      To see that internal code you will need to reverse engineer this dll (Microsoft.SharePoint.dll) using tools like .Net Reflector. Then you open the file Microsoft.SharePoint.Navigation.SPHierarchyNode.cs to reach NavigateUrl() method which I already put its code above!

      Delete
  2. Ibraheem, i have created some application pages, which is giving the below error: Uncaught ReferenceError: _spNavigateHierarchy is not defined hence not allowing navigation in my site, and in my lists, (allitems.aspx) page also throwing the same error, hence, ribbon or other navigations are not occurring, can u please, help me with this....

    ReplyDelete