Wednesday 20 October 2010

SP2010 AJAX–Part 2: Using the JavaScript Client OM + jQuery to work with lists

  1. Boiling jQuery down to the essentials (technique)
  2. Using the JavaScript Client OM to work with lists (technique) - this article
  3. Using jQuery AJAX with a HTTP handler (technique)
  4. Returning JSON from a HTTP handler (technique)
  5. Enable Intellisense for Client OM and jQuery (tip)
  6. Debugging jQuery/JavaScript (tip)
  7. Useful tools when building AJAX applications (tip)
  8. Migrating existing applications to jQuery/AJAX

Having looked at essential jQuery techniques to update the page last time, now we start to talk to SharePoint. Of course, the Client Object Model is an obvious tool for developers looking to build AJAX applications on SharePoint 2010 – it’s whole purpose is to make it simpler to call server methods from JavaScript or Silverlight, or ‘offline’ .Net code in the case of the Managed Client OM, and work with the SharePoint in those environments. It does this by providing a layer which takes care of the complex bits of calling SharePoint web services from e.g. JavaScript. This means building SharePoint applications which aren’t posting back to the server/fully refreshing the page is easier than it would be otherwise. This articles walks through some examples and illustrates some important performance tips.

Note that this article focuses on the JavaScript/ECMAScript flavor of the Client OM. The other two flavors (Silverlight and Managed Code) have slightly different syntax, so if you are cross-referencing MSDN/other articles, make sure you’re looking at the right thing!

Some key things to consider if you’re looking at the Client OM for the first time:

  • Async programming model – the basic sequence of steps is to get a reference to the ClientContext (similar to SPContext), read/update data, then call ClientContext.ExecuteQueryAsync(). This last method is what triggers things to actually happen (i.e. the request to the server), and you pass 2 ‘callback’ method names to run on success/failure.
    • This is similar to other disconnected programming, and also jQuery’s AJAX methods which we’ll look at next article
  • Calling .Load() on objects you will use – although the SharePoint object hierarchy is intentionally the same as on the server, one difference is that you must call ClientContext.Load(myJavaScriptVariable) on every variable which represents a SharePoint object (site/web/list/list item/something else) you wish to use. This ensures the details for that object are passed to the client. However, we’ll also see later that you should *only* call ClientContext.Load() on objects you genuinely do need to.

Examples

  1.   Fetching some data (title of current web)

    A simple example to start with – we’ll retrieve the title of the current web in jQuery (a jQuery button click event) and display it in a JavaScript alert box. This is about as simple as it gets, which is perfect for illustrating the callback model. One important ‘pattern’ thing to notice is that we’re declaring a global JavaScript variable which gets loaded, and because it’s global is also available in the success callback: 

    ClientOM_Demo1_Off
    ClientOM_Demo1_On
    <fieldset id="fldDemo1">
        <legend>Demo 1 - fetching web title</legend>
        <div id="Div1" class="demoRow">
                <div class="demoControls">
                    <button id="btnDemo1" type="button">Fetch web title</button>
                </div>
        </div>
    </fieldset>
    <script type="text/javascript">
        var currentWeb;
     
        $('#btnDemo1').click(function () {
            var ctx = new SP.ClientContext.get_current();
     
            currentWeb = ctx.get_web();
            ctx.load(currentWeb);
     
            ctx.executeQueryAsync(getWebSuccess, getWebFailure);
        });
     
        function getWebSuccess(sender, args) {
            alert('Title of current web is: ' + currentWeb.get_title());
        }
     
        function getWebFailure(sender, args) {
            alert('Failed to get web. \nError: ' + args.get_message() + '\nStackTrace: ' + args.get_stackTrace());
        }
    </script>
  2.  Fetching items from a list/library

    Something we may definitely want to do at some point with the Client OM is retrieve the items in a list or library. The same pattern is used but we effectively navigate down the SharePoint hierarchy to the list in question, and then run a CAML query (no LINQ in the Client OM). Notice that there is no ‘indexer’ property to get a single list on the client – we call .get_lists() to return all the lists in the web, then call .getByTitle(‘My list name’) on the resulting collection – a good example of the occasional difference compared to the server-side object model. In the callback, we have a collection we can enumerate through, with each item having a property named ‘FileLeafRef’ (which you may have seen previously in CAML queries) for the filename:

    ClientOM_Demo2_Off
    ClientOM_Demo2_On
    <fieldset id="fldDemo2">
        <legend>Demo 2 - fetching list items</legend>
        <div id="demo2Row" class="demoRow">
                <div class="demoControls">
                    <button id="btnDemo2" type="button">Fetch items</button>
                </div>
                <div class="demoResults">
                    <span id="demo2Result" />
                </div>
                <div class="clearer" />
        </div>
    </fieldset>
    <script type="text/javascript">
        var allDocs;
     
        $('#btnDemo2').click(function () {
            var ctx = new SP.ClientContext.get_current();
     
            var targetList = ctx.get_web().get_lists().getByTitle('Shared Documents');
            var query = SP.CamlQuery.createAllItemsQuery();
     
            allDocs = targetList.getItems(query);
            ctx.load(allDocs);
     
            ctx.executeQueryAsync(Function.createDelegate(this, getDocsAllItemsSuccess), 
                Function.createDelegate(this, getDocsAllItemsFailure));
        });
     
        function getDocsAllItemsSuccess(sender, args) {
            var listEnumerator = allDocs.getEnumerator();
            while (listEnumerator.moveNext()) {
                $('#demo2Result').append(listEnumerator.get_current().get_item("FileLeafRef") + '<br />');
            }
        }
     
        function getDocsAllItemsFailure(sender, args) {
            alert('Failed to get list items. \nError: ' + args.get_message() + '\nStackTrace: ' + args.get_stackTrace());
        }
    </script>

    A couple of other things to notice about this example:

    - I’m using the slightly more complex syntax of Function.createDelegate(this, myFunctionName) with ClientContext.ExecuteQueryAsync(). The MSDN samples use this syntax, but I understand it’s not strictly necessary and does clutter the code somewhat. Certainly my testing shows that the code still works and the args/sender variables are still passed to the callbacks, which is something I’d wondered about.
    - The SP.CamlQuery.createAllItemsQuery() method provides a handy shortcut in the Client OM to creating a CAML query for all the items in the list.

  3.  Fetching list items with query (type-ahead)

    Let’s turn the last example into something more useful and cool – a list of items on the page which dynamically filters as you type the filename into a box. This is an extremely useful pattern to understand, and is essentially the same mechanism for many cool things you may have seen e.g. Google Instant Search. In fact we only need to change a couple of things to enable this – instead of a button click event we respond to the textbox’s keyup event, and instead of a CAML query which returns all items, we need a CONTAINS query which drops the value from the text box into the clause:

    ClientOM_Demo3_On1
    ClientOM_Demo3_On2
    <fieldset id="fldDemo3">
        <legend>Demo 3 - fetching list items with query</legend>
        <div id="demo3Row" class="demoRow">
                <div class="demoControls">
                    <label for="txtFilenameContains">Filename contains:</label>
                    <input type="text" id="txtFilenameContains" />
                </div>
                <div class="demoResults">
                    <span id="demo3Result" />
                </div>
                <div class="clearer" />
        </div>
    </fieldset>
    <script type="text/javascript">
        var selectedDocs;
     
        $('#txtFilenameContains').keyup(function (event) {
            filterDocs();
        });
     
        function filterDocs() {
            var ctx = new SP.ClientContext.get_current();
     
            var docLib = ctx.get_web().get_lists().getByTitle('Shared Documents');
            var query = new SP.CamlQuery();
            query.set_viewXml("<View><Query><Where><Contains><FieldRef Name='FileLeafRef'/><Value Type='Text'>" + $('#txtFilenameContains').val() + "</Value></Contains></Where></Query></View>");
     
            selectedDocs = docLib.getItems(query);
            ctx.load(selectedDocs);
     
            ctx.executeQueryAsync(getDocsWithQuerySuccess, getDocsWithQueryFailure);
        }
     
        function getDocsWithQuerySuccess(sender, args) {
            $('#demo3Result').empty();
            var listEnumerator = selectedDocs.getEnumerator();
            while (listEnumerator.moveNext()) {
                $('#demo3Result').append(listEnumerator.get_current().get_item("FileLeafRef") + '<br />');
            }
        }
     
        function getDocsWithQueryFailure(sender, args) {
            alert('Failed to get list items. \nError: ' + args.get_message() + '\nStackTrace: ' + args.get_stackTrace());
        }
    </script>

  4.  Add new list items to a list

    Adding new data to SharePoint generally revolves around creating a somethingCreationInformation object (e.g. WebCreationInformation, ListCreationInformation etc.) in JavaScript, populating values for the item, then calling .update() and .executeQueryAsync(). In this example I’m using the ListItemCreationInformation object to add a new item to the ‘Tasks’ list in the current web, based on a title entered into a textbox:

    ClientOM_Demo4_Off
    ClientOM_Demo4_On1
    ClientOM_Demo4_Result
    <fieldset id="fldDemo4">
        <legend>Demo 4 - add list items</legend>
        <div id="demo4Row" class="demoRow">
            <div><span class="demoLabel">Task title:</span><input id="txtTaskTitle" type="text" /></div>
            <div><button id="btnAddTask" type="button">Add task</button></div>
            <div><span class="demoLabel">Result:</span><span id="addResult" /></div>
        </div>
    </fieldset>
    <script type="text/javascript">
        var newTask;
     
        $('#btnAddTask').click(function () {
            var taskTitle = $('#txtTaskTitle').val();
            var taskDesc = $('#txtTaskDescription').val();
     
            var ctx = new SP.ClientContext.get_current();
            var taskList = ctx.get_web().get_lists().getByTitle('Tasks');
            // use ListItemCreationInformation to provide values..
            var taskItemInfo = new SP.ListItemCreationInformation();
            newTask = taskList.addItem(taskItemInfo);
            newTask.set_item('Title', taskTitle);
            // could set other fields here in same way..
            newTask.update();
     
            ctx.load(newTask);
            ctx.executeQueryAsync(addTaskSuccess, addTaskFailure);
     
            function addTaskSuccess(sender, args) {
                $('#addResult').html("Task " + newTask.get_item('Title') + " added to the Tasks list");
            }
     
            function addTaskFailure(sender, args) {
                alert('Failed to add new task. \nError: ' + args.get_message() + '\nStackTrace: ' + args.get_stackTrace());
            }
        });
    </script>

Performance/writing efficient Client OM code

Now that we understand the basics of the JavaScript Client OM, we should be aware that how we write the code can have a dramatic impact on performance. We mentioned earlier that you should only call ClientContext.Load() on objects you will actually use, and the always-excellent Steve Peschka mentions this in his series which focuses on the Managed Client OM. Essentially the more objects you call .Load() on, the more data goes over the wire to the client – my preferred way of looking at this is in Firebug (a Firefox add-on), but Fiddler works fine too. Both will show the JSON-formatted response (N.B. JSON is something I discuss later in this series):

ViewingJsonResponse
So what kind of things make a difference? Well, there are two main ones:

  • Calling .Load() on objects where it’s not needed
  • Returning more properties (e.g. fields for a list item) than are needed

For the first, consider that on our way to get some lists items we can write some code to get a list in two ways:

// bad way..
var web = ctx.get_web();
var lists = ctx.get_web().get_lists();
var targetList = lists.getByTitle('Shared Documents');
ctx.load(web);
ctx.load(lists);
ctx.load(targetList);
/* although we can't see the surrounding code, the 'web' and 'lists' objects 
   actually weren't used on the client for anything.. */
// better way:
var targetList = ctx.get_web().get_lists().getByTitle('Shared Documents');

In both cases, we would later have a ctx.load(listItems) line. However in the second case, the JSON for the web, lists and target list are not sent over the wire – only the JSON for actual list items. This will cut down the data significantly (I’ll show numbers later).

For the second (returning more properties for an object than are needed), this is obviously analagous to a SQL SELECT * vs SELECT [mySingleColumn] query. We haven’t yet shown how to filter the properties returned, but it’s very important – first let’s look at what we have been doing so far:

var query = SP.CamlQuery.createAllItemsQuery();
demo5listItems = targetList.getItems(query);
// bad way - requesting all properties here..
ctx.load(demo5listItems);

The syntax for the limiting the fields returned in such a query in the JavaScript Client OM looks like this – here I’m just getting the filename only:

var query = SP.CamlQuery.createAllItemsQuery();
demo5listItems = targetList.getItems(query);
// better way:
ctx.load(demo5listItems, 'Include(FileLeafRef)');

When I was testing the different code patterns, this is what I saw – note I was only testing on a query which returned 8 items:

image

My observations from this are:

  • None of these numbers are scary, but then again we’re talking a mere 8 items here! However, the proportions are very interesting, and if (say) 100 items were being returned then clearly the amount of data would mean big benefits from writing code the right way
  • Proportions - the largest data set is 15 times the size of the smallest
  • The automatic compression in the Client OM saves your ass. The ‘Uncompressed size’ column is technically irrelevant as data is always compressed over the wire – however at some point it exists decompressed, and we can see how big the data really is here (61KB for 8 list items would be very bad indeed).

So to reiterate, only call ClientContext.Load() on objects you need to and be sure to restrict the properties returned. Hopefully though you can see that the Client OM and jQuery are great tools for building SharePoint apps which aren’t postback hell.

Next time: Using jQuery AJAX with a HTTP handler

4 comments:

Andy Buns said...

You mention compression - is that web server compression, or does the client object model do something of it's own? (I can't imagine decompression in JavaScript being very good)

Chris O'Brien said...

@Andy,

It's a good question, and I don't know the details. I *think* it's a function of the Client.svc WCF service (i.e. done in code inside the implementation), although I notice IIS compression is on by default for the hosting directory.

In any case, I'd bet that having the client need to decompress the payload is ultimately better than have bigger volumes of data go over the wire. Certainly my example of filtering a list as you type reacts quick enough, and that has a call on each keystroke.

Cheers,

Chris.

Anonymous said...

Hey, Chris. I had someone mention your compression comment here in a thread on the SPServices site. I'm not sure what you're referring to. Are you just tlaking about GZIP or is there something else going on in Client OM traffic? I don't have a simple way to test it.

Thanks,
M.

Chris O'Brien said...

Hey Marc,

I just checked, and it is in fact native browser GZIP compression. You can tell by isolating the response from a client OM call in Fiddler/Firebug and looking at the Content-Encoding.

Thanks for the prompt, I've always meant to look at that ;)

HTH,

Chris.