Second Life of a Hungarian SharePoint Geek

March 26, 2018

Querying SharePoint list items including their attachment in a single request from the JavaScript Client Object Model

Filed under: CAML, ECMAScript Client Object Model, JSCOM, SP 2013 — Tags: , , , — Peter Holpar @ 23:31

Assume there is a SharePoint list including a few hundreds of items, each item might have a few attachments, but there might be items without attachments as well. We need to display only some of the items (filtered dynamically based on specific conditions) for the users on a web page. We use already the JavaScript client object model on that page, so it would be ideal to keep this technology, instead of involving another one (like REST or SharePoint web services) just because this task.

I found several useful pages while looking for a solution for my problem, like this one about using CAML from the client object model, although some of the statements in the sections “Include attachment URLs” and “Limitations” turned to be a bit misleading later.

Few posts and forum answers suggested to query the items first using a CAML query, then read the attachments for each matching item in subsequent requests individually. That would not perform very well if there are a lot of matching item and is generally not a nice solution. Another suggestion I often found is to query the items with a CAML query (just like in the previous case), then query all of the attachments (they are stored in a folder called Attachments n the list with a separate subfolder, like 1, 2, etc. for each list items) in a second query, then match the attachments files to items based on the relative path of the attachments (attachment for the list item with ID 1 are in the folder /Attachments/1, etc.). If you have a lot of items having a few attachments for each item, that might have performance problem as well, and the implementation itself is challenging.

Vadim Gremyachev posted a solution using the managed client object model with a single request only on SharePoint Stack Exchange. In his solution, he uses the following line to create an execute the CAML query:

var items = list.GetItems(CamlQuery.CreateAllItemsQuery());

The static CreateAllItemsQuery method of the CamlQuery object is only an easy way to create a CAML query having the RecursiveAll view scope, it means, content of the subfolders (if there is any) would be searched through either.

public static CamlQuery CreateAllItemsQuery()
{
    return new CamlQuery { ViewXml = "<View Scope=\"RecursiveAll\">\r\n    <Query>\r\n    </Query>\r\n</View>" };
}

As we have a flat list, without any folders, I’ve slightly modified Vadim’s code first:

  1. var siteUrl = "http://YourSharePoint/site/subsite&quot;;
  2. using (var ctx = new ClientContext(siteUrl))
  3. {
  4.     var web = ctx.Web;
  5.     var list = web.Lists.GetByTitle("TitleOfYourList");
  6.     // Vadim's version fo a list with a folder struture
  7.     //var items = list.GetItems(CamlQuery.CreateAllItemsQuery());
  8.     // my version for a flat list without folders
  9.     var items = list.GetItems(new CamlQuery());
  10.     ctx.Load(items, icol => icol.Include(i => i["Title"], i => i.AttachmentFiles));
  11.     ctx.ExecuteQuery();
  12.  
  13.     //Print attachment Urls for list items
  14.     foreach (var item in items)
  15.     {
  16.         if (item.AttachmentFiles.Count > 0)
  17.         {
  18.             Console.WriteLine("{0} attachments:", item["Title"]);
  19.             foreach (var attachment in item.AttachmentFiles)
  20.             {
  21.                 Console.WriteLine(attachment.ServerRelativeUrl);
  22.             }
  23.         }
  24.         else
  25.         {
  26.             Console.WriteLine("No attachments were found for list item '{0}' ", item["Title"]);
  27.         }
  28.     }
  29. }

It resulted in the following request sent to the server, captured by Fiddler:

image

Note, that only the properties Title and AttachmentFiles were requested in the Include statement, and the ViewXml is empty.

We wanted to reproduce the same request using the JavaScript object model, but haven’t found any solution immediately ready to use, so first made a quick check, which alternative solutions are available in JavaScript, if we were not able to transform the managed code sample to JavaScript. Let’s see what we found.

I found a jQuery-based approach here, that utilizes the GetListItems method of the Lists SharePoint web service:

  1. $("table.ms-listviewtable tr:has(td.ms-vb2)").find("td:first").filter(function() {
  2.     return $(this).text().toLowerCase() == ID;
  3. }).html("<img src='" + url[1] + "' width=150 height=100 />");
  4.  
  5. $(document).ready(function() {
  6.     querySPWebServices();
  7. });
  8. function querySPWebServices() {
  9.     var soapEnv = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'&gt; \\par         <soapenv:Body> \\par         <GetListItems xmlns='http://schemas.microsoft.com/sharepoint/soap/'&gt; \\par         <listName>Projects</listName> \\par         <viewFields> \\par         <ViewFields> \\par         <FieldRef Name='Title' /> \\par         <FieldRef Name='Body' /> \\par         <FieldRef Name='ID' /> \\par         <FieldRef Name='Attachments' /> \\par         </ViewFields> \\par         </viewFields> \\par         <query> \\par         <Query /> \\par         </query> \\par         <queryOptions> \\par         <QueryOptions> \\par         <IncludeAttachmentUrls>TRUE</IncludeAttachmentUrls> \\par         </QueryOptions> \\par         </queryOptions> \\par         </GetListItems> \\par         </soapenv:Body> \\par         </soapenv:Envelope>";
  10.     $.ajax({
  11.         async: false,
  12.         url: "http://server.name.com/site/_vti_bin/lists.asmx&quot;,
  13.         type: "POST",
  14.         dataType: "xml",
  15.         data: soapEnv,
  16.         complete: processResult,
  17.         contentType: "text/xml; charset=\"utf-8\""
  18.     });
  19. };
  20.  
  21. function processResult(xData, status) {
  22.     $(xData.responseXML).find("z\\:row").each(function() {
  23.         if ($(this).attr("ows_Attachments") != 0) {
  24.             var url = $(this).attr("ows_Attachments").replace(/#/g, "").split(';');
  25.             var ID = $(this).attr("ows_ID");
  26.             $("table.ms-listviewtable tr:has(td.ms-vb2)").find("td:first").filter(function() {
  27.                 return $(this).text().toLowerCase() == ID;
  28.             }).html("<img src='" + url[1] + "' width=150 height=100 />");
  29.         };
  30.     });
  31. };

Another solution invoking the GetListItems method as well, but this time using SPServices can be found here:

  1. var p = $().SPServices({
  2.     operation: "GetListItems",
  3.     listName: "listname",
  4.     CAMLQueryOptions: "<QueryOptions>" +
  5.             "<IncludeAttachmentUrls>TRUE</IncludeAttachmentUrls>" +
  6.         "</QueryOptions>"
  7. });
  8.   
  9. p.done(function() {
  10.   
  11.     $(p.responseXML).).SPFilterNode("z:row").each(function() {
  12.       
  13.         var attachments = [];
  14.   
  15.         var att = $(this).attr("ows_Attachments");
  16.         if(att !== "0") {
  17.             attachments = att.split(";#"); // Now you'll have an array of attachment URLs
  18.         } else {
  19.             // att will be "0", indicating that there are no attachments
  20.         }
  21.       
  22.     });
  23.   
  24. });

You could query list items with their attachments via the REST interface as well:

http://YourSharePoint/site/subsite/_api/Web/Lists/GetByTitle('TitleOfYourList')/Items/?$expand=AttachmentFiles

One can use CAML in conjunction with REST by using POST requests and the GetItems method as described here, but as far as I see, the $expand=AttachmentFiles plays not well with the GetItems method. Instead of CAML, you can add the condition, ordering and row limit attributes of your query via $filter, $orderby and $top to the REST query as described here.

After this short overview, let’s try to rewrite our C# code above in JavaScript.

First, I came up with a query like the one below. I requested the very same properties, that we had in the managed client object version: the Title and AttachmentFiles. Note, that I omitted all of the filters I needed in the case of real life list from the CAML query due to the readability as this part is not relevant now in solving the main problem. In contrast to the managed client object code above, I restricted the fields in the CAML query as well via the ViewFields element. That was a mistake, as it turned out later, but I’m a strong believer, that mistakes are there to teach us something new.

  1. var list = web.get_lists().getByTitle("TitleOfYourList");
  2.  
  3. var camlQuery = new SP.CamlQuery();
  4. var query = '<View><ViewFields><FieldRef Name="Title"/><FieldRef Name="AttachmentFiles"/></ViewFields></View>';
  5. camlQuery.set_viewXml(query);
  6.  
  7. var items = list.getItems(camlQuery);
  8. ctx.load(items, 'Include(Title, AttachmentFiles)');

The result was not satisfying, the request failed (screenshot from Fiddler):

image

ErrorMessage: Value does not fall within the expected range.

ErrorTypeName: System.ArgumentException

ErrorStackTrace:

Microsoft.SharePoint.SPFieldMap.GetColumnNumber(String strFieldName, Boolean bThrow)
   at Microsoft.SharePoint.SPListItemCollection.GetColumnNumber(String groupName, Boolean bThrowException)
   at Microsoft.SharePoint.SPListItem.GetValue(SPField fld, Int32 columnNumber, Boolean bRaw, Boolean bThrowException)
   at Microsoft.SharePoint.SPListItem.get_Attachments()
   at Microsoft.SharePoint.SPAttachmentCollection_Client.<GetEnumerator>d__0.MoveNext()
   at Microsoft.SharePoint.Client.ServerStub.<EnumerateChildItems>d__36.MoveNext()
   at Microsoft.SharePoint.Client.ServerStub.WriteChildItems(JsonWriter writer, Object obj, ClientObjectQuery objectQuery, ProxyContext proxyContext)
   at Microsoft.SharePoint.Client.ServerStub.WriteAsJson(JsonWriter writer, Object obj, ClientObjectQuery objectQuery, ProxyContext proxyContext)
   at Microsoft.SharePoint.Client.ServerStub.WriteAsJsonWithMonitoredScope(JsonWriter writer, Object value, ClientObjectQuery objectQuery, ProxyContext proxyContext)
   at Microsoft.SharePoint.ServerStub.SPListItemServerStub.WriteOnePropertyValueAsJson(JsonWriter writer, Object target, ClientQueryProperty field, ProxyContext proxyContext)
   at Microsoft.SharePoint.Client.ServerStub.WriteAsJson(JsonWriter writer, Object obj, ClientObjectQuery objectQuery, ProxyContext proxyContext)
   at Microsoft.SharePoint.Client.ServerStub.WriteChildItems(JsonWriter writer, Object obj, ClientObjectQuery objectQuery, ProxyContext proxyContext)
   at Microsoft.SharePoint.Client.ServerStub.WriteAsJson(JsonWriter writer, Object obj, ClientObjectQuery objectQuery, ProxyContext proxyContext)
   at Microsoft.SharePoint.Client.ServerStub.WriteAsJsonWithMonitoredScope(JsonWriter writer, Object value, ClientObjectQuery objectQuery, ProxyContext proxyContext)
   at Microsoft.SharePoint.Client.ClientMethodsProcessor.WriteQueryResults(Object obj, ClientObjectQuery objQuery)
   at Microsoft.SharePoint.Client.ClientMethodsProcessor.ProcessQuery(XmlElement xe)
   at Microsoft.SharePoint.Client.ClientMethodsProcessor.ProcessOne(XmlElement xe)
   at Microsoft.SharePoint.Client.ClientMethodsProcessor.ProcessStatements(XmlNode xe)
   at Microsoft.SharePoint.Client.ClientMethodsProcessor.Process()

You can see the getter method of the Attachments property of the SPListItem object in the fourth line of the stack trace. Let’s have a quick view, how this method works:

  1. public SPAttachmentCollection get_Attachments()
  2. {
  3.    if (this.m_Attachments == null)
  4.    {
  5.        string pbstrUrlPrefix = null;
  6.        object[,] objArrAttachments = null;
  7.        uint pdwRowCount = 0;
  8.        bool bDoQuery = false;
  9.        if (!this.HasExternalDataSource && (this.ID != 0))
  10.        {
  11.            object obj3;
  12.            object obj2 = this.GetValue("Attachments");
  13.            if (obj2 != null)
  14.            {
  15.                bDoQuery = obj2.ToString() != "0";
  16.            }
  17.            SPSecurity.SetListInHttpContext(HttpContext.Current, this.m_Items.List.InternalName);
  18.            SPWeb web = this.Web;
  19.            web.Request.GetAttachmentsInfo(web.Url, this.m_Items.List.InternalName, Convert.ToInt32(this.ID), bDoQuery, out pbstrUrlPrefix, out pdwRowCount, out obj3);
  20.            if (bDoQuery)
  21.            {
  22.                objArrAttachments = (object[,]) obj3;
  23.            }
  24.        }
  25.        this.m_Attachments = new SPAttachmentCollection(this, pbstrUrlPrefix, pdwRowCount, objArrAttachments);
  26.    }
  27.    return this.m_Attachments;
  28. }

As we’ve learnt from Karine’s post, the Attachments field is a flag indicating if a list item has any attachments or not. Obviously, the getter method depends on this property (see the line object obj2 = this.GetValue("Attachments"); in code above), and we should request this property explicitly in our code.

That is the new version of my query:

  1. var camlQuery = new SP.CamlQuery();
  2. var query = '<View><ViewFields><FieldRef Name="Title"/><FieldRef Name="Attachments"/><FieldRef Name="AttachmentFiles"/></ViewFields></View>';
  3. camlQuery.set_viewXml(query);
  4.  
  5. var items = list.getItems(camlQuery);
  6. ctx.load(items, 'Include(Title, Attachments, AttachmentFiles)');

The request was captured by Fiddler as shown bellow. See the properties included in the Include statement, as well the ViewXml:

image

This version works already as expected, but comparing the request with the one captured at C#-based version (see above) I found, that one can omit the Attachments field from the Include statement without any problem.

  1. var camlQuery = new SP.CamlQuery();
  2. var query = '<View><ViewFields><FieldRef Name="Title"/><FieldRef Name="Attachments"/><FieldRef Name="AttachmentFiles"/></ViewFields></View>';
  3. camlQuery.set_viewXml(query);
  4.  
  5. var items = list.getItems(camlQuery);
  6. ctx.load(items, 'Include(Title, AttachmentFiles)');

Furthermore, there is no problem, if you omit the ViewFields element entirely from the CAML query (in our case, we needed later other parts of the CAML query, like the Where element to build up the filters, but it is irrelevant now). That is actually the same query we had with the managed client object model sample at the beginning of this post.

  1. var list = web.get_lists().getByTitle("TitleOfYourList");
  2.  
  3. var camlQuery = new SP.CamlQuery();
  4.  
  5. var items = list.getItems(camlQuery);
  6. ctx.load(items, 'Include(Title, AttachmentFiles)');

Note: Using of the ViewFields element might have an effect on the performance of the query on the server side (especially if you have a lot of columns in your list and you need only a few of them) and so on the response time as well, but after the query has been completed on the server, it seems to be irrelevant regarding to the network bandwidth usage.

That is the full version of the code (to be exact, one of the working ones), including displaying the information (this time in the console only) returned by the server:

  1. var ctx = SP.ClientContext.get_current();
  2. var web = ctx.get_web();
  3.  
  4. var list = web.get_lists().getByTitle("TitleOfYourList");
  5.  
  6. var camlQuery = new SP.CamlQuery();
  7.  
  8. var items = list.getItems(camlQuery);
  9. ctx.load(items, 'Include(Title, Attachments, AttachmentFiles)');
  10.  
  11. ctx.executeQueryAsync(
  12.     function () {
  13.         var enumerator = items.getEnumerator();
  14.         while (enumerator.moveNext()) {
  15.             var item = enumerator.get_current();
  16.             console.log(item.get_item('Title'));
  17.             if (item.get_item('Attachments')) {
  18.                 var attEnumerator = item.get_attachmentFiles().getEnumerator();
  19.                 while (attEnumerator.moveNext()) {
  20.                     var attachment = attEnumerator.get_current();
  21.                     console.log(attachment.get_serverRelativeUrl()); // or get_fileName()
  22.                 }
  23.             }
  24.         }
  25.     },
  26.     function (sender, args) {
  27.         console.log('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
  28.     }
  29. );

That means, we fulfilled our goal by returning the items and their attachments in a single request/response via the JavaScript client object model.

Note, that after the response returned, we first check if there are any attachments for the item by reading the value of item.get_item(‘Attachments’), then accessing the attachments via the item.get_attachmentFiles() method. You could, however, check the existence of the attachments simply by using the get_attachmentFiles() method, without the Attachments field, like this:

var af = item.get_attachmentFiles();
if (af) {
    var attEnumerator = af.getEnumerator();

After extending the CAML query with the filters I need in the Where element, the query returned only the filtered items without any negative effect of the existing functionality, so that part of the task was completed either.

Lessons learned: If you need to read the attachments of your items, you must include the AttachmentFiles field in your Include statement. Including the Attachments field in the Include statement is optional, you need it only if you would like to use it explicitly (see the condition if (item.get_item(‘Attachments’)) in the last code snippet), otherwise you can omit it. If you would like to restrict the fields via the ViewFields element of your CAML query, you should include both of the Attachments and AttachmentFiles fields, but if you don’t restrict the columns by the usage of a ViewFields element, it works as well.

One final note: As so many times before, Fiddler was an invaluable tool to understand how the various versions work, or actually how they don’t work, by comparing the requests sent by the codes down to the wire.

Advertisements

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: