viernes, 1 de agosto de 2014

Using promises to simplify javascript asynchronous development in SharePoint

SharePoint APIs have been dramatically improved over the last few years, especially when it comes to javascript but, even with those improvements, it’s sometimes very hard for developers with backend background to achieve goals that initially seem simple. To the fact that we don’t have (or we don’t know how to use) good development tools to help us with developing and debugging javascript code, we need to add the way you normally interact with SharePoint objects. Let me show you this with an example:

Imagine I want to show a list with elements coming from a taxonomy termset. Basically I would like to be able to query the site collection default term store and, by calling a method passing the name of the termset containing the items, retrieve the list of terms, iterate through them and fill my html control. To make it a little more complex, imagine we have actually two different html controls to fill with two different termset terms.

The first problem you are going to face when trying to implement the scenario above is the fact that SharePoint doesn’t provide you a method to retrieve the terms of a termset of the default group of the site collection’s default termstore. You will need to make a list of request to get the data, starting with this:

Code Snippet
  1. var getGroups = function () {
  2.     var clientContext = SP.ClientContext.get_current();
  3.     var taxSession = SP.Taxonomy.TaxonomySession.getTaxonomySession(clientContext);
  4.     var termStore = taxSession.getDefaultSiteCollectionTermStore();
  5.  
  6.     var groups = termStore.get_groups();
  7.     clientContext.load(groups);
  8.  
  9.     clientContext.executeQueryAsync(function () {
  10.  
  11.         // Add here code to execute when groups are loaded
  12.  
  13.     }, function (sender, args) {
  14.  
  15.         console.log(args.get_message());
  16.  
  17.     });
  18. }

Then, in line 11 you will need to iterate through the list of groups to search the one you are looking for. In the case we are working on we are selecting the site collection’s default group. Once we have it, we will need to create a new request to get the list of termsets included in the selected group and after that, a new request for getting the list of terms on those termsets we are interested in. You will easily end up with something like:

Code Snippet
  1.  
  2. var getGroups = function () {
  3.     var clientContext = SP.ClientContext.get_current();
  4.     var taxSession = SP.Taxonomy.TaxonomySession.getTaxonomySession(clientContext);
  5.     var termStore = taxSession.getDefaultSiteCollectionTermStore();
  6.  
  7.     var groups = termStore.get_groups();
  8.     clientContext.load(groups);
  9.  
  10.     clientContext.executeQueryAsync(function () {
  11.  
  12.         var groupsEnum = groups.getEnumerator();
  13.  
  14.         while (groupsEnum.moveNext()) {
  15.  
  16.             var currentGroup = groupsEnum.get_current();
  17.  
  18.             if (currentGroup.get_isSiteCollectionGroup()) {
  19.                 var groupName = currentGroup.get_name();
  20.  
  21.                 var termSets = currentGroup.get_termSets();
  22.                 var clientContext = SP.ClientContext.get_current();
  23.                 clientContext.load(termSets);
  24.  
  25.                 clientContext.executeQueryAsync(function () {
  26.  
  27.                     var clientContext = SP.ClientContext.get_current();
  28.  
  29.                     var termSetsEnumerator = termSets.getEnumerator();
  30.  
  31.                     while (termSetsEnumerator.moveNext()) {
  32.  
  33.                         var currentTermSet = termSetsEnumerator.get_current();
  34.                         var termSetName = currentTermSet.get_name();
  35.  
  36.                         if (termSetName === "field1" || termSetName === "field2") {
  37.  
  38.                             var terms = currentTermSet.getAllTerms();
  39.                             clientContext.load(terms);
  40.  
  41.                             clientContext.executeQueryAsync(function () {
  42.  
  43.                                 // Fill your controls here...
  44.  
  45.                             }, function (sender, args) {
  46.  
  47.                                 console.log(args.get_message());
  48.  
  49.                             });
  50.  
  51.                         }
  52.                     }
  53.  
  54.  
  55.                 }, function (sender, args) {
  56.  
  57.                     console.log(args.get_message());
  58.  
  59.                 });
  60.  
  61.                 break;
  62.             }
  63.         }
  64.  
  65.     }, function (sender, args) {
  66.  
  67.         console.log(args.get_message());
  68.  
  69.     });
  70. }

Please, do not copy the code above. I have wrote it just to illustrate problems and I am sure it won’t work.

What I wanted to highlight with the previous example is the typical problems you are going to face if you follow a similar approach. The first and most obvious is the spaguetti code syndrome. Yes, you are right, this can be easily fixed using delegate functions instead of nesting calls but you are going to face then some problems with the scope of variables and functions’ returned objects. Those that, like me, are not experts on javascript, will finish declaring everything globally and then you are on your own. Good luck! In addition, you must take into consideration that the subsequent calls will be queued and executed in a way that is not probably the one you are expecting. Again, problems, waste of time and frustration.

Promises

So, is there any way to make this process easier? One possible answer is “use promises”. The basic idea is that you can work with functions in a similar way of how you would work in synchronous development. The main function of the example above could be something similar to this:

Code Snippet
  1. var p = getGroups();
  2.  
  3. p.done(function (result) {
  4.  
  5.     var p2 = getTermSets(result);
  6.  
  7.     p2.done(function (result) {
  8.  
  9.         var p3 = getTerms(result, "field1");
  10.  
  11.         p3.done(function (result) {
  12.             // Fill your control here
  13.         });
  14.  
  15.         p3.fail(function (result) {
  16.             console.log(result);
  17.         });
  18.  
  19.         p3 = getTerms(result, "field2");
  20.  
  21.         p3.done(function (result) {
  22.             // Fill your control here
  23.         });
  24.  
  25.         p3.fail(function (result) {
  26.             console.log(result);
  27.         });
  28.  
  29.     });
  30.  
  31.     p2.fail(function (result) {
  32.         console.log(result);
  33.     });
  34.  
  35. });
  36.  
  37. p.fail(function (result) {
  38.     console.log(result);
  39. });

The magic is done within the methods GetGroups, GetTermSets and GetTerms. For example, the GetGroups method could look like:

Code Snippet
  1. var getGroups = function () {
  2.  
  3.     var d = $.Deferred();
  4.  
  5.     var clientContext = SP.ClientContext.get_current();
  6.     var taxSession = SP.Taxonomy.TaxonomySession.getTaxonomySession(clientContext);
  7.     var termStore = taxSession.getDefaultSiteCollectionTermStore();
  8.  
  9.     var groups = termStore.get_groups();
  10.     clientContext.load(groups);
  11.  
  12.     var o = { d: d, groups: groups };
  13.  
  14.     clientContext.executeQueryAsync(
  15.         Function.createDelegate(o, this.getGroupsCallback),
  16.         Function.createDelegate(o, this.failCallback)
  17.     );
  18.  
  19.     return d.promise();
  20. }
  21.  
  22. var getGroupsCallback = function () {
  23.     this.d.resolve(this.groups);
  24. }
  25.  
  26. var failCallback = function () {
  27.     this.d.reject("something bad happened");
  28. }

In order to see how you share information between methods, we could look at the GetTermSets method

Code Snippet
  1. var getTermSets = function (_groups) {
  2.  
  3.     var d = $.Deferred();
  4.  
  5.     var clientContext = SP.ClientContext.get_current();
  6.  
  7.  
  8.     var groupsEnum = _groups.getEnumerator();
  9.  
  10.     while (groupsEnum.moveNext()) {
  11.  
  12.         var currentGroup = groupsEnum.get_current();
  13.  
  14.         if (currentGroup.get_isSiteCollectionGroup()) {
  15.             var groupName = currentGroup.get_name();
  16.  
  17.             var termSets = currentGroup.get_termSets();
  18.             clientContext.load(termSets);
  19.  
  20.             var o = { d: d, termSets: termSets };
  21.  
  22.             clientContext.executeQueryAsync(
  23.                 Function.createDelegate(o, this.getTermSetsCallback),
  24.                 Function.createDelegate(o, this.failCallback)
  25.             );
  26.  
  27.             return d.promise();
  28.  
  29.             break;
  30.         }
  31.     }
  32. }

As you have probably seen, the final result is really cleaner and easy to follow and you won’t need to deal with global variables because you can easily send parameters to functions and retrieve returning values.

For those of you who are still reading, I need to clarify something. If you search for “Promises” and “Javascript” you will find that I’m not actually using the cool new feature natively supported by some browsers. I’m using the implementation of this pattern included in jquery called Deferred. If you want to learn more about this topic, I recommend you visiting http://www.html5rocks.com/en/tutorials/es6/promises/

0 comentarios: