Wednesday, December 3, 2014

Adding a new version in version collection of Document set using SharePoint 2013 CSOM & JSOM

Hi,
I had requirement to add new version to document set using SharePoint 2013  CSOM (Client side object Model) & JSOM (Javacript side object Model). CSOM & JSOM doesn't have any such API to add a version to Document Set directly. Also, I have to achieve this functionaliy in Sharepoint hosted App (for Sharepoint Online).
Below is the server side Code that I have to achieve using CSOM & JSOM.
web.AllowUnsafeUpdates = true;
listItem.Update();
DocumentSet documentSet = DocumentSet.GetDocumentSet(listItem.Folder);
documentSet.VersionCollection.Add(true, currentUser.Name);
web.AllowUnsafeUpdates = false;

Solution :- 
With absence of any proper APIs in CSOM & JSOM, a possible workaround was that monitor the POST request using Fiddler when clicking the "Capture Version" button in the ribbon on Document Set welcome page, then Create a custom POST request to imitate the Original "Capture Version" action.
Therefore, I have achieved this functionality by creating custom POST request to imitate the Original "Capture Version" Action. On click of "Capture Version" button in the ribbon it creates a Post Request on "/_layouts/15/CreateDocSetVersion.aspx" Page along with ListId & ItemId (i.e. Document Set Item of whom version need to be captured) in Query String Parameter "List={listguidid}&ID=docsetitemid".
 Also, It passes "CreateComments={docsetversioncomments}" in the post request body to set document set version comments.
Reference :- https://social.msdn.microsoft.com/Forums/office/en-US/2b3d11d0-05ee-4e53-91bc-66362a0751ef/adding-a-new-version-in-version-collection-of-document-set-using-sharepoint-2013-jsom?forum=sharepointdevelopment
CSOM
Using CSOM API by creating custom POST request to Capture Document Set Version. 
var CreateDocSetVersionUrl = SiteURL + "/_layouts/15/CreateDocSetVersion.aspx?List={" + spListID + "}&ID=" + spListItemID;

System.Net.CookieContainer cookieContainer = new System.Net.CookieContainer();
// create Web Request using client context
HttpWebRequest request = clientContext.WebRequestExecutorFactory.CreateWebRequestExecutor(clientContext, CreateDocSetVersionUrl).WebRequest;

if (clientContext.Credentials != null)
{
 // Get Authentication Cookie
 SecureString passWord = new SecureString();
 foreach (char c in UserPassword.ToCharArray()) passWord.AppendChar(c);

 var credentials = new Microsoft.SharePoint.Client.SharePointOnlineCredentials(UserName, passWord);
 var authCookieValue = credentials.GetAuthenticationCookie(new Uri(CreateDocSetVersionUrl));

 // Create fed auth Cookie
 System.Net.Cookie fedAuth = new Cookie();
 fedAuth.Name = "FedAuth";
 fedAuth.Value = authCookieValue.TrimStart(new char[] { 'S', 'P', 'O', 'I', 'D', 'C', 'R', 'L', '=' });
 fedAuth.Path = "/";
 fedAuth.Secure = true;
 fedAuth.HttpOnly = true;
 fedAuth.Domain = new Uri(clientContext.Url).Host;

 // Hookup authentication cookie to request
 cookieContainer.Add(fedAuth);

 request.CookieContainer = cookieContainer;
}
else
{
 // No specific authentication required
 request.UseDefaultCredentials = true;
}

request.ContentLength = 0;
WebResponse response = request.GetResponse();


// decode response
string strResponse;
Stream stream = response.GetResponseStream();
if (!string.IsNullOrEmpty(response.Headers["Content-Encoding"]))
{
 if (response.Headers["Content-Encoding"].ToLower().Contains("gzip"))
 {
  stream = new System.IO.Compression.GZipStream(stream, System.IO.Compression.CompressionMode.Decompress);
 }
 else if (response.Headers["Content-Encoding"].ToLower().Contains("deflate"))
 {
  stream = new System.IO.Compression.DeflateStream(stream, System.IO.Compression.CompressionMode.Decompress);
 }
}

// get response string
System.IO.StreamReader sr = new System.IO.StreamReader(stream);

strResponse = sr.ReadToEnd();

sr.Close();
sr.Dispose();

stream.Close();

// Look for inputs and add them to the dictionary for postback values
var inputs = new List<KeyValuePair<string, string>>();

string patInput = @"<input.+?\/??>";
string patName = @"name=\""(.+?)\""";
string patValue = @"value=\""(.+?)\""";

// Instantiate the regular expression object.
Regex r = new Regex(patInput, RegexOptions.IgnoreCase);
Regex rName = new Regex(patName, RegexOptions.IgnoreCase);
Regex rValue = new Regex(patValue, RegexOptions.IgnoreCase);

// Match the regular expression pattern against a text string.
Match m = r.Match(strResponse);
while (m.Success)
{
 string name = string.Empty;
 string value = string.Empty;
 if (rName.IsMatch(m.Value))
 {
  name = rName.Match(m.Value).Groups[1].Value;
 }

 if (rValue.IsMatch(m.Value))
 {
  value = rValue.Match(m.Value).Groups[1].Value;
 }

 if (string.IsNullOrEmpty(name))
 {
  m = m.NextMatch();
  continue;
 }

 var dict = new KeyValuePair<string, string>(name, value);
 inputs.Add(dict);

 m = m.NextMatch();
}


response.Close();
response.Dispose();

// Format inputs as postback data string
string strPost = "";
string strComments = "Doc set version comments";
bool IsCommentsExist = false;
foreach (KeyValuePair<string, string> inputKey in inputs)
{
 if (!string.IsNullOrEmpty(inputKey.Key) && inputKey.Key.EndsWith("CreateComments"))
 {
  IsCommentsExist = true;
  strPost += System.Uri.EscapeDataString(inputKey.Key) + "=" + System.Uri.EscapeDataString(strComments) + "&";
 }
 else if (!string.IsNullOrEmpty(inputKey.Key))
 {
  strPost += System.Uri.EscapeDataString(inputKey.Key) + "=" + System.Uri.EscapeDataString(inputKey.Value) + "&";
 }
}

if (!IsCommentsExist)
{
 // Set Document Set Version Comments
 strPost += System.Uri.EscapeDataString("CreateComments") + "=" + System.Uri.EscapeDataString(strComments) + "&";
}

strPost = strPost.TrimEnd(new char[] { '&' });

byte[] postData = System.Text.Encoding.UTF8.GetBytes(strPost);

// Build postback request
HttpWebRequest activateRequest = clientContext.WebRequestExecutorFactory.CreateWebRequestExecutor(clientContext, CreateDocSetVersionUrl).WebRequest;
activateRequest.Method = "POST";
activateRequest.Accept = "text/html, application/xhtml+xml, */*";
if (clientContext.Credentials != null)
{
 activateRequest.CookieContainer = cookieContainer;
}
else
{
 // No specific authentication required
 activateRequest.UseDefaultCredentials = true;
}
activateRequest.ContentType = "application/x-www-form-urlencoded";
activateRequest.ContentLength = postData.Length;
activateRequest.UserAgent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)";
activateRequest.Headers["Cache-Control"] = "no-cache";
activateRequest.Headers["Accept-Encoding"] = "gzip, deflate";
activateRequest.Headers["Accept-Language"] = "en-US";

// Add postback data to the request stream
stream = activateRequest.GetRequestStream();
stream.Write(postData, 0, postData.Length);
stream.Close();
stream.Dispose();

// Perform the postback
response = activateRequest.GetResponse();
response.Close();
response.Dispose();

Reference :- https://github.com/janikvonrotz/PowerShell-PowerUp/blob/master/functions/SharePoint%20Online/Switch-SPOEnableDisableSolution.ps1

JSOM
In JSOM, I have to achieve this on App Web Page of Sharepoint Hosted App. I have created one page "DocSet.aspx" to create custom post request to add document set version. This page takes "SPHostUrl=hosturl&SPAppWebUrl=appweburl&ItemID=itemid&ListID=listid&Comments=docsetversioncomments" following parameter from query string & create custom POST request to add Document set version.
DocSet.aspx
<%-- The following 4 lines are ASP.NET directives needed when using SharePoint components --%>
<%@ Page Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" MasterPageFile="~masterurl/default.master" Language="C#" %>
<%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>


<%-- The markup and script in the following Content element will be placed in the <head> of the page --%>
<asp:Content ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">
 <script type="text/javascript" src="../JS/jquery.js" ></script>
    <script type="text/javascript" src="/_layouts/15/MicrosoftAjax.js"></script>
    <SharePoint:ScriptLink ID="ScriptLink5" name="sp.js" runat="server" LoadAfterUI="true" Localizable="false" />
    <SharePoint:ScriptLink ID="ScriptLink6" name="sp.runtime.js" runat="server" LoadAfterUI="true" Localizable="false" />
    <SharePoint:ScriptLink ID="ScriptLink7" name="sp.core.js" runat="server" LoadAfterUI="true" Localizable="false" />
    <SharePoint:ScriptLink ID="ScriptLink8" name="SP.RequestExecutor.js" runat="server" LoadAfterUI="true" Localizable="false" />
</asp:Content>

<%-- The markup in the following Content element will be placed in the TitleArea of the page --%>
<asp:Content ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea" runat="server">
 Page Title
</asp:Content>

<%-- The markup and script in the following Content element will be placed in the <body> of the page --%>
<asp:Content ContentPlaceHolderID="PlaceHolderMain" runat="server">
<WebPartPages:AllowFraming runat="server" />

 <table class="ms-formtable" border="0" cellspacing="0" width="100%">
        <tr>
            <td>
                &nbsp;
            </td>
        </tr>
    </table>
 <span id="errorMessage"></span>
 
 <script type="text/javascript">
  
        var errorMessage = '';
        function LoggerLog(logMessage) {
            errorMessage += '<br/>' + logMessage;
            $('#errorMessage').html(errorMessage);
        }
  var hostUrl;
  var appWebUrl;               
        var context;
        var spListID;
  var spListItemID;
  var Comments;

        function getParameterByName(name) {
            name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
            var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
                results = regex.exec(location.search);
            return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
        }
  
        LoadProperties();        
  
  String.prototype.endsWith = function(suffix) {
   return this.indexOf(suffix, this.length - suffix.length) !== -1;
  };
  
   if (this.length == 0)
   return this;
   c = c ? c : ' ';
   var i = 0;
   var val = 0;
   for (; this.charAt(i) == c && i < this.length; i++);
   return this.substring(i);
  }
  String.prototype.trimEnd=function(c)
  {
   c = c?c:' ';
   var i=this.length-1;
   for(;i>=0 && this.charAt(i)==c;i--);
   return this.substring(0,i+1);
  }
   
        function LoadProperties() {
            try {
                //debugger;
                hostUrl = getParameterByName('SPHostUrl');
                appWebUrl = getParameterByName('SPAppWebUrl');
                spListItemID = getParameterByName('ItemID');
                spListID = getParameterByName('ListID');
                Comments = getParameterByName('Comments');

                //debugger;
                if (spListItemID != '') {
                    context = new SP.ClientContext.get_current(); 
     
     // Add Document Set Version
     AddDocSetVersion();
    }
   }
            catch (ex) {
    //debugger;
                console.log(ex.toString());
                LoggerLog(ex.toString());
            }
        }
  
  function AddDocSetVersion()
        {
            console.log("UpdateLeadInfo Method  started..");

            try
            {
                //debugger;                
    var redirectUrl = hostUrl + '/_layouts/15/CreateDocSetVersion.aspx?List={' + spListID + '}&ID=' + spListItemID +'&IsDlg=1';

    var value = new SP.ClientRequest(context);
    var webRequest = value.get_webRequest();
    
    // Set the request verb.
    webRequest.set_httpVerb("GET");
    // Set the request Url.  
    webRequest.set_url(redirectUrl);  
    webRequest.get_headers()['Content-Length'] = 0;
    
    // Set the web request completed event handler, for processing return data.
    webRequest.add_completed(OnWebGetRequestCompleted);        
    
    // Execute the request.
    webRequest.invoke();  
        
                console.log("AddDocSetVersion Method  completed..");
            }
   catch (ex) {
    //debugger;
                console.log("Error occured in AddDocSetVersion : " + ex.toString());
                LoggerLog("Error occured in AddDocSetVersion : " + ex.toString());
            }
        }
  
  function OnWebGetRequestCompleted(executor, eventArgs)
  {
         //debugger;
    if(executor.get_responseAvailable()) 
       {
         var statusCode=executor.get_statusCode();  // STATUS CODE 204 MEANS ok, BUT NO DATA TO RETUIRN. ie CHANGES THIS TO 1223 AND DROPS ALL THE HEADERES
         var statusText=executor.get_statusText(); 
         var responseData=executor.get_responseData();
       var newHeaders=executor.getAllResponseHeaders();
     
     // Look for inputs and add them to the dictionary for postback values
     var inputs = [];

     var patInput = /<input.+?\/??>/ig;
     var patName = /name=\"(.+?)\"/i;
     var patValue = /value=\"(.+?)\"/i;
   
     while(res = patInput.exec(responseData)) {
      var name1 = '';
      var value1 = '';
      var InputTag = res[0];
      
      var resName = patName.exec(InputTag);
      if(resName !== null) {
       name1 = resName[1];
      }
      
      var resValue = patValue.exec(InputTag);
      if(resValue !== null) {
       value1 = resValue[1];
      }

      if (name1 == '')
      {
       continue;
      }
      
      inputs.push({key:name1,value:value1}); 
     }
     
     // Format inputs as postback data string, but ignore the one that ends with iidIOGoBack
     var strPost = "";
     var strComments = Comments;
     var IsCommentsExist = false;
     for(i=0;i<inputs.length;i++){     
      if (inputs[i].key != '' && inputs[i].key.endsWith("CreateComments"))
      {
       IsCommentsExist = true;
       strPost += encodeURIComponent(inputs[i].key) + "=" + encodeURIComponent(strComments) + "&";
      }
      else if (inputs[i].key != '')
      {
       strPost += encodeURIComponent(inputs[i].key) + "=" + encodeURIComponent(inputs[i].value) + "&";
      }      
     }
     
     
     if (!IsCommentsExist)
     {
      // Set Document Set Version Comments
      strPost += encodeURIComponent("CreateComments") + "=" + encodeURIComponent(strComments) + "&";
     }

     strPost = strPost.trimEnd('&');
     
     var postData = strPost;

     // Build postback request          
     var redirectUrl = hostUrl + '/_layouts/15/CreateDocSetVersion.aspx?List={' + spListID + '}&ID=' + spListItemID +'&IsDlg=1';

     var value = new SP.ClientRequest(context);
     var webRequest = value.get_webRequest();
     
     // Set the request verb.
     webRequest.set_httpVerb("POST");
     // Set the request Url.  
     webRequest.set_url(redirectUrl);  
     webRequest.set_body(postData);
     webRequest.get_headers()['Accept'] = "text/html, application/xhtml+xml, */*";
     webRequest.get_headers()['Content-Type'] = "application/x-www-form-urlencoded";
     webRequest.get_headers()['Content-Length'] = postData.length;
     webRequest.get_headers()['User-Agent'] = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)";
     webRequest.get_headers()['Cache-Control'] = "no-cache";
     webRequest.get_headers()['Accept-Encoding'] = "gzip, deflate";
     webRequest.get_headers()['Accept-Language'] = "en-US";
     
     // Set the web request completed event handler, for processing return data.
     webRequest.add_completed(OnWebPostRequestCompleted);        
     
     // Execute the request.
     webRequest.invoke();  
       }
  }
   
  function OnWebPostRequestCompleted(executor, eventArgs)
  {
         //debugger;
   if(executor.get_responseAvailable()) 
   {
    var statusCode=executor.get_statusCode();  // STATUS CODE 204 MEANS ok, BUT NO DATA TO RETUIRN. ie CHANGES THIS TO 1223 AND DROPS ALL THE HEADERES
    var statusText=executor.get_statusText(); 
    var responseData=executor.get_responseData();
    var newHeaders=executor.getAllResponseHeaders();
   }
  }
         
 </script>

</asp:Content>
then I have uploaded that page in "StyleLibrary" of Sharepoint online site.
After on App Web page, I have created "SPAppIframe" control to load above created "DocSet.aspx" page along with the query string to add Document Set Version for an document set item.
<SharePoint:SPAppIFrame ID="SPDocSetVersionCreation" Style="display: none !important;" runat="server" Src="" Width="100%" Height="100%"></SharePoint:SPAppIFrame>
Code to load "DocSet.aspx" page on App Web page dynamically using jquery.
<script type="text/javascript">
// Update Document Set Version
var Iframeurl = '';
Iframeurl += '&SPHostUrl=' + HostUrl + '&SPAppWebUrl=' + AppWebUrl + '&ItemID=' + ItemId + '&ListID=' + spListID + '&Comments=' + encodeURIComponent('comments');
$('#SPDocSetVersionCreation').attr('src', Iframeurl);
$('#SPDocSetVersionCreation').load( function () {
alert('Document set version added suceesfully !');
});
 </script>

6 comments:

  1. good info, nice to see you blogging about SharePoint

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Hi Nitinkumar,

    Thank you for the very helpful blogpost. I am still stuck with authenticating SharePoint Online for the WebRequest to create the document set. You use the ClientContext.Credentials but when using an app and its ContextToken, there is no explicit username/password. Also I don't want to use username/password, I want to do this through the app itself...
    Any idea on how to do that?

    ReplyDelete
  4. Nice post thks this helped a lot.
    There is also another way.
    1 - Create a 2010 workflow attached to the list.
    You can capture versions on the 2010 workflows.
    2- Post a message outside the iframe that redirects to that address.

    ReplyDelete