Second Life of a Hungarian SharePoint Geek

August 29, 2016

Permission-based Rendering Templates, Part 1: The Asynchronous Solution

Filed under: CSR, JavaScript, jQuery, JSCOM, SP 2013 — Tags: , , , , — Peter Holpar @ 21:49

Recently I read a question on SharePoint StackExchange about how one can restrict the available options in a choice field based on the group membership of the current user. My answer was to create a custom client rendering templates (CSR) and set it via the JSLink property of the choice field. If you are new in the usage of custom rendering templates, you can find several superb introduction on the topic on the web, like this one. At the time of my answer I had no sample code ready to publish (honestly, I was rather surprised that I have not found any such example on the web), but in the meantime I prepared two various implementations for the same problem. In this post I describe the first possible approach, an asynchronous solution based on the JavaScript client object model (JSCOM) and jQuery. The other solution will be discussed in a later post.

Both approaches share the same custom list: it is a list having a standard Title field and choice field called Status that has three state options: ‘Approved’, ‘Rejected’ and ‘Resubmit’. If the current use is member of a specific SharePoint group (let’s say ‘MyGroup’), the options ‘Approved’ and ‘Rejected’ should be displayed in the editable mode (that means on ‘EditForm’ and on ‘NewForm’), otherwise only the option ‘Resubmit’.

In our JavaScript rendering template we define the custom namespace, that includes the member properties and methods of the template. The same editFieldMethod function will be used in both editable modes. It’s simply a wrapper around the standard display template of choice fields (SPFieldChoice_Edit), the single extra work it performs is to store the ID of the corresponding HTML element (select in this case) into a member property called controlId. The standard format of the Id is NameOfTheChoiceField_GuidOfTheChoiceField__$DropDownChoice, for example in my case it is Status_fb5a9aac-5fdb-442e-96ac-ab7161cc4208_$DropDownChoice. We store its value to be able to find the HTML element and it children option elements via jQuery later in our asynchronous callback method.

  1. var restrictedValues1 = ['Approved', 'Rejected'];
  2. var restrictedValues2 = ['Resubmit'];
  3.  
  4. var custom = custom || {};
  5.  
  6. custom.controlId = null;
  7.  
  8. custom.editFieldMethod = function (ctx) {
  9.     var fieldSchema = ctx.CurrentFieldSchema;
  10.     custom.controlId = fieldSchema.Name + '_' + fieldSchema.Id + '_$DropDownChoice';
  11.     var html = SPFieldChoice_Edit(ctx);
  12.     return html;
  13. }

We created a simple escapeForJQuery helper function to escape the dollar sign ($) in the ID, as I found IE 11 and  jQuery have issues with that character when used in selectors.

  1. custom.escapeForJQuery = function (value) {
  2.     var newValue = value.replace(/\$/g, "\\$");
  3.     return newValue;
  4. }

Note: you might have problems with the underscore (_) as well, especially if you use old browser versions, however I have not experienced such problems.In this case you should extend the escapeForJQuery helper function. See this guide:

Given this fact, authors who write CSS often attempt to employ the underscore in a similar fashion when creating class and ID names. This should not be done. Although underscores are, as of this writing, technically permitted in class and ID names, there are many historical and practical reasons why they should be avoided.

We utilize the escapeForJQuery function in our next helper function. The hideOptions method hides those options of a specific HTML element with ID specified in the ctrlId parameter that have any of the the values specified in the restrictedValues array parameter:

  1. custom.hideOptions = function (ctrlId, restrictedValues) {
  2.     restrictedValues.forEach(function (rv) {
  3.         var selector = "#" + custom.escapeForJQuery(ctrlId) + " option[value='" + custom.escapeForJQuery(rv) + "']";
  4.         $(selector).remove();
  5.     });        
  6. }

We use a third helper function called isCurrentUserMemberOfGroup to determine via CSOM if the current user is member of a group. This function – borrowed from this answer – has two parameters: the name of the group (groupName) and a callback method (OnComplete).

  1. custom.isCurrentUserMemberOfGroup = function (groupName, OnComplete) {
  2.  
  3.     var clientContext = new SP.ClientContext.get_current();
  4.     var currentUser = clientContext.get_web().get_currentUser();
  5.  
  6.     var userGroups = currentUser.get_groups();
  7.     clientContext.load(userGroups);
  8.  
  9.     clientContext.executeQueryAsync(OnSuccess, OnFailure);
  10.  
  11.     function OnSuccess(sender, args) {
  12.         var isMember = false;
  13.         var groupsEnumerator = userGroups.getEnumerator();
  14.         while (groupsEnumerator.moveNext()) {
  15.             var group = groupsEnumerator.get_current();
  16.             if (group.get_title() == groupName) {
  17.                 isMember = true;
  18.                 break;
  19.             }
  20.         }
  21.  
  22.         OnComplete(isMember);
  23.     }
  24.  
  25.     function OnFailure(sender, args) {
  26.         OnComplete(false);
  27.     }
  28. }

The isCurrentUserMemberOfGroup function is invoked by the applyPermissions function. In the callback function we hide the adequate options based on the group membership of the user.

  1. var adminGroup = "MyGroup";
  2.  
  3. custom.applyPermissions = function (ctx) {
  4.     custom.isCurrentUserMemberOfGroup(adminGroup, function (isCurrentUserInGroup) {
  5.         console.log("Current user is member of group '" + adminGroup + "': " + isCurrentUserInGroup);
  6.  
  7.         if (custom.controlId) {
  8.             if (isCurrentUserInGroup) {
  9.                 custom.hideOptions(custom.controlId, restrictedValues1);
  10.             }
  11.             else {
  12.                 custom.hideOptions(custom.controlId, restrictedValues2);
  13.             }
  14.         }
  15.     });
  16. };

In our rendering template we register the custom editing method editFieldMethod, and set the applyPermissions function to be called as OnPostRender:

  1. var customOverrides = {};
  2. customOverrides.Templates = {};
  3.  
  4. customOverrides.Templates.Fields = {
  5.     'Status': {
  6.         'EditForm': custom.editFieldMethod,
  7.         'NewForm': custom.editFieldMethod
  8.     }
  9. };
  10.  
  11. customOverrides.Templates.OnPostRender = custom.applyPermissions;
  12.  
  13. SPClientTemplates.TemplateManager.RegisterTemplateOverrides(customOverrides);

The full source code of the rendering template introduced in this post:

  1. 'use strict';
  2.  
  3. (function () {
  4.  
  5.     var restrictedValues1 = ['Approved', 'Rejected'];
  6.     var restrictedValues2 = ['Resubmit'];
  7.  
  8.     var custom = custom || {};
  9.  
  10.     custom.controlId = null;
  11.  
  12.     custom.editFieldMethod = function (ctx) {
  13.         var fieldSchema = ctx.CurrentFieldSchema;
  14.         custom.controlId = fieldSchema.Name + '_' + fieldSchema.Id + '_$DropDownChoice';
  15.         var html = SPFieldChoice_Edit(ctx);
  16.         return html;
  17.     }
  18.  
  19.     custom.isCurrentUserMemberOfGroup = function (groupName, OnComplete) {
  20.  
  21.         var clientContext = new SP.ClientContext.get_current();
  22.         var currentUser = clientContext.get_web().get_currentUser();
  23.  
  24.         var userGroups = currentUser.get_groups();
  25.         clientContext.load(userGroups);
  26.  
  27.         clientContext.executeQueryAsync(OnSuccess, OnFailure);
  28.  
  29.         function OnSuccess(sender, args) {
  30.             var isMember = false;
  31.             var groupsEnumerator = userGroups.getEnumerator();
  32.             while (groupsEnumerator.moveNext()) {
  33.                 var group = groupsEnumerator.get_current();
  34.                 if (group.get_title() == groupName) {
  35.                     isMember = true;
  36.                     break;
  37.                 }
  38.             }
  39.  
  40.             OnComplete(isMember);
  41.         }
  42.  
  43.         function OnFailure(sender, args) {
  44.             OnComplete(false);
  45.         }
  46.     }
  47.  
  48.     custom.escapeForJQuery = function (value) {
  49.         var newValue = value.replace(/\$/g, "\\$");
  50.         return newValue;
  51.     }
  52.  
  53.     custom.hideOptions = function (ctrlId, restrictedValues) {
  54.         restrictedValues.forEach(function (rv) {
  55.             var selector = "#" + custom.escapeForJQuery(ctrlId) + " option[value='" + custom.escapeForJQuery(rv) + "']";
  56.             $(selector).remove();
  57.         });        
  58.     }
  59.  
  60.     var adminGroup = "MyGroup";
  61.  
  62.     custom.applyPermissions = function (ctx) {
  63.         custom.isCurrentUserMemberOfGroup(adminGroup, function (isCurrentUserInGroup) {
  64.             console.log("Current user is member of group '" + adminGroup + "': " + isCurrentUserInGroup);
  65.  
  66.             if (custom.controlId) {
  67.                 if (isCurrentUserInGroup) {
  68.                     custom.hideOptions(custom.controlId, restrictedValues1);
  69.                 }
  70.                 else {
  71.                     custom.hideOptions(custom.controlId, restrictedValues2);
  72.                 }
  73.             }
  74.         });
  75.     };
  76.     
  77.     var customOverrides = {};
  78.     customOverrides.Templates = {};
  79.  
  80.     customOverrides.Templates.Fields = {
  81.         'Status': {
  82.             'EditForm': custom.editFieldMethod,
  83.             'NewForm': custom.editFieldMethod
  84.         }
  85.     };
  86.  
  87.     customOverrides.Templates.OnPostRender = custom.applyPermissions;
  88.  
  89.     SPClientTemplates.TemplateManager.RegisterTemplateOverrides(customOverrides);
  90.     
  91. })();

Assuming your custom list is called PermBasedField, and both jQuery (in my case it is jquery-1.9.1.min.js) and our custom JavaScript rendering template (in my case it’s called permissionBasedFieldTemplate.js) are stored in the root of the Site Assets library of the root web, you can register the template using the following PowerShell script:

$web = Get-SPWeb http://YourSharePointSite
$list = $web.Lists["PermBasedField"]

$field = $list.Fields.GetFieldByInternalName("Status")
$field.JSLink = "~sitecollection/_layouts/15/sp.runtime.js|~sitecollection/_layouts/15/sp.js|~sitecollection/SiteAssets/jquery-1.9.1.min.js|~sitecollection/SiteAssets/permissionBasedFieldTemplate.js"
$field.Update()

Stay tuned, the second part of the post including a synchronous approach should come soon.

Advertisements

August 20, 2016

Why can’t We Send E-Mails from the SharePoint JavaScript Client Object Model, and How to Enable this Feature

Filed under: JavaScript, JSCOM, Mails, SP 2013 — Tags: , , , — Peter Holpar @ 22:43

As you might know, we can send e-mails from the SharePoint managed client object model (see code sample below), but not from the JavaScript version of the client object model.

Note: This feature has its own limitations. For example, you can not send mails to any arbitrary external mail addresses, only to the SharePoint users, and you can not add attachments to the mails, just to name a few.

  1. using (var clientContext = new ClientContext("http://YourSharePointServer/"))
  2. {
  3.     var ep = new EmailProperties();
  4.     ep.To = new List<string> { "user1@company.com", "user2@company.com" };
  5.     ep.From = "user3@company.com";
  6.     ep.Body = "body";
  7.     ep.Subject = "subject";
  8.     Utility.SendEmail(clientContext, ep);
  9.     clientContext.ExecuteQuery();
  10. }

However, if you would like to send mails using the JavaScript client object model, you find quickly, that there is no sendEmail method defined under the methods of the SP.Utilities.Utility class. That means, if you really have to send a mail from your web page using JavaScript, and would like to work only with the out-of-the-box features of SharePoint, you have to use the OData / REST interface, as illustrated by the code sample in this forum thread or see this one if you need additional mail headers.

But wait a minute. As far as I know, both the managed client object model and its JavaScript counterpart use the same server-side components and the same communication protocol between the server and client side. So what is that limitation in this case? Personally, I prefer to useing the JavaScript client object model to invoking the REST methods. Why should I mix them if my other components are written for the JavaScript client object model?

After having several questions on SharePoint StackExchange in the previous days regarding e-mail sending from JavaScript, I’ve decided to look behind the scenes.

As a first step, I searched for the JavaScript implementation of the SP.Utilities.EmailProperties and the SP.Utilities.Utility classes and found them in SP.debug.js (and in SP.js, of course), and really, there is no sendEmail method defined for the SP.Utilities.Utility class.

The mail sending for the client components is implemented on the servers side in the internal static SendEmail_Client method of  the Microsoft.SharePoint.Utilities.SPUtility class (Microsoft.SharePoint assembly). This method has the ClientCallableMethod attribute as follows:

[ClientCallableMethod(Name="SendEmail", OperationType=OperationType.Read, ClientLibraryTargets=ClientLibraryTargets.RESTful | ClientLibraryTargets.Silverlight | ClientLibraryTargets.DotNetFramework)]

You can see, that as the value of the ClientLibraryTargets property it does not define either ClientLibraryTargets.JavaScript or ClientLibraryTargets.All. It means, that it is not intended to be used from JavaScript.

In the rest of the post I show you a few alternatives, how to enable this missing functionality.

Note: The workarounds included in the post are provided “as is”, without any responsibility. Whether they work for you or not may depend on the patch level of your SharePoint environment, and any new installed patch may make a solution build on these workaround unusable. So I suggest you to not use this approach in a productive environment.

I’ve created a new web part page on my site, and added a Script Editor Web Part to it. I set the following content for the new web part:

/_layouts/15/sp.runtime.js
/_layouts/15/sp.js
/SiteAssets/sendMail.js

<button onclick="sendMail()" type="button">Send mail</button>

In the Site Assets library of the site I’ve created a new text file called sendMail.js, and edited its content.

After studying the existing static methods of the SP.Utilities.Utility class in SP.debug.js it was easy to implement the sendEmail method as well. We should have wait, while the loading of  the SP.js file has been finished, then extend the (at this time already) existing SP.Utilities.Utility class with the new method. The next snippet shows our code at this point:

  1. 'use strict';
  2.  
  3. function main() {  
  4.     SP.Utilities.Utility.sendEmail = function SP_Utilities_Utility$resolvePrincipal(context, properties) {
  5.         if (!context) {
  6.             throw Error.argumentNull('context');
  7.         }
  8.         var $v_0 = new SP.ClientActionInvokeStaticMethod(context, '{16f43e7e-bf35-475d-b677-9dc61e549339}', 'SendEmail', [properties]);
  9.  
  10.         context.addQuery($v_0);
  11.     };
  12. }
  13.  
  14. function sendMail() {
  15.     var ctx = SP.ClientContext.get_current();
  16.  
  17.     var emailProperties = new SP.Utilities.EmailProperties();
  18.     emailProperties.set_to(['user1@company.com', 'user2@company.com']);
  19.     emailProperties.set_from('user3@company.com');
  20.     emailProperties.set_body('body');
  21.     emailProperties.set_subject('subject');
  22.  
  23.     SP.Utilities.Utility.sendEmail(ctx, emailProperties);
  24.  
  25.     ctx.executeQueryAsync(
  26.                 function () {
  27.                     console.log("Mail sent");
  28.                 },
  29.                 function (sender, args) {
  30.                     console.log('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
  31.                 }
  32.             );
  33.  
  34. }
  35.  
  36.  
  37. SP.SOD.executeOrDelayUntilScriptLoaded(main, "sp.js");

Unfortunately, if you try out the page, and clicks the “Send mail” button, this code sends no mail, but you get an error instead:

Object doesn’t support property or method ‘get_bCC

The corresponding stack trace:

SP.DataConvert.invokeGetProperty [Line: 2, Col: 18240], sp.runtime.js
SP.DataConvert.writePropertiesToXml [Line: 2, Col: 11813], sp.runtime.js
SP.Utilities.EmailProperties.prototype.writeToXml [Line: 2, Col: 441169], sp.js
SP.DataConvert.writeValueToXmlElement [Line: 2, Col: 14393], sp.runtime.js
SP.ClientActionInvokeStaticMethod.prototype.$i_1 [Line: 2, Col: 30238], sp.runtime.js
SP.ClientActionInvokeStaticMethod [Line: 2, Col: 29671], sp.runtime.js
SP_Utilities_Utility$resolvePrincipal [Line: 8, Col: 9], sendMail.js
sendMail [Line: 23, Col: 5], sendMail.js
onclick [Line: 610, Col: 18], SendMailTest.aspx

If you check the property names of the SP.Utilities.EmailProperties class, you see that there are properties like BCC and CC. The corresponding getter and setter method definitions of the same class in the SP.debug.js file according to it have the name get_BCC / set_BCC and get_CC / set_CC.

The problem is, that the SP.DataConvert.invokeGetProperty mehtod calls a private method of the SP.DataConvert class, that – due to the name convention used in the JavaScript client object model – converts the property name BCC to bCC (and CC to cC), converting the first letter to lower case.

SP.DataConvert.$2V=function(a){ULSnd3:;return a.substr(0,1).toLowerCase()+a.substr(1)}    function(a){ULSnd3:;return a.substr(0,1).toLowerCase()+a.substr(1)}

What can we do? There are fortunately several options!

In the fist case, we simply create a new SP.Utilities.EmailProperties instance as earlier, then decorate the new instance in the AddMethods method with the getter / setter methods required by the SP.DataConvert.invokeGetProperty mehtod before sending the mail via the sendEmail method. In these new methods we simply wrap the original methods (the ones with the full uppercase property names).

  1. 'use strict';
  2.  
  3. function main() {  
  4.     SP.Utilities.Utility.sendEmail = function SP_Utilities_Utility$resolvePrincipal(context, properties) {
  5.         if (!context) {
  6.             throw Error.argumentNull('context');
  7.         }
  8.         var $v_0 = new SP.ClientActionInvokeStaticMethod(context, '{16f43e7e-bf35-475d-b677-9dc61e549339}', 'SendEmail', [properties]);
  9.  
  10.         context.addQuery($v_0);
  11.     };
  12. }
  13.  
  14. function AddMethods(emailProps) {
  15.     emailProps.get_bCC = function SP_Utilities_EmailProperties$get_bCC() {
  16.         return emailProps.get_BCC();
  17.     };
  18.     emailProps.set_bCC = function SP_Utilities_EmailProperties$set_bCC(value) {
  19.         emailProps.get_BCC(value);
  20.         return value;
  21.     };
  22.     emailProps.get_cC = function SP_Utilities_EmailProperties$get_cC() {
  23.         return emailProps.get_CC()
  24.     };
  25.     emailProps.set_cC = function SP_Utilities_EmailProperties$set_cC(value) {
  26.         emailProps.get_CC(value)
  27.         return value;
  28.     };
  29. }
  30.  
  31. function sendMail() {
  32.     var ctx = SP.ClientContext.get_current();
  33.  
  34.     var emailProperties = new SP.Utilities.EmailProperties();
  35.     AddMethods(emailProperties);
  36.  
  37.     emailProperties.set_to(['user1@company.com', 'user2@company.com']);
  38.     emailProperties.set_from('user3@company.com');
  39.     emailProperties.set_body('body');
  40.     emailProperties.set_subject('subject');
  41.  
  42.     SP.Utilities.Utility.sendEmail(ctx, emailProperties);
  43.  
  44.     ctx.executeQueryAsync(
  45.                 function () {
  46.                     console.log("Mail sent");
  47.                 },
  48.                 function (sender, args) {
  49.                     console.log('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
  50.                 }
  51.             );
  52.  
  53. }
  54.  
  55. SP.SOD.executeOrDelayUntilScriptLoaded(main, "sp.js");

This approach works, but you should invoke the AddMethods method for each of the SP.Utilities.EmailProperties instance you create.

As another option, we can take the source code of SP.Utilities.EmailProperties class from the SP.debug.js file, copy it to our own sendMail.js file, then replace the EmailProperties with EmailPropertiesCustom. Finally, fix the wrong method names.

  1. 'use strict';
  2.  
  3. SP.Utilities.EmailPropertiesCustom = function SP_Utilities_EmailPropertiesCustom() {
  4.     SP.Utilities.EmailPropertiesCustom.initializeBase(this);
  5. };
  6. SP.Utilities.EmailPropertiesCustom.prototype = {
  7.     $1u_1: null,
  8.     $22_1: null,
  9.     $C_1: null,
  10.     $24_1: null,
  11.     $2T_1: null,
  12.     $31_1: null,
  13.     $36_1: null,
  14.     get_additionalHeaders: function SP_Utilities_EmailPropertiesCustom$get_additionalHeaders() {
  15.         return this.$1u_1;
  16.     },
  17.     set_additionalHeaders: function SP_Utilities_EmailPropertiesCustom$set_additionalHeaders(value) {
  18.         this.$1u_1 = value;
  19.         return value;
  20.     },
  21.     get_bCC: function SP_Utilities_EmailPropertiesCustom$get_bCC() {
  22.         return this.$22_1;
  23.     },
  24.     set_bCC: function SP_Utilities_EmailPropertiesCustom$set_bCC(value) {
  25.         this.$22_1 = value;
  26.         return value;
  27.     },
  28.     get_body: function SP_Utilities_EmailPropertiesCustom$get_body() {
  29.         return this.$C_1;
  30.     },
  31.     set_body: function SP_Utilities_EmailPropertiesCustom$set_body(value) {
  32.         this.$C_1 = value;
  33.         return value;
  34.     },
  35.     get_cC: function SP_Utilities_EmailPropertiesCustom$get_cC() {
  36.         return this.$24_1;
  37.     },
  38.     set_cC: function SP_Utilities_EmailPropertiesCustom$set_cC(value) {
  39.         this.$24_1 = value;
  40.         return value;
  41.     },
  42.     get_from: function SP_Utilities_EmailPropertiesCustom$get_from() {
  43.         return this.$2T_1;
  44.     },
  45.     set_from: function SP_Utilities_EmailPropertiesCustom$set_from(value) {
  46.         this.$2T_1 = value;
  47.         return value;
  48.     },
  49.     get_subject: function SP_Utilities_EmailPropertiesCustom$get_subject() {
  50.         return this.$31_1;
  51.     },
  52.     set_subject: function SP_Utilities_EmailPropertiesCustom$set_subject(value) {
  53.         this.$31_1 = value;
  54.         return value;
  55.     },
  56.     get_to: function SP_Utilities_EmailPropertiesCustom$get_to() {
  57.         return this.$36_1;
  58.     },
  59.     set_to: function SP_Utilities_EmailPropertiesCustom$set_to(value) {
  60.         this.$36_1 = value;
  61.         return value;
  62.     },
  63.     get_typeId: function SP_Utilities_EmailPropertiesCustom$get_typeId() {
  64.         return '{fab1608d-fdfb-4c8c-bb0a-9b9cc3618a15}';
  65.     },
  66.     writeToXml: function SP_Utilities_EmailPropertiesCustom$writeToXml(writer, serializationContext) {
  67.         if (!writer) {
  68.             throw Error.argumentNull('writer');
  69.         }
  70.         if (!serializationContext) {
  71.             throw Error.argumentNull('serializationContext');
  72.         }
  73.         var $v_0 = ['AdditionalHeaders', 'BCC', 'Body', 'CC', 'From', 'Subject', 'To'];
  74.  
  75.         SP.DataConvert.writePropertiesToXml(writer, this, $v_0, serializationContext);
  76.         SP.ClientValueObject.prototype.writeToXml.call(this, writer, serializationContext);
  77.     },
  78.     initPropertiesFromJson: function SP_Utilities_EmailPropertiesCustom$initPropertiesFromJson(parentNode) {
  79.         SP.ClientValueObject.prototype.initPropertiesFromJson.call(this, parentNode);
  80.         var $v_0;
  81.  
  82.         $v_0 = parentNode.AdditionalHeaders;
  83.         if (!SP.ScriptUtility.isUndefined($v_0)) {
  84.             this.$1u_1 = SP.DataConvert.fixupType(null, $v_0);
  85.             delete parentNode.AdditionalHeaders;
  86.         }
  87.         $v_0 = parentNode.BCC;
  88.         if (!SP.ScriptUtility.isUndefined($v_0)) {
  89.             this.$22_1 = SP.DataConvert.fixupType(null, $v_0);
  90.             delete parentNode.BCC;
  91.         }
  92.         $v_0 = parentNode.Body;
  93.         if (!SP.ScriptUtility.isUndefined($v_0)) {
  94.             this.$C_1 = $v_0;
  95.             delete parentNode.Body;
  96.         }
  97.         $v_0 = parentNode.CC;
  98.         if (!SP.ScriptUtility.isUndefined($v_0)) {
  99.             this.$24_1 = SP.DataConvert.fixupType(null, $v_0);
  100.             delete parentNode.CC;
  101.         }
  102.         $v_0 = parentNode.From;
  103.         if (!SP.ScriptUtility.isUndefined($v_0)) {
  104.             this.$2T_1 = $v_0;
  105.             delete parentNode.From;
  106.         }
  107.         $v_0 = parentNode.Subject;
  108.         if (!SP.ScriptUtility.isUndefined($v_0)) {
  109.             this.$31_1 = $v_0;
  110.             delete parentNode.Subject;
  111.         }
  112.         $v_0 = parentNode.To;
  113.         if (!SP.ScriptUtility.isUndefined($v_0)) {
  114.             this.$36_1 = SP.DataConvert.fixupType(null, $v_0);
  115.             delete parentNode.To;
  116.         }
  117.     }
  118. };
  119.  
  120. SP.Utilities.EmailPropertiesCustom.registerClass('SP.Utilities.EmailPropertiesCustom', SP.ClientValueObject);
  121.  
  122. function main() {  
  123.     SP.Utilities.Utility.sendEmail = function SP_Utilities_Utility$resolvePrincipal(context, properties) {
  124.         if (!context) {
  125.             throw Error.argumentNull('context');
  126.         }
  127.         var $v_0 = new SP.ClientActionInvokeStaticMethod(context, '{16f43e7e-bf35-475d-b677-9dc61e549339}', 'SendEmail', [properties]);
  128.  
  129.         context.addQuery($v_0);
  130.     };
  131. }
  132.  
  133. function sendMail() {
  134.     var ctx = SP.ClientContext.get_current();
  135.  
  136.     // note: we use our custom class in this case!
  137.     var emailProperties = new SP.Utilities.EmailPropertiesCustom();
  138.  
  139.     emailProperties.set_to(['user1@company.com', 'user2@company.com']);
  140.     emailProperties.set_from('user3@company.com');
  141.     emailProperties.set_body('body');
  142.     emailProperties.set_subject('subject');
  143.  
  144.     SP.Utilities.Utility.sendEmail(ctx, emailProperties);
  145.  
  146.     ctx.executeQueryAsync(
  147.                 function () {
  148.                     console.log("Mail sent");
  149.                 },
  150.                 function (sender, args) {
  151.                     console.log('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
  152.                 }
  153.             );
  154.  
  155. }
  156.  
  157. SP.SOD.executeOrDelayUntilScriptLoaded(main, "sp.js");

This approach works either, however you must not forget to create an instance of the SP.Utilities.EmailPropertiesCustom class instead of an SP.Utilities.EmailProperties instance, and pass it as a parameter when invoking the sendEmail method.

In the last approach we take the source code of SP.Utilities.EmailProperties class from the SP.debug.js file, copy it to our own sendMail.js file again. This time, however, we don’t change the class name. As this class is already registered earlier in the SP.js, we would get an error like this:

SCRIPT5022: Sys.InvalidOperationException: Type SP.Utilities.EmailProperties has already been registered. The type may be defined multiple times or the script file that defines it may have already been loaded. A possible cause is a change of settings during a partial update.

To avoid this error, we should first remove the existing registration. This is possible by this line of code:

Sys.__registeredTypes[‘SP.Utilities.EmailProperties’] = false;

The code snippet for the third option:

  1. 'use strict';
  2.  
  3. SP.Utilities.EmailProperties = function SP_Utilities_EmailProperties() {
  4.     SP.Utilities.EmailProperties.initializeBase(this);
  5. };
  6. SP.Utilities.EmailProperties.prototype = {
  7.     $1u_1: null,
  8.     $22_1: null,
  9.     $C_1: null,
  10.     $24_1: null,
  11.     $2T_1: null,
  12.     $31_1: null,
  13.     $36_1: null,
  14.     get_additionalHeaders: function SP_Utilities_EmailProperties$get_additionalHeaders() {
  15.         return this.$1u_1;
  16.     },
  17.     set_additionalHeaders: function SP_Utilities_EmailProperties$set_additionalHeaders(value) {
  18.         this.$1u_1 = value;
  19.         return value;
  20.     },
  21.     get_bCC: function SP_Utilities_EmailProperties$get_bCC() {
  22.         return this.$22_1;
  23.     },
  24.     set_bCC: function SP_Utilities_EmailProperties$set_bCC(value) {
  25.         this.$22_1 = value;
  26.         return value;
  27.     },
  28.     get_body: function SP_Utilities_EmailProperties$get_body() {
  29.         return this.$C_1;
  30.     },
  31.     set_body: function SP_Utilities_EmailProperties$set_body(value) {
  32.         this.$C_1 = value;
  33.         return value;
  34.     },
  35.     get_cC: function SP_Utilities_EmailProperties$get_cC() {
  36.         return this.$24_1;
  37.     },
  38.     set_cC: function SP_Utilities_EmailProperties$set_cC(value) {
  39.         this.$24_1 = value;
  40.         return value;
  41.     },
  42.     get_from: function SP_Utilities_EmailProperties$get_from() {
  43.         return this.$2T_1;
  44.     },
  45.     set_from: function SP_Utilities_EmailProperties$set_from(value) {
  46.         this.$2T_1 = value;
  47.         return value;
  48.     },
  49.     get_subject: function SP_Utilities_EmailProperties$get_subject() {
  50.         return this.$31_1;
  51.     },
  52.     set_subject: function SP_Utilities_EmailProperties$set_subject(value) {
  53.         this.$31_1 = value;
  54.         return value;
  55.     },
  56.     get_to: function SP_Utilities_EmailProperties$get_to() {
  57.         return this.$36_1;
  58.     },
  59.     set_to: function SP_Utilities_EmailProperties$set_to(value) {
  60.         this.$36_1 = value;
  61.         return value;
  62.     },
  63.     get_typeId: function SP_Utilities_EmailProperties$get_typeId() {
  64.         return '{fab1608d-fdfb-4c8c-bb0a-9b9cc3618a15}';
  65.     },
  66.     writeToXml: function SP_Utilities_EmailProperties$writeToXml(writer, serializationContext) {
  67.         if (!writer) {
  68.             throw Error.argumentNull('writer');
  69.         }
  70.         if (!serializationContext) {
  71.             throw Error.argumentNull('serializationContext');
  72.         }
  73.         var $v_0 = ['AdditionalHeaders', 'BCC', 'Body', 'CC', 'From', 'Subject', 'To'];
  74.  
  75.         SP.DataConvert.writePropertiesToXml(writer, this, $v_0, serializationContext);
  76.         SP.ClientValueObject.prototype.writeToXml.call(this, writer, serializationContext);
  77.     },
  78.     initPropertiesFromJson: function SP_Utilities_EmailProperties$initPropertiesFromJson(parentNode) {
  79.         SP.ClientValueObject.prototype.initPropertiesFromJson.call(this, parentNode);
  80.         var $v_0;
  81.  
  82.         $v_0 = parentNode.AdditionalHeaders;
  83.         if (!SP.ScriptUtility.isUndefined($v_0)) {
  84.             this.$1u_1 = SP.DataConvert.fixupType(null, $v_0);
  85.             delete parentNode.AdditionalHeaders;
  86.         }
  87.         $v_0 = parentNode.BCC;
  88.         if (!SP.ScriptUtility.isUndefined($v_0)) {
  89.             this.$22_1 = SP.DataConvert.fixupType(null, $v_0);
  90.             delete parentNode.BCC;
  91.         }
  92.         $v_0 = parentNode.Body;
  93.         if (!SP.ScriptUtility.isUndefined($v_0)) {
  94.             this.$C_1 = $v_0;
  95.             delete parentNode.Body;
  96.         }
  97.         $v_0 = parentNode.CC;
  98.         if (!SP.ScriptUtility.isUndefined($v_0)) {
  99.             this.$24_1 = SP.DataConvert.fixupType(null, $v_0);
  100.             delete parentNode.CC;
  101.         }
  102.         $v_0 = parentNode.From;
  103.         if (!SP.ScriptUtility.isUndefined($v_0)) {
  104.             this.$2T_1 = $v_0;
  105.             delete parentNode.From;
  106.         }
  107.         $v_0 = parentNode.Subject;
  108.         if (!SP.ScriptUtility.isUndefined($v_0)) {
  109.             this.$31_1 = $v_0;
  110.             delete parentNode.Subject;
  111.         }
  112.         $v_0 = parentNode.To;
  113.         if (!SP.ScriptUtility.isUndefined($v_0)) {
  114.             this.$36_1 = SP.DataConvert.fixupType(null, $v_0);
  115.             delete parentNode.To;
  116.         }
  117.     }
  118. };
  119.  
  120. // re-register the type
  121. // to avoid the error
  122. // SCRIPT5022: Sys.InvalidOperationException: Type SP.Utilities.EmailProperties has already been registered. The type may be defined multiple times or the script file that defines it may have already been loaded. A possible cause is a change of settings during a partial update.
  123. // we should first remove the existing registration
  124. Sys.__registeredTypes['SP.Utilities.EmailProperties'] = false;
  125. SP.Utilities.EmailProperties.registerClass('SP.Utilities.EmailProperties', SP.ClientValueObject);
  126.  
  127. function main() {  
  128.     SP.Utilities.Utility.sendEmail = function SP_Utilities_Utility$resolvePrincipal(context, properties) {
  129.         if (!context) {
  130.             throw Error.argumentNull('context');
  131.         }
  132.         var $v_0 = new SP.ClientActionInvokeStaticMethod(context, '{16f43e7e-bf35-475d-b677-9dc61e549339}', 'SendEmail', [properties]);
  133.  
  134.         context.addQuery($v_0);
  135.     };
  136. }
  137.  
  138. function sendMail() {
  139.     var ctx = SP.ClientContext.get_current();
  140.     
  141.     var emailProperties = new SP.Utilities.EmailProperties();
  142.     emailProperties.set_to(['user1@company.com', 'user2@company.com']);
  143.     emailProperties.set_from('user3@company.com');
  144.     emailProperties.set_body('body');
  145.     emailProperties.set_subject('subject');
  146.  
  147.     SP.Utilities.Utility.sendEmail(ctx, emailProperties);
  148.  
  149.     ctx.executeQueryAsync(
  150.                 function () {
  151.                     console.log("Mail sent");
  152.                 },
  153.                 function (sender, args) {
  154.                     console.log('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
  155.                 }
  156.             );
  157.  
  158. }
  159.  
  160. SP.SOD.executeOrDelayUntilScriptLoaded(main, "sp.js");

Although it is possible the less supported option (if there are different levels of supportability at all) from the three options discussed in the post, I prefer this last one, as its usage is the most transparent for the developer. One can use the SP.Utilities.EmailProperties class and there is no need for invoking helper methods.

Note: Be aware, that when using one of the last two options, you should call the get_bCC / set_bCC and get_cC / set_cC methods instead of the get_BCC / set_BCC and get_CC / set_CC methods if you need to read / set the BCC / CC properties. In the case of the first option however, you can call both of the methods with first uppercase letter, and the methods with first lowercase letter.

February 11, 2015

Migrating AngularJS-based SharePoint 2013 Applications to Former SharePoint Versions

Filed under: AngularJS, JSCOM, SP 2010, SP 2013 — Tags: , , , — Peter Holpar @ 23:34

In the past years we created several more or less simple AngularJS-based applications for SharePoint 2013 using REST / OData or the JavaScript Client Object Model (JSCOM). As we found this combo quite powerful and effective, we tried to apply the same technologies in other environments too, where we had to work with SharePoint 2010 (with and even without the visual upgrade). Since the migration into the backward direction was not quite trivial, I thought it might be worth to share my experience.

The sample application that I use in this post to illustrate the steps to fix the errors during the migration is really a simple one: it utilizes AngularJS v1.3.0-beta.6 to display the First Name, Last Name, Business Phone and Email Address fields of items from a standard SharePoint Contacts lists, and lets the items to be ordered as the user clicks on the field names in the header.

For example, we have a Contacts list like this:

image

that will be displayed as it shown below:

Contacts15

The HTML code that we used in a Content Editor web part to define the UI:

  1. <script type="text/javascript" src="/_layouts/15/sp.runtime.js"></script>
  2. <script type="text/javascript" src="/_layouts/15/sp.js"></script>
  3. <script type="text/javascript" src="/_layouts/15/contacts/angular.min.js"></script>
  4. <script type="text/javascript" src="/_layouts/15/contacts/contacts.js"></script>
  5.  
  6. <div ng-app="myApp">
  7.     <div ng-controller="contactsCtrl">
  8.         <table>
  9.             <tr>
  10.                 <td><a href="" ng-click="predicate = 'firstName'; reverse=!reverse">First name</a></td>
  11.                 <td><a href="" ng-click="predicate = 'lastName'; reverse=!reverse">Last name</a></td>
  12.                 <td><a href="" ng-click="predicate = 'eMail'; reverse=!reverse">E-Mail</a></td>
  13.                 <td><a href="" ng-click="predicate = 'workPhone'; reverse=!reverse">Phone</a></td>
  14.             </tr>
  15.                     <tr ng-repeat="contact in contacts | orderBy:predicate:reverse">
  16.                 <td>{{contact.firstName}}</td>
  17.                 <td>{{contact.lastName}}</td>
  18.                 <td><a href="mailto:{{contact.eMail}}">{{contact.eMail}}</a></td>
  19.                 <td>{{contact.workPhone}}</td>
  20.             </tr>
  21.         </table>
  22.     </div>
  23. </div>

The AngularJS controller (contactsCtrl) as well as the custom service (mySharePointService) were defined in the contacts.js. The service access the SharePoint list via JSCOM.

  1. 'use strict';
  2.  
  3. var contactListName = 'Contacts';
  4.  
  5. var myApp = angular.module('myApp', []);
  6.  
  7. myApp.service('mySharePointService', function ($q, $http) {
  8.  
  9.     this.getContacts = function ($scope) {
  10.         var deferred = $q.defer();
  11.  
  12.         var ctx = new SP.ClientContext.get_current();
  13.  
  14.         var web = ctx.get_web();
  15.         var list = web.get_lists().getByTitle(contactListName);
  16.  
  17.         var camlQuery = new SP.CamlQuery();
  18.         camlQuery.set_viewXml('<View><ViewFields><FieldRef Name=\'Title\'/><FieldRef Name=\'FirstName\'/><FieldRef Name=\'Email\'/><FieldRef Name=\'WorkPhone\'/></ViewFields></View>');
  19.         var contacts = list.getItems(camlQuery);
  20.  
  21.         ctx.load(contacts);
  22.  
  23.         ctx.executeQueryAsync(
  24.             function () {
  25.                 deferred.resolve(contacts);
  26.             },
  27.             function (sender, args) {
  28.                 deferred.reject('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
  29.             }
  30.         );
  31.  
  32.         return deferred.promise;
  33.     };
  34.  
  35.  
  36. });
  37.  
  38. myApp.controller('contactsCtrl', function ($scope, mySharePointService) {
  39.     var promiseContacts = mySharePointService.getContacts($scope);
  40.  
  41.     promiseContacts.then(function (contacts) {
  42.         $scope.contacts = [];
  43.         var contactEnumerator = contacts.getEnumerator();
  44.         while (contactEnumerator.moveNext()) {
  45.             var contact = contactEnumerator.get_current();
  46.             $scope.contacts.push({
  47.                 firstName: contact.get_item('FirstName'),
  48.                 lastName: contact.get_item('Title'),
  49.                 workPhone: contact.get_item('WorkPhone'),
  50.                 eMail: contact.get_item('Email')
  51.             });
  52.         }
  53.  
  54.     }, function (errorMsg) {
  55.         console.log("Error: " + errorMsg);
  56.     });
  57. });

When we was to use the same HTML and .js in SP 2010 SP2 with the visual upgrade (that is the standard SP 2010 UI, below is a screenshot of the Contacts list itself), we had to face some issues:

ContactList14

First of all, although the data binding was correct and it has been initialized (the double curly braces disappeared), no data was displayed at all. When checking the console in the IE Developer Tools, we saw error messages like:

The collection has not been initialized. It has not been requested or the request has not been executed. It may need to be explicitly requested.

Remark: In other cases, when we did not work with collections, but object properties, the error was:

The property or field has not been initialized. It has not been requested or the request has not been executed. It may need to be explicitly requested.

As you may know, this error message typically indicates, that the collection / property has not been loaded into the client context. However, when I checked the network traffic between the client and server using Fiddler, I saw that the collection was requested by the client, and the response contains it as well. To make the symptoms even stranger, occasionally the contacts were displayed without any error message. This fact indicated me that it might be a kind of timing issue.

The solution to this issue was really inserting a delay (1 sec in our case) in the controller before we invoke the getContacts method of the mySharePointService service.

setTimeout(function () {
    $scope.getContacts($scope, mySharePointService);
}, 1000);

Second, sorting the items was not working anymore. Although one could see for a moment, that the sort order of the items was changed on clicking on the field name in the headers, but a second later the page reloaded and was displayed with the original sort order.

The solution to this problem was to replace the value of the href attribute of the anchors in the header. The original value was

<a href="" …

The new one, that changes the sort order without reloading the page:

<a href="javascript:;" …

We had a third error as well, that seems to be a consequence of the standard document mode (IE 8) used by the SharePoint 2010 master pages:

SCRIPT5014: Array.prototype.slice: ‘this’ is not a JavaScript object
angular.min.js, line 26 character 36

As we switched the document mode manually (for example, via the IE Developer Tools) the error disappeared. As this error had no negative effect on the functionality, we simply ignored that.

The updated version of the HTML content:

  1. <script type="text/javascript" src="/_layouts/sp.core.js"></script>
  2. <script type="text/javascript" src="/_layouts/sp.runtime.js"></script>
  3. <script type="text/javascript" src="/_layouts/sp.js"></script>
  4. <script type="text/javascript" src="/_layouts/contacts/angular.min.js"></script>
  5. <script type="text/javascript" src="/_layouts/contacts/contacts.js"></script>
  6.  
  7. <div ng-app="myApp">
  8. <div ng-controller="contactsCtrl">
  9.   <table>
  10.     <tr>
  11.         <td><a href="javascript:;" ng-click="predicate = 'firstName'; reverse=!reverse">First name</a></td>
  12.         <td><a href="javascript:;" ng-click="predicate = 'lastName'; reverse=!reverse">Last name</a></td>
  13.         <td><a href="javascript:;" ng-click="predicate = 'eMail'; reverse=!reverse">E-Mail</a></td>
  14.         <td><a href="javascript:;" ng-click="predicate = 'workPhone'; reverse=!reverse">Phone</a></td>
  15.     </tr>
  16.             <tr ng-repeat="contact in contacts | orderBy:predicate:reverse">
  17.         <td>{{contact.firstName}}</td>
  18.         <td>{{contact.lastName}}</td>
  19.         <td><a href="mailto:{{contact.eMail}}">{{contact.eMail}}</a></td>
  20.         <td>{{contact.workPhone}}</td>
  21.     </tr>
  22. </table>
  23. </div>
  24. </div>

The full and updated .js file:

  1. 'use strict';
  2.  
  3. var contactListName = 'Contacts';
  4.  
  5. var myApp = angular.module('myApp', []);
  6.  
  7. myApp.service('mySharePointService', function ($q, $http) {
  8.  
  9.   this.getContacts = function ($scope) {
  10.     var deferred = $q.defer();
  11.  
  12.     var ctx = new SP.ClientContext.get_current();
  13.  
  14.     var web = ctx.get_web();
  15.     var list = web.get_lists().getByTitle(contactListName);
  16.  
  17.     var camlQuery = new SP.CamlQuery();
  18.     camlQuery.set_viewXml('<View><ViewFields><FieldRef Name=\'Title\'/><FieldRef Name=\'FirstName\'/><FieldRef Name=\'Email\'/><FieldRef Name=\'WorkPhone\'/></ViewFields></View>');
  19.     var contacts = list.getItems(camlQuery);
  20.  
  21.     ctx.load(contacts);
  22.  
  23.     ctx.executeQueryAsync(
  24.             function () {
  25.               deferred.resolve(contacts);
  26.             },
  27.             function (sender, args) {
  28.               deferred.reject('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
  29.             }
  30.         );
  31.  
  32.     return deferred.promise;
  33.   };
  34.  
  35. });
  36.  
  37. myApp.controller('contactsCtrl', function ($scope, mySharePointService) {
  38.     
  39.     setTimeout(function () {
  40.       $scope.getContacts($scope, mySharePointService);
  41.     }, 1000);
  42.  
  43.     $scope.getContacts = function ($scope, mySharePointService) {
  44.       var promiseContacts = mySharePointService.getContacts($scope);
  45.  
  46.       promiseContacts.then(function (contacts) {
  47.         $scope.contacts = [];
  48.         var contactEnumerator = contacts.getEnumerator();
  49.         while (contactEnumerator.moveNext()) {
  50.           var contact = contactEnumerator.get_current();
  51.           $scope.contacts.push({
  52.             firstName: contact.get_item('FirstName'),
  53.             lastName: contact.get_item('Title'),
  54.             workPhone: contact.get_item('WorkPhone'),
  55.             eMail: contact.get_item('Email')
  56.           });
  57.         }
  58.  
  59.       }, function (errorMsg) {
  60.         console.log("Error: " + errorMsg);
  61.       });
  62.     };
  63. });

The result as rendered by a Content Editor web part:

Contacts14

In case of SP 2010 SP2 without the visual update (that is the WSS 3.0 / MOSS 2007 UI, see the screenshot of the list below) we had all the above mentioned problems, and the following one:

ContactList12

The data binding was not working at all, see the image below:

NoDataBinding

It turned out to have two causes, both of them was an effect of the standard document mode (quirks mode) used by the WSS 3.0 master pages on working of AngularJS.

To solve these issues, first we had to append id="ng-app" to the HTML DIV we bind the AngularJS application to.

The original version was:

<div ng-app="myApp">

The new one is:

<div ng-app="myApp" id="ng-app">

Second, we had to disable the $sce service of AngularJS in the application if the document is displayed in quirks mode. We can achieve that via the config method of the application:

myApp.config(function ($sceProvider) {
    // Completely disable SCE to support IE7 (quirks mode in SharePoint 2007 / 2010).
    // SCRIPT5022: [$sce:iequirks]
http://errors.angularjs.org/1.3.0-beta.6/$sce/iequirks
    if ((document.documentMode == 5) || (document.documentMode == 7)) {
        // or: if (document.documentMode < 8) {
        $sceProvider.enabled(false);
    }
});

The updated version of the HTML content:

  1. <script type="text/javascript" src="/_layouts/sp.core.js"></script>
  2. <script type="text/javascript" src="/_layouts/sp.runtime.js"></script>
  3. <script type="text/javascript" src="/_layouts/sp.js"></script>
  4. <script type="text/javascript" src="/_layouts/contacts/angular.min.js"></script>
  5. <script type="text/javascript" src="/_layouts/contacts/contacts12.js"></script>
  6.  
  7. <div ng-app="myApp" id="ng-app">
  8. <div ng-controller="contactsCtrl">
  9.   <table>
  10.     <tr>
  11.         <td><a href="javascript:;" ng-click="predicate = 'firstName'; reverse=!reverse">First name</a></td>
  12.         <td><a href="javascript:;" ng-click="predicate = 'lastName'; reverse=!reverse">Last name</a></td>
  13.         <td><a href="javascript:;" ng-click="predicate = 'eMail'; reverse=!reverse">E-Mail</a></td>
  14.         <td><a href="javascript:;" ng-click="predicate = 'workPhone'; reverse=!reverse">Phone</a></td>
  15.     </tr>
  16.             <tr ng-repeat="contact in contacts | orderBy:predicate:reverse">
  17.         <td>{{contact.firstName}}</td>
  18.         <td>{{contact.lastName}}</td>
  19.         <td><a href="mailto:{{contact.eMail}}">{{contact.eMail}}</a></td>
  20.         <td>{{contact.workPhone}}</td>
  21.     </tr>
  22. </table>
  23. </div>
  24. </div>

The full and updated .js file:

  1. 'use strict';
  2.  
  3. var contactListName = 'Contacts';
  4.  
  5. var myApp = angular.module('myApp', []);
  6.  
  7. myApp.service('mySharePointService', function ($q, $http) {
  8.  
  9.   this.getContacts = function ($scope) {
  10.     var deferred = $q.defer();
  11.  
  12.     var ctx = new SP.ClientContext.get_current();
  13.  
  14.     var web = ctx.get_web();
  15.     var list = web.get_lists().getByTitle(contactListName);
  16.  
  17.     var camlQuery = new SP.CamlQuery();
  18.     camlQuery.set_viewXml('<View><ViewFields><FieldRef Name=\'Title\'/><FieldRef Name=\'FirstName\'/><FieldRef Name=\'Email\'/><FieldRef Name=\'WorkPhone\'/></ViewFields></View>');
  19.     var contacts = list.getItems(camlQuery);
  20.  
  21.     ctx.load(contacts);
  22.  
  23.     ctx.executeQueryAsync(
  24.             function () {
  25.               deferred.resolve(contacts);
  26.             },
  27.             function (sender, args) {
  28.               deferred.reject('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
  29.             }
  30.         );
  31.  
  32.     return deferred.promise;
  33.   };
  34.  
  35. });
  36.  
  37. myApp.controller('contactsCtrl', function ($scope, mySharePointService) {
  38.     
  39.     setTimeout(function () {
  40.       $scope.getContacts($scope, mySharePointService);
  41.     }, 1000);
  42.  
  43.     $scope.getContacts = function ($scope, mySharePointService) {
  44.       var promiseContacts = mySharePointService.getContacts($scope);
  45.  
  46.       promiseContacts.then(function (contacts) {
  47.         $scope.contacts = [];
  48.         var contactEnumerator = contacts.getEnumerator();
  49.         while (contactEnumerator.moveNext()) {
  50.           var contact = contactEnumerator.get_current();
  51.           $scope.contacts.push({
  52.             firstName: contact.get_item('FirstName'),
  53.             lastName: contact.get_item('Title'),
  54.             workPhone: contact.get_item('WorkPhone'),
  55.             eMail: contact.get_item('Email')
  56.           });
  57.         }
  58.  
  59.       }, function (errorMsg) {
  60.         console.log("Error: " + errorMsg);
  61.       });
  62.     };
  63.   });
  64.  
  65.   myApp.config(function ($sceProvider) {
  66.     // Completely disable SCE to support IE7 (quirks mode in SharePoint 2007 / 2010).
  67.     // SCRIPT5022: [$sce:iequirks] http://errors.angularjs.org/1.3.0-beta.6/$sce/iequirks
  68.     if ((document.documentMode == 5) || (document.documentMode == 7)) {
  69.       // or: if (document.documentMode < 8) {
  70.       $sceProvider.enabled(false);
  71.     }
  72.   });

The result as rendered by a Content Editor web part:

Contacts12

I assume that this method should work even in case of WSS 3.0 / MOSS 2007, although if you really work with this SharePoint version, you have to obviously re-write the custom mySharePointService service to utilize the SharePoint web services (for example, via the SPServices library) instead of JSCOM.

Note: Another alternative approach to include AngularJS-based content in former SharePoint version is to use an IFRAME (for example, via Page Viewer Web Part) to display a simple HTML document, that you can store either in a document library in SharePoint or in the file system in the _layouts hive. Since in this page you don’t use the master page that enforced the document mode that caused the issues above, you can work without the restrictions or problems we faced when included the AngularJS content in the .aspx page itself.

I hope that the tips and hints in this post help all of you to utilize the really great features of AngularJS even if you have to work with former versions of SharePoint.

November 25, 2012

How to reduce the number of requests sent to the server when working with the Client Object Model?

Filed under: JSCOM, Managed Client OM, SP 2010 — Tags: , , — Peter Holpar @ 21:27

Assume you need to access data on SharePoint server. The information you need consists of a batch of similar items, for example, properties (like name) of users having their ID from a specific array, or the title of sites having their URL from an array as well. In the case of users (or other entities represented via list items) you could use CAML (see my former post for a similar, simple JSCOM example), although the query itself would be rather complex after a few dozens of items, however, other cases (like site names) might raise more serious issues.

You could send the requests consecutively, sending the next request only after you received the response for the former one, or you can send a set of asynchronous requests one after the other, and then process the responses parallel. I found neither of these options to be ideal. On one side, the high number of roundtrips between the client and server won’t help performance, on the other side, synchronizing requests and responses (have we received all the responses for our requests?) could be a nightmare.

Instead of these ones, we can implement a solution, where we cumulate all of the requests into a single context, then send the query at once, and process the single response.

Below I provide an example of this implementation for the managed client object model:

  1. List<int> lookupids = new List<int> { 19, 23, 32 };
  2. List<ListItem> userInfos = new List<ListItem>();
  3. using (ClientContext clientContext = new ClientContext("http://yoursharepointsite&quot;))
  4. {
  5.     lookupids.ForEach(id =>
  6.         {
  7.             ListItem userInfo = clientContext.Web.SiteUserInfoList.GetItemById(id);
  8.             userInfos.Add(userInfo);
  9.             clientContext.Load(userInfo);
  10.         });
  11.     clientContext.ExecuteQuery();
  12.     IEnumerable<string> userEmails = userInfos.Select(userInfo => userInfo["EMail"].ToString());
  13.     Console.WriteLine(String.Join(", ", userEmails.ToArray()));
  14. }

and the JavaScript version as well:

  1. <script type="text/javascript">
  2.  
  3. var siteUrl = '/';
  4. var userInfos;
  5.  
  6. function retrieveUsers() {
  7.     var ids = [19, 23, 32];
  8.     userInfos = new Array();
  9.  
  10.     var clientContext = new SP.ClientContext(siteUrl);
  11.     var web = clientContext.get_web();
  12.     var userList = web.get_siteUserInfoList();
  13.  
  14.     for(i=0; i<ids.length; i++) {
  15.         var id = ids[i];
  16.         var userInfo = userList.getItemById(id);
  17.         clientContext.load(userInfo);
  18.         userInfos[i] = userInfo;
  19.     }
  20.  
  21.     clientContext.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceeded), Function.createDelegate(this, this.onQueryFailed));        
  22. }
  23.  
  24. function onQuerySucceeded(sender, args) {  
  25.     var result = '';
  26.     for(i=0; i<userInfos.length; i++)
  27.     {
  28.         var userInfo = userInfos[i];
  29.         result += userInfo.get_item('Name') + ', ' + userInfo.get_item('Title') + ', ' + userInfo.get_item('EMail') + '\n';
  30.     }  
  31.     alert(result);
  32. }
  33.  
  34. function onQueryFailed(sender, args) {
  35.  
  36.     alert('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
  37. }
  38.  
  39. ExecuteOrDelayUntilScriptLoaded(retrieveListItems, "sp.ribbon.js");
  40.  
  41. </script>

Note: The key factor of this solution is that you subsequent queries must be independent from the responses of the former requests.

How to get the users from a multivalued field using JSCOM?

Filed under: JavaScript, JSCOM, SP 2010 — Tags: , , — Peter Holpar @ 21:17

Assume you have a SharePoint list called Projects with a field called Members of type Person or Group and the option Allow Multiple Selections is enabled. You are to retrieve the users from the Members field using JavaScript Client Object Model (JSCOM).

Note: A similar problem and solution for the Managed Client Object Model can be found here.

In this case the value of the Members field is an instance of SPFieldUserValueCollection on the server side, on the client side it’s an array of FieldUserValue instances. This latter type is a special case of the lookup value, inherited from the FieldLookupValue type, so we can access its members similarly, that means, we should use the get_lookupId and get_lookupValue methods.

In our case the JavaScript code to access the members of a specific project looks like this:

  1. <script type="text/javascript">
  2.  
  3. var siteUrl = '/';
  4.  
  5. function retrieveListItems() {
  6.  
  7.     var clientContext = new SP.ClientContext(siteUrl);
  8.     var projectList = clientContext.get_web().get_lists().getByTitle('Projects');
  9.         
  10.     // instead of the simple CAML query we could use the getItemById method of the list object
  11.     // especially if we need a simple item only
  12.     // but if we need for example the top 5 items, the CAML way is easier
  13.     var camlQuery = new SP.CamlQuery();
  14.     camlQuery.set_viewXml('<View><Query><Where><Geq><FieldRef Name=\'ID\'/><Value Type=\'Number\'>1</Value></Geq></Where></Query><RowLimit>5</RowLimit></View>');
  15.     this.collListItem = projectList.getItems(camlQuery);
  16.         
  17.     clientContext.load(collListItem);
  18.         
  19.     clientContext.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceeded), Function.createDelegate(this, this.onQueryFailed));        
  20.         
  21. }
  22.  
  23. function onQuerySucceeded(sender, args) {
  24.     var listItemInfo = '';
  25.     var listItemEnumerator = collListItem.getEnumerator();
  26.     while (listItemEnumerator.moveNext()) {
  27.          var oListItem = listItemEnumerator.get_current();
  28.         var result = '';
  29.         var members = oListItem.get_item('Members');
  30.         for(i=0; i<members.length; i++)
  31.         {
  32.             var member = members[i];
  33.             result += member.get_lookupId() + ', ' + member.get_lookupValue() + '\n';
  34.         }  
  35.  
  36.        
  37.         listItemInfo += '\nID: ' + oListItem.get_id() +
  38.         '\nMembers: ' +  result +
  39.             '\nTitle: ' + oListItem.get_item('Title');
  40.     }
  41.  
  42.     alert(listItemInfo.toString());
  43. }
  44.  
  45. function onQueryFailed(sender, args) {
  46.  
  47.     alert('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
  48. }
  49.  
  50. ExecuteOrDelayUntilScriptLoaded(retrieveListItems, "sp.ribbon.js");
  51.  
  52. </script>

Create a free website or blog at WordPress.com.