Friday, February 21, 2014

SharePoint 2013: Manage Site/Web/Lists/Folder Filter using Picker Tree Dialog (LaunchPickerTreeDialogSelectUrl )

This last days i received some emails about the LaunchPickerTreeDialogSelectUrl Method and which parameters can be used to manage in my solution "Manage List Threshold" and how we can filter "Sites/web/Lists/Folders" and be used in other SharePoint Solutions, because of that i am making this short article.

First thing i have to say is that this option is a SharePoint OoB solution (i remember this method from SharePoint 2007) and you can find in many places in SharePoint, for example when you are uploading a document and you need to choose a sub folder or when you need to select a List from specific Web Parts.

Here some articles talking about this Method:

The Method LaunchPickerTreeDialogSelectUrl  from the SharePoint file 'pickertreedialog.js'.

Image Example (select List/folder from the current Site)



Here are my findings you can do to help implement this option:

The first thing we should know is the use of Internet Explorer Developer Tools to identify the properties and options of the Method.

For this example i access to modal dialog of Document Library where the Method that call the tree picker in the option "Choose Folder..."



when you access to folder using "Chrome" you are able to see the url with the parameter that was used to access the Picker Method, this can be useful to filter your tree dialog box .




When you select the IE Developer Tools you can access to the properties of the Method when you search for the method "LaunchPickerTreeDialogSelectUrl"

In the F12 (Alt+x > F12 Developer Tools) of your Internet Explorer and select Debugger and the page "Upload.aspx" you will be able to see the properties.




One of the most important parameter when we are filtering a List/Document Library is the var currAnchor "SPList:[ID]?SPWeb:[ID]:".
This option filter the Picker Tree dialog for a specif List/Document Library.




Here are some parameter we can use in the Method:

websLists - Returns all Lists/Document Library
websListsFolders -  Returns all Lists/Document Library and Folders
"scopeToWeb" - Return the Lists of the current Web, if you need to Scope for all Site Collection add the property "&scopeToSite=true" workaround in *Custom Browser for All Sites and Sub Sites talked in the examples below.
requireCT - Validate if a selected Content Type "Example CT Document 0x0101" exists in the List/document Library, if not returns error message.



To call the Method LaunchPickerTreeDialogSelectUrl  should use the reference to API File 'pickertreedialog.js'.

Custom Browser for List Picker (Code example):

For this Method add a dialogbox button and should open the current Web Object and list and Lists and their Folders. 

<script type="text/ecmascript" language="ecmascript">

function LaunchTargetPicker() {
    var callback = function (dest) {

        if (dest != null && dest != undefined && dest[3] != null) {
            document.getElementById('URLTextBox').value = dest[3];
        }
    };
   
    var iconUrl = "/_layouts/15/images/smt_icon.gif?rev=32";
    SP.SOD.executeFunc('pickertreedialog.js', 'LaunchPickerTreeDialogSelectUrl', function () {
        LaunchPickerTreeDialogSelectUrl('CbqPickerSelectListTitle', 'CbqPickerSelectListText', 'websListsFolders', '', 'https://[Site]', '', '', '', iconUrl, '', callback, 'true', '');
    });
}
</script>
<input type="text" name="" id="URLTextBox" value=" " />
<input type="submit" class="ms-input" value="Browser" id="BtnBrowseReportStorageLocation" onclick="LaunchTargetPicker(); return false;" /> 



I

Custom Browser for All Sites and Sub Sites (Code example):

For this Method the dialogbox should open the current Site and their SubSites and list and Lists with their Folders

function LaunchTargetPicker() {
    var callback = function (dest) {

        if (dest != null && dest != undefined && dest[3] != null) {
            document.getElementById('URLTextBox').value = dest[3];
        }
    };

    var iconUrl = "/_layouts/15/images/smt_icon.gif?rev=32";
    SP.SOD.executeFunc('pickertreedialog.js', 'LaunchPickerTreeDialogSelectUrl', function () {
        LaunchPickerTreeDialogSelectUrl('CbqPickerSelectListTitle', 'CbqPickerSelectListText', 'websListsFolders', '', 'https://[site]', '', '', '', iconUrl, '', callback, '&scopeToSite=true', '');
    });
}




Custom Browser for Folder Picker in specific Library (Code example):

This method Filter a select Lists/Document Library and his Folder.
to support this method is needed the values SPList ID of the List you want to filter and Web ID from the site and concatenate with the following sequence;
SPLIST:[ID]&SPWeb:[ID]:
To automatic select the subfolder se the url Path as describe in parameter [add SubFolder url]

function LaunchTargetPicker(TextBoxId) {

    var callback = function (dest) {
        if (dest != null && dest != undefined && dest[3] != null) {

            document.getElementById('URLTextBox').value = dest[3];
        }
    };

    var iconUrl = "/_layouts/15/images/smt_icon.gif?rev=32";
    SP.SOD.executeFunc('pickertreedialog.js', 'LaunchPickerTreeDialogSelectUrl', function () {
        LaunchPickerTreeDialogSelectUrl('CbqPickerSelectListTitle', 'CbqPickerSelectFolderText', 'websListsFolders', 'SPList:94dbbb89-0a50-4f16-b002-ecb5cabda31b?SPWeb:be7803ee-376e-4e7f-a4b9-e483f749f377:''https://[site]', '[add SubFolder url]', '', '', iconUrl, '', callback, 'true', '');
    });

}





This is a last minute method, This is another Dialog Picker you can use from the page "AssetPortalBrowser.aspx". This Out of the box Page gives you the ability to select file or List from the existing Site.





Custom Browser to select File in Site (Code example):


<script>
function OpenAssetPortalBrowserDialog() {

    var clientContext = SP.ClientContext.get_current();
    Web = clientContext.get_web();
    clientContext.load(Web);
    clientContext.executeQueryAsync(function(){
        var UrlPagePicker= "/_layouts/15/AssetPortalBrowser.aspx";
        var Parameteres = "?&AssetType=Link&AssetUrl=" + _spPageContextInfo.webServerRelativeUrl + "&RootFolder=" + _spPageContextInfo.webServerRelativeUrl + "&MDWeb=" + Web.get_id();
        var url='';
        if (_spPageContextInfo.webServerRelativeUrl =='/')
        {url=UrlPagePicker+Parameteres}
        else{url=_spPageContextInfo.webServerRelativeUrl+UrlPagePicker+Parameteres}
        var options = SP.UI.$create_DialogOptions();
        options.url = url;
        options.dialogReturnValueCallback = Function.createDelegate(null, CloseAssetPortalBrowserCallback);
        SP.UI.ModalDialog.showModalDialog(options);
    }, function(){alert('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());});      
}
function CloseAssetPortalBrowserCallback(result, target) {
    if (result == SP.UI.DialogResult.OK) {
        document.getElementById('URLTextBox').value=target.AssetUrl;
    } 
}
</script>

<input type="text" name="" id="URLTextBox" value=" " />

<input type="submit" class="ms-input" value="Browser" id="BtnBrowseDocumentStorageLocation" onclick="OpenAssetPortalBrowserDialog(); return false;" />




To give one overview of the Values that return, you can select the Callback Value from the Modal Dialog using IE Developer Tools Debugger Mode and see the parameters






Hope you like this article, 
Kind regards, 
Andre Lage

Monday, February 17, 2014

SharePoint 2013 App - Manage List Threshold (Moving Multiple Documents in Folders) Office365/ On-premise

This is a new app project that came when was working with SharePoint Online (Office 365), 
I had a Library with folders with more then 5000 documents and wasn't able to access the View, well... nothing new, only needed to make a query and move the documents, i used the same approach in SharePoint 2010 ECMAScript and done, batch 1000 by 1000 to another folder.

Then i think to myself how a normal user or "Admin" of a site (can IT or Business) will manage this?

I get surprise how people get stuck with this type of situations, even when they are instructed to avoid that, this happen very often.

Objective:
My goal was to create a simple tool using JSOM to migrate documents from folder with more then 5000 Documents and avoid the error message.
This solution audits folders, in a Document Library with a defined number of documents (e.g >5000 Documents). 
This Folder Url will be use to get the ID of the Documents and migrate to other/New Folder in the same Document Library and avoid the following error.



No, This is not another article talking about configuring the List Thresholds in Web application and Server side code, "SharePoint Online don't allow to change this values or make Server Side code".

If you are interested in that area please read this articles about that.
Here some articles talking about configuration and working with List Thresholds: 

Manage List Threshold Files App Solution

This solution was made using the ECMAScript and JSOM to track and make bulk migration of Documents.





This Solution can be implemented in the following tested environments:
  • Office 365
  • SharePoint 2013 Standard/Enterprise
App Manifest:
  • Hosting-Type: SharePoint-Hosted
  • Scope: Web
  • Permissions: Write
Tools:
  • Visual Studio 2013
    • SharePoint App
  • Fiddler
  • Internet Explorer (Developer Tools)
  • You can use "Napa" Office 365 Development Tools to 
Who should Test:
  • Developers in their Test Environment with Know-How of SharePoint Lists and SharePoint JSOM.
PS: This solution wasn't validated with all situations and errors could appear.
This is a example made with JSOM and SharePoint Online.
I don't recommend publish in production. If you made, will be by your own risk.

Functionality of the Solution





Audit Folders:
  • This section give the option to select the Document Library and make a auditory to identify the number of items by Folder and Sub-folder.
  • It is possible filter the Search by the numbers of Document by Folder.
  • By default the value is 0, but you can change to 5000 to return folder with more than 5000 Documents.
  • The option "Copy url" will copy the serverRelativeUrl to "List Folder" field in "Get Items ID".

Get Items  ID:
  • This section gives the option to select a Folder and retrieve the ID's Files.  This array of ID's will be use to make the bulk move.
  • It is possible to filter a define number of ID's to be moved,  (max. is 1000) because of specific REST Url Query.
  • The ID's helps the bulk moving of Documents.


Move Items/Files:
  • In this section it is possible to create Sub Folders in a Document Library
    • Add a new Folder Path in the Destination Folder and select the option "Create Folder"

·         Move Document to another Folder
o   Select the folder where the documents should move. Copy paste the ID's from the Tab "Get Items ID" and paste in Tab "Move Items/Files"   
o   Select the option "Move Items/Files" and wait until the process is finish.



Boring Part: 
  • You can only move 1000 documents each time and that takes time...


Reason and Tests associated:

If you Query a List with more then 5000 items, the list view threshold of SharePoint will block and provide a error Message.

This issue was very well manage in this App using the SharePoint 2010 REST call "_vti_bin/ListData.svcto the Res(T)cue.

But made some tests using the CAML and REST (the new _api from 2013) using JSOM to get other possible solutions, but the results weren't good...

Here are the results.

CAML

CAML with Filter:

This CAML Query will return the threshold  error  because the Document Library has more then 5000 item, value defined by default in the Web Application threshold option "even when i call only 1000 items".

Example:
camlQuery.set_viewXml('<View Scope=\'RecursiveAll\'><Query><Where><And><Eq><FieldRef Name=\'FSObjType\' /><Value Type=\'int\'>0</Value></Eq><Eq><FieldRef Name=\'FileDirRef\' /><Value Type=\'Lookup\'>'/sites/Dev/Document/Example'</Value></Eq></And></Where></Query><ViewFields><FieldRef Name=\'ID\' /><FieldRef Name=\'FileDirRef\' /></ViewFields> <RowLimit>1000</RowLimit></View>');

Error message:

"The attempted operation is prohibited because it exceeds the list view threshold enforced by the administrator."


Output Image:


CAML noFilter:

This CAML Query return the first 5000 items but it is not possible make filters.
This is not good because if you have different folders, you aren't sure about the values that are fetched. 
If you try to make any type of filter will return the threshold error.

Example work for 5000:
camlQuery.set_viewXml('<View Scope=\'RecursiveAll\'><Query><Where></Where></Query><RowLimit>5000</RowLimit></View>');

Error:
If you try to include a folder url will be consider as filter and then returna "threshold" error.
camlQuery.set_folderServerRelativeUrl('/sites/Dev/Document/Example');


Output Image:


REST

After testing the CAML Query, the same Test's were made using REST url's and you will see the same behavior in this methods 

REST by Folder

"/sites/Dev/_api/SP.AppContextSite(@target)/web/getFolderByServerRelativeUrl('/sites/Dev/Document/Example')/Files?@target='hostweburl'

Output Image:


REST with Filter

/sites/Dev/_api/SP.AppContextSite(@target)/web/lists/getbytitle('Document')/Items?$top=5000&$filter=(FileDirRef eq '/sites/Dev/Document/Example')&@target='hostweburl'

Output Image:



REST All Items 


This REST URL Query return 5000 Items from a Document Library but with no filter associated.

/sites/Dev/_api/SP.AppContextSite(@target)/web/lists/getbytitle('Document')/Items?$top=5000&@target='hostweburl'

Output Image:




REST ListData.svc

After this Tests the only one that was able to make query in Folder with more then 5000 items was the _vti_bin/ListData.svc/(documentLibrary) and (query associated).
After this the app was able to call the Document Library Data in a specific Filter.
For this case was use the following filter:
 $filter=((Path eq '[Url to Folder]') and (ContentType ne 'Folder'))





ECMAScript Methods:

Get the ID's of a Folder with more then 5000 Documents 
The Max REST call is 1000 items)

Get ID of Documents in Document Library Method
executor.executeAsync(
      {
          url:
              appweburl + "/../_vti_bin/ListData.svc/" + Document + "?$top="+item+"&$select=Id&$filter=((Path eq '" + FolderFilter + "') and (ContentType ne 'Folder'))"
              ,
              method: "GET",
          headers: { "Accept": "application/json; odata=verbose" },
          success: function (data) {     
              var jsonObject = JSON.parse(data.body);
              var results = jsonObject.d;
              if (results.length > 0) {
                  onDataReturned(results);
              }
              else {
                  CloseWaitForm();
                  alert("No result!!");
              }
          },
          error: function (xhr) {
              CloseWaitForm();
              alert(xhr.status + ': ' + xhr.statusText);
          }
      }


Open Folder Tree view Method

function LaunchTargetPicker(TextBoxId) {

    var callback = function (dest) {
        if (dest != null && dest != undefined && dest[3] != null) {
 //return the Folder URL
            $("#" + TextBoxId).val($("#IdLists option:selected").val().split(";")[0] + dest[3]);
        }
    };

    var iconUrl = "/_layouts/15/images/smt_icon.gif?rev=32";
    SP.SOD.executeFunc('pickertreedialog.js', 'LaunchPickerTreeDialogSelectUrl', function () {
//Open the Treview in the Document Library URL
        LaunchPickerTreeDialogSelectUrl('CbqPickerSelectListTitle', 'CbqPickerSelectFolderText', 'websListsFolders', $("#IdLists option:selected").val().split(";")[1], appweburl + "/../", $("#FolderFilter").val(), '', '', iconUrl, '', callback, 'true', '');
    });

}



Move Documents Method
function MoveItems() {
    Items = $('#CopyItemID').val().split(";");
    CountMov = 0;

    context = new SP.ClientContext(appweburl);
    var factory = new SP.ProxyWebRequestExecutorFactory(appweburl);
    context.set_webRequestExecutorFactory(factory);
    app = new SP.AppContextSite(context, hostweburl);
    var site = app.get_web();
    var list = site.get_lists().getByTitle($("#IdLists option:selected").text());
    currentItem = list.getItemById(Items[CountMov]);
    file = currentItem.get_file();
    context.load(currentItem, 'File.Name');

    context.executeQueryAsync(MoveFileHandler, onQueryFailed);
}

function MoveFileHandler() {
    try {
        if (file != null) {
            var _destinationlibUrl;
            if ($('#DestinationFolder').val().slice(-1) == "/")
            { _destinationlibUrl = $('#DestinationFolder').val() + file.get_name(); }
            else {
                _destinationlibUrl = $('#DestinationFolder').val() +"/"+ file.get_name();
            }
            file.moveTo(_destinationlibUrl, SP.MoveOperations.allowBrokenThickets);
            context.executeQueryAsync(MoveSuccess, onQueryFailed);
        }
    } catch (e) {
        alert(e.message);
    }

}


Get Folders Method

function GetFolders() {
    $('#Audit').html('');
   
    context = new SP.ClientContext(appweburl);
    var factory = new SP.ProxyWebRequestExecutorFactory(appweburl);
    context.set_webRequestExecutorFactory(factory);
    app = new SP.AppContextSite(context, hostweburl);
   
    var site = app.get_web();
    var list = site.get_lists().getByTitle($("#IdLists option:selected").text());
    folderCollection = list.get_rootFolder();
    context.load(folderCollection);
    context.executeQueryAsync(successHandler, onQueryFailed);

}
function successHandler(sender, args) {
        recursiveMethod(folderCollection);
}

The call the folder in a Recursive way. (back to school...)
I recommend to call the Folders in a recursive way, this avoid possible issues with the limit of the List View.
The recursive Method for folders with more than 5000 Documents was the only one that worked for me, even with this, it is not possible to get sub Folders from a Folder with more then 5000 Document using ECMAScript of Client Object Model.



Here is the Link for the Solution.


To resolve the threshold issue you can index the Columns in the List and use the 
http://blogs.msdn.com/b/sowmyancs/archive/2013/07/31/sharepoint-list-throttling-amp-indexed-columns.aspx
In the CAML Query you can use the normal Query but take in consideration the Orderby option with the parameter "UseIndexForOrderBy"
OrderBy Element
https://msdn.microsoft.com/en-us/library/office/ms467378.aspx

I hope you like this new article.

Support Links:
SharePoint 2013 Boundaries
Working with List View Thresholds in SharePoint 2013

SharePoint 2013 changing the list view limit