Second Life of a Hungarian SharePoint Geek

November 30, 2016

Using Edge.js as a Replacement for win32ole

Filed under: ActiveX, NodeJS, SP 2013 — Tags: , , — Peter Holpar @ 21:13

Last month I had to create a NodeJS script that invokes methods of ActiveX object. I work in a Windows-only environment, so it should be no problem. I find the win32ole package quickly. Based on its description and the samples I’ve found, it seemed to be the perfect tool for my requirements. However, as many others (see issues on GitHub, and a lot of  threads about the build problem on StackOverflow), I had issues installing the package in my environment:

OS: Windows Server 2008 R2 SP1, Windows Server 2012 R2
npm: 2.15.9
node: 4.5.0

The last two lines are from the output of the npm version command.

As far as I see, a package win32ole depends on (node-gyp) fails to build:

npm ERR! win32ole@0.1.3 install: ‘node-gyp rebuild’
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the win32ole@0.1.3 install script ‘node-gyp rebuild’.

As agape824 commented on Aug 28 2015 regarding a similar issue:

“I solved the problem
by installing node.js v0.8.18 & npm v1.4.28.
Previous erros were produced by different version of node files (eg. v8.h).”

So I’ve removed all NodeJS and npm installation on one of our systems, and installed the suggested versions. Downloaded the node-v0.8.18-x64.msi, and the right npm version was installed via the command:

npm install npm@1.4.28 -g

Now invoking the command npm version results:

node: 0.8.18
npm: 1.4.28

and we can install win32ole using:

npm install –save-dev win32ole

Having win32ole installed, we can create NodeJS scripts that interact with ActiveX object.

For example, you can get the content of a web page via the MSXML2.XMLHTTP object:

var win32ole = require(‘win32ole’);

var url = "http://www.yoursite.com";
var xhr = new ActiveXObject("MSXML2.XMLHTTP");
xhr.open("GET", url, false);
xhr.send();
console.log(xhr.responseText);

Of course, you can do it much easier and a platform-independent way using other NodeJS libraries, it is just to illustrate, how to invoke ActiveX object methods. However, you can perform other, not such trivial actions using the win32ole library as well, like interacting with the Windows application that support automation via ActiveX object (like Excel, Word, or Outlook, see the examples here), or even access the HTML DOM loaded into your Internet Explorer browser, and extract values from it.

In my case I needed Internet Explorer to perform Windows-integrated authentication against a SharePoint server. In this case, SharePoint returns an authentication ticket in the response page (in the hidden input field ‘__REQUESTDIGEST’), that one can include in subsequent requests.

var win32ole = require(‘win32ole’);
var uri = "
http://YourSharePointServer";

try{
var ie = new ActiveXObject(‘InternetExplorer.Application’);
  // displaying the UI of IE might be useful when debugging
  //ie.Visible = true;
  console.log(uri);
  ie.Navigate(uri);
  while(ie.ReadyState != 4) {
    win32ole.sleep(1000);
  }
  var token = ie.Document.getElementById("__REQUESTDIGEST").value;
  console.log(token);
  ie.Quit();
}catch(e){
  console.log(‘*** exception cached ***\n’ + e);

So the win32ole package would be really great, but it has not been updated in the past 4 years or so, and we don’t work with obsolete node and npm versions just to be able to use this package. Instead of that, we tried to find a replacement solution for win32ole. And I think, we’ve found something that is even better than win32ole, and it is the Edge.js package. Edge.js enables interaction between your NodeJS and .NET code in both direction, and not only on the Windows platform, as it supports Mono and CoreCLR as well. It supports PowerShell, and other languages beyond C#, like F#, Lisp or Python just to name the most important ones.

To tell the truth, creating ActiveX object and invoking their methods only a very small subset of functionality enabled by this package. Obviously, you can not create ActiveX objects on operating systems that do not support them, but it is not the limitation of the package.

After you install the Edge.js package, for example:

npm install –save-dev edge

you can create NodeJS scripts that invokes your C# code. In the C# code you can create ActiveX objects and invoke their members as well. In the following simple example create an instance of the WScript.Shell ActiveX object, and displays a greeting message via its Popup method:

var edge = require(‘edge’);

var wshShell = edge.func(function () {/*
  async (input) => { 
       dynamic wshShell = Activator.CreateInstance(Type.GetTypeFromProgID("WScript.Shell"));
       wshShell.Popup("Hello, " + input + "!");

        return string.Empty;
    }
*/});

wshShell("world", function (error, result) {
    if (error) throw error;
    console.log(result);
});

Or we can re-create our win32ole example showed above using Edge.js, and read the authentication token via the HTML DOM in Internet Explorer:

var edge = require(‘edge’);

var uri = "http://YourSharePointServer";

var getToken = edge.func(function () {/*
    async (uri) => { 

            dynamic ie = Activator.CreateInstance(Type.GetTypeFromProgID("InternetExplorer.Application"));
            // if you want to see the UI (for example, when debugging)
            //ie.Visible = true;
            ie.Navigate(uri);
            while (ie.ReadyState != 4)
            {
                System.Threading.Thread.Sleep(1000);
            }
            var token = ie.Document.getElementById("__REQUESTDIGEST").value;
            ie.Quit();

        return token.ToString();
    }
*/});

getToken(uri, function (error, result) {
    if (error) throw error;
    console.log(result);
});

An alternative solution to the above is to read a SharePoint web page via the MSXML2.XMLHTTP object and parse the HTML DOM via cheerio to get the hidden field that contains the request digest.

var edge = require(‘edge’);
var cheerio = require(‘cheerio’);

var uri = "http://YourSharePointServer";

var getToken = edge.func(function () {/*
    async (uri) => {

            dynamic xhr = Activator.CreateInstance(Type.GetTypeFromProgID("MSXML2.XMLHTTP"));
            xhr.open("GET", uri, false);
            xhr.send();

            return xhr.responseText;
    }
*/});

getToken(uri, function (error, result) {
    if (error) throw error;
    $ = cheerio.load(result);
        console.log($(‘#__REQUESTDIGEST’).val());  
});

I hope these scripts help other developers frustrated by the build issues of win32ole to create workarounds. I think Edge.js is a really useful NodeJS package, I am sure I will find a lot of application areas for it in the future. In contrast to win32ole, Edge.js is a living project, and that is very important to us. Many thanks to Thomas Janczuk for creating and supporting this gem! Keep up the excellent job!

Creating an AngularJS Directive for Mouse Hold

Filed under: AngularJS — Tags: — Peter Holpar @ 21:12

Most of the time we use AngularJS to create our single-page application (SPA) in SharePoint and Project Server.

Recently we had a requirement that AngularJS does not provide an out-of-the-box solution for: there is a container on the page that displays a fix number of items and two buttons (one is located above the container, the other is below the container), that should scroll the items in the container back and forth. When the user clicks the upper button, new items should appear at the bottom of the container and the top items should disappear. When the user clicks the button below, the items formerly scrolled out at the top should re-appear and the items below should disappear. The user must be able to scroll the items one-by-one. However, there is a large number of items, so clicking the buttons 20 times just to scroll 20 items down would be rather inconvenient for the users.

So we need something similar, that we already have in the scroll bar in our traditional Windows applications:

  • If the user clicks on the arrow button at the bottom of the scroll bar, and holds the mouse button in the position (there is a single mouse down event but no mouse up event), the application will scroll the content in small steps down.
  • Similarly, if the user clicks on the arrow button at the top of the scroll bar, and holds the mouse button in the position (there is a single mouse down event but no mouse up event), the application will scroll the content in small steps up.
  • If the user clicks on either of the arrow buttons, holds the mouse button in this position, but moves the mouse pointer out of the area of the arrow button (the mouse down event is raised in the area of the button, but a mouse out event is raised before the mouse up event) the content will be scrolled only while the mouse pointer is over the arrow button.
  • If the user clicks on the screen as the mouse pointer is out of the arrow button area, holds the mouse button in this position, and moves the pointer over the arrow button only later (the mouse down event is raised outside the area of the button) the content will be not scrolled.

To sum up the above rules for our case: the single mouse down event should happen while the mouse pointer is over the button, and the action performed by the application (in our case it was scrolling up / down) is repeated until either a mouse up event or a mouse out event occurs.

We decided to implement the requirements as a reusable component in AngularJS, namely a directive, that encapsulates the functionality and enables to apply it to various HTML elements declaratively.

We also wanted to provide the following parameters to our component:

  • Which action the component should repeat, similar to the other, built-in AngularJS events, like ng-click. Its value should be the name of the JavaScript function available in the scope.
  • The time interval to configure the frequency of the repetition the action. Its value should be a numeric value of the delay in milliseconds.
  • The time interval to configure the delay for starting the repetition (for example, the user has to hold the mouse button down for 1 sec. to start the repetition, but once it is started, the action is performed in every 0.2 sec.) was in our first scope of work, but it was selected as victim of  feature cutting.

We found several similar solutions on web blogs and forums, but none of them fulfilled our demands completely, or they simply just didn’t work.

Our implementation was created as an attribute-level AngularJS directive: the mandatory ‘on-mouse-hold’ attribute contains the name of the function that would be invoked as action for the mouse hold event. The optionally ‘mouse-hold-repeat’ attribute contains the delay for the repetition (in milliseconds), the default value is 0.5 sec.

Note: In this post I illustrate the usage of the directive in a non-SharePoint-specific application for those of you who are not interested in SharePoint, and to separate this piece of functionality from the other (IMHO not less interesting) SharePoint-related stuff. I plan to write a further post about using the directive in a SharePoint-specific application, namely how to load items dynamically in case of scrolling using the JavaScript client object model.

The following HTML snippet illustrates using the directive in simple case. There are two buttons having different actions and delays. In this case we simply count a numeric value up and down.

  1. <div ng-app="myApp" ng-controller="counterCtrl">
  2.     <button type="button" on-mouse-hold="countUp">Count up</button>
  3.     <div>{{counter}}</div>
  4.     <button type="button" on-mouse-hold="countDown" mouse-hold-repeat="50">Count down</button>
  5. </div>

The functionality of the AngularJS directive is implemented in the JavaScript code below:

  1. 'use strict';
  2.  
  3. var myApp = angular.module('myApp', []);
  4.  
  5. myApp.controller('counterCtrl', function ($scope) {
  6.  
  7.     $scope.counter = 0;
  8.  
  9.     $scope.countDown = function () {
  10.         $scope.counter–;
  11.     }
  12.  
  13.     $scope.countUp = function () {
  14.         $scope.counter++;
  15.     }
  16.  
  17. }).directive('onMouseHold', function ($parse, $interval) {
  18.     var stop;
  19.  
  20.     var dirDefObj = {
  21.         restrict: 'A',
  22.         scope: { method: '&onMouseHold' },
  23.         link: function (scope, element, attrs) {
  24.             var expressionHandler = scope.method();
  25.             var actionInterval = (attrs.mouseHoldRepeat) ? attrs.mouseHoldRepeat : 500;
  26.  
  27.             var startAction = function () {
  28.                 expressionHandler();
  29.                 stop = $interval(function () {
  30.                     expressionHandler();
  31.                 }, actionInterval);
  32.             };
  33.  
  34.             var stopAction = function () {
  35.                 if (stop) {
  36.                     $interval.cancel(stop);
  37.                     stop = undefined;
  38.                 }
  39.             };
  40.  
  41.             element.bind('mousedown', startAction);
  42.             element.bind('mouseup', stopAction);
  43.             element.bind('mouseout', stopAction);
  44.         }
  45.     };
  46.  
  47.     return dirDefObj;
  48. });

If you want to test the functionality online, visit this page on jsfiddle.

August 29, 2016

Permission-based Rendering Templates, Part 2: The Synchronous Solution

Filed under: CSR, JavaScript, jQuery, REST, SP 2013 — Tags: , , , , — Peter Holpar @ 22:14

In my recent post I’ve illustrated how can you implement a permission-based custom rendering template using the JavaScript client object model (JSCOM)  and jQuery. That rendering template was implemented using the standard asynchronous JavaScript patterns via a callback method to not block the UI thread of the browser. In a fast network (in a LAN, for example) however, a synchronous implementation can function as well. Although there are some unsupported methods to make a JSCOM request synchronously, the JavaScript client object model was designed for asynchronous usage (see its executeQueryAsync method). To send our requests synchronously, we utilize the REST / OData interface in this post, and send the requests via the ajax function of jQuery.

To understand the original requirements and the configuration (field and list names, etc.), I suggest to read the first part first.

To enable using of jQuery selectors containing the dollar sign ($), we use the same escapeForJQuery helper function that we’ve created for the first part.

  1. var restrictedValues1 = ['Approved', 'Rejected'];
  2. var restrictedValues2 = ['Resubmit'];
  3.  
  4. var custom = custom || {};
  5.  
  6. custom.controlId = null;
  7.  
  8. var adminGroup = "MyGroup";
  9.  
  10. custom.escapeForJQuery = function (value) {
  11.     var newValue = value.replace(/\$/g, "\\$");
  12.     return newValue;
  13. }

Instead of simply wrapping the standard display template of choice fields (SPFieldChoice_Edit), the editFieldMethod function is responsible to get the HTML content of the field control, as it would be rendered without the customization by invoking the SPFieldChoice_Edit function, then we determine the group membership of the user by calling the synchronous isCurrentUserMemberOfGroup function (more about that a bit later), finally we alter the HTML content by hiding the adequate options by calling the hideOptions function (see it later as well).

  1. custom.editFieldMethod = function (ctx) {
  2.     var fieldSchema = ctx.CurrentFieldSchema;
  3.     custom.controlId = fieldSchema.Name + '_' + fieldSchema.Id + '_$DropDownChoice';
  4.     var html = SPFieldChoice_Edit(ctx);
  5.  
  6.     var isCurrentUserInGroup = custom.isCurrentUserMemberOfGroup(adminGroup);
  7.     if (isCurrentUserInGroup) {
  8.         html = custom.hideOptions(html, custom.controlId, restrictedValues1);
  9.     }
  10.     else {
  11.         html = custom.hideOptions(html, custom.controlId, restrictedValues2);
  12.     }
  13.  
  14.     return html;
  15. }

The hideOptions function loads the HTML source of the control into the DOM and removes the options that should be hidden for the given group. Finally it returns the HTML source of the altered control:

  1. custom.hideOptions = function (html, ctrlId, restrictedValues) {
  2.     var parsedHtml = $(html);
  3.     restrictedValues.forEach(function (rv) {
  4.         var selector = "#" + custom.escapeForJQuery(ctrlId) + " option[value='" + custom.escapeForJQuery(rv) + "']";
  5.         $(parsedHtml).find(selector).remove();
  6.     });
  7.     var result = $(parsedHtml).html();
  8.  
  9.     return result;
  10. }

The isCurrentUserMemberOfGroup function sends a synchronous REST request via the the ajax function of jQuery to determine the group membership of the current user:

  1. var serverUrl = String.format("{0}//{1}", window.location.protocol, window.location.host);
  2.  
  3. custom.isCurrentUserMemberOfGroup = function (groupName) {
  4.     var isMember = false;
  5.  
  6.     $.ajax({
  7.         url: serverUrl + "/_api/Web/CurrentUser/Groups?$select=LoginName",
  8.         type: "GET",
  9.         async: false,
  10.         contentType: "application/json;odata=verbose",
  11.         headers: {
  12.             "Accept": "application/json;odata=verbose",
  13.             "X-RequestDigest": $("#__REQUESTDIGEST").val()
  14.         },
  15.         complete: function (result) {
  16.             var response = JSON.parse(result.responseText);
  17.             if (response.error) {
  18.                 console.log(String.format("Error: {0}\n{1}", response.error.code, response.error.message.value));
  19.             }
  20.             else {
  21.                 var groups = response.d.results;
  22.                 groups.forEach(function (group) {
  23.                     var loginName = group.LoginName;
  24.                     console.log(String.format("Group name: {0}", loginName));
  25.                     if (groupName == loginName) {
  26.                         isMember = true;
  27.                     }
  28.                 });
  29.             }
  30.         }
  31.     });
  32.  
  33.     return isMember;
  34. }

In this case we simply register the editFieldMethod for both the ‘EditForm’ and for the ‘NewForm’ mode of the Status field, there is no need for the OnPostRender method:

  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. 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.     var adminGroup = "MyGroup";
  13.  
  14.     custom.escapeForJQuery = function (value) {
  15.         var newValue = value.replace(/\$/g, "\\$");
  16.         return newValue;
  17.     }
  18.  
  19.     custom.hideOptions = function (html, ctrlId, restrictedValues) {
  20.         var parsedHtml = $(html);
  21.         restrictedValues.forEach(function (rv) {
  22.             var selector = "#" + custom.escapeForJQuery(ctrlId) + " option[value='" + custom.escapeForJQuery(rv) + "']";
  23.             $(parsedHtml).find(selector).remove();
  24.         });
  25.         var result = $(parsedHtml).html();
  26.  
  27.         return result;
  28.     }
  29.  
  30.     custom.editFieldMethod = function (ctx) {
  31.         var fieldSchema = ctx.CurrentFieldSchema;
  32.         custom.controlId = fieldSchema.Name + '_' + fieldSchema.Id + '_$DropDownChoice';
  33.         var html = SPFieldChoice_Edit(ctx);
  34.  
  35.         var isCurrentUserInGroup = custom.isCurrentUserMemberOfGroup(adminGroup);
  36.         if (isCurrentUserInGroup) {
  37.             html = custom.hideOptions(html, custom.controlId, restrictedValues1);
  38.         }
  39.         else {
  40.             html = custom.hideOptions(html, custom.controlId, restrictedValues2);
  41.         }
  42.  
  43.         return html;
  44.     }
  45.  
  46.     var serverUrl = String.format("{0}//{1}", window.location.protocol, window.location.host);
  47.  
  48.     custom.isCurrentUserMemberOfGroup = function (groupName) {
  49.         var isMember = false;
  50.  
  51.         $.ajax({
  52.             url: serverUrl + "/_api/Web/CurrentUser/Groups?$select=LoginName",
  53.             type: "GET",
  54.             async: false,
  55.             contentType: "application/json;odata=verbose",
  56.             headers: {
  57.                 "Accept": "application/json;odata=verbose",
  58.                 "X-RequestDigest": $("#__REQUESTDIGEST").val()
  59.             },
  60.             complete: function (result) {
  61.                 var response = JSON.parse(result.responseText);
  62.                 if (response.error) {
  63.                     console.log(String.format("Error: {0}\n{1}", response.error.code, response.error.message.value));
  64.                 }
  65.                 else {
  66.                     var groups = response.d.results;
  67.                     groups.forEach(function (group) {
  68.                         var loginName = group.LoginName;
  69.                         console.log(String.format("Group name: {0}", loginName));
  70.                         if (groupName == loginName) {
  71.                             isMember = true;
  72.                         }
  73.                     });
  74.                 }
  75.             }
  76.         });
  77.  
  78.         return isMember;
  79.     }
  80.  
  81.     var customOverrides = {};
  82.     customOverrides.Templates = {};
  83.  
  84.     customOverrides.Templates.Fields = {
  85.         'Status': {
  86.             'EditForm': custom.editFieldMethod,
  87.             'NewForm': custom.editFieldMethod
  88.         }
  89.     };
  90.  
  91.     SPClientTemplates.TemplateManager.RegisterTemplateOverrides(customOverrides);
  92.  
  93. })();

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 permissionBasedFieldTemplate2.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/SiteAssets/jquery-1.9.1.min.js|~sitecollection/SiteAssets/permissionBasedFieldTemplate2.js"
$field.Update()

Note, that (in contrast to the script introduced in the first part of this post) there is no need for the JSCOM JavaScript files (sp.runtime.js and sp.js) in this case.

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.

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/&quot;))
  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.

Issues with the PSContext Object

Filed under: Bugs, PS 2013 — Tags: , — Peter Holpar @ 22:43

A few weeks ago I wrote a post about how to access the PSI methods via the PSContext object, that represent the Project Server context in the server side object model. As I wrote in the side note in my post, there are some issues with this object. In the current post I explain what I mean on that exactly. The content of the post relates to the 2016 May CU of Project Server, older and newer CUs might behave differently.

The PSContext class has no public constructor. There are however three overloads of the static GetContext method you can use to get the context: one with an SPSite parameter, one with an SPWeb parameter, and one with a Uri parameter. These methods call the corresponding private constructors (one with an SPSite parameter, one with an SPWeb parameter, and one with a Uri parameter either) of the PSContext class. The private constructor having a Uri parameter creates an SPSite instance based on the Uri and then calls the constructor having an SPSite parameter. The constructor having an SPSite parameter takes the root web of the SPSite  instance, and calls the constructor having the SPWeb parameter.

A potential issue I found is, that the PSContext constructor with the SPSite parameter type stores the SPSite parameter in a private field and dispose it when the PSContext object gets disposed. Although I have not yet faced with its side effects, you should be aware of this behavior to avoid surprises.

  1. using (SPSite site = new SPSite(pwaUrl))
  2. {                
  3.     using (PSContext projectContext = PSContext.GetContext(site))
  4.     {
  5.         Console.WriteLine("Project count: {0}", projectContext.Projects.Count());
  6.     }
  7.  
  8.     // the site is already disposed at this point
  9.     // but you may want to use it further
  10.     Console.WriteLine("Site url: {0}", site.Url);
  11. }

I think the site should be stored in the private field and be disposed in the case of the PSContext constructor with the Uri parameter type (as we create a new SPSite instance in this case), and not in the constructor with the SPSite parameter when we get the SPSite instance from the external code.

But a more serious issue that caused me some headache already is the next one:

In my recent post I described, that the private static RetrieveValue method of the PJClientCallableContext class (that is an important part of the context construction) behaves differently based on the condition, whether or not the process runs with or without a HTTP context. Without HTTP context, the SPWeb object passed to the PSContext constructor is used to create the context. Otherwise, if we have an HTTP context, the SPWeb object (or the Uri, or the SPSite, depending on which constructor you invoked) passed to the PSContext constructor is simply ignored. Let’s see what it means for the developers.

As long as you use the PSContext object from a console application, or from an application without an HTTP context, like a timer job or Windows service, it’s OK, as far as I see. You might be however surprised if you think the parameters of the GetContext methods have any significance if the PSContext object is used in a web application. Based on my experience they do not have any. Of course, the value of the Uri parameter, if you use this overload, should point to a SharePoint site, to enable the creation of the SPSite instance. But beyond that, the parameter is simply ignored.

If the web page belongs to a PWA instance, then this PWA instance will be used as the context.

If you happen to have multiple PWA instances on the same server, you can not access the other one via the PSContext object, even if you pass a parameter (Uri, SPSite or SPWeb) to the GetContext method that points to the other PWA instance.

You can even pass a parameter (Uri, SPSite or SPWeb) to the GetContext method that points to a SharePoint site without PWA instance, still the PWA instance of the page will be used.

It means on the other side, that you can not use the PSContext object in a web page without PWA instance (at least, unless you try to fake the HTTP context as described here). If you try it, you receive an HTTP 403 error (similar to the error discussed in this post):

A first chance exception of type ‘System.UnauthorizedAccessException’ occurred in mscorlib.dll

Additional information: Attempted to perform an unauthorized operation.

   at Microsoft.ProjectServer.PJClientCallableContext.get_PJContext()
   at Microsoft.ProjectServer.PJClientCallable.HandleDelegation()
   at Microsoft.ProjectServer.PJClientCallable.CallPSITag[TResult](UInt32 ulsID, String caller, Func`2 body, Action`2 onError)
   at Microsoft.ProjectServer.ProjectCollection.<>c__DisplayClass17.<>c__DisplayClass25.<.ctor>b__8()
   at System.Lazy`1.CreateValue()
   at System.Lazy`1.LazyInitValue()
   at Microsoft.ProjectServer.ProjectCollection.<>c__DisplayClass17.<.ctor>b__7()
   at System.Lazy`1.CreateValue()
   at System.Lazy`1.LazyInitValue()
   at Microsoft.ProjectServer.ProjectCollection.GetEnumerator()
   at System.Linq.Enumerable.Count[TSource](IEnumerable`1 source)
   at Custom.YourTest.Page_Load(Object sender, EventArgs e)

I think a better implementation of the PSContext object would be to have a static Current getter property (similar to the SPContext class) that we could use only if there is a HTTP context, otherwise it would return null or throw an Exception. The static GetContext method should have been reserved for usage from processes without HTTP context. Invoking these methods from a process having HTTP context should throw an exception.

July 26, 2016

Creating Project Server Enterprise Resources via REST

Filed under: Enterprise Resources, JavaScript, PS 2013, REST — Tags: , , , — Peter Holpar @ 21:27

Recently I’ve got a question on this blog about how to create Project Server enterprise resources using the REST interface. Although I consider this task to be a rather common requirement, there are really very few (if any) practical information about that on the web.

In this post I show you how to perform this operation using C# and JavaScript as well.

In the C# example I assume you have the following using statement in your code:

using System.Net;

Furthermore, it is assumed you have a string field called baseUrl in your class that contains the URL of your PWA site.

private readonly string baseUrl = "http://YourProjectServer/PWA&quot;;

I’m using some helper methods, a few of them are borrowed from the code of this SharePoint StackExchange forum thread.

First we need to get a form digest value we will use in our later POST requests. This task is performed by the GetFormDigestValue method.

  1. private string GetFormDigestValue()
  2. {
  3.     string digest = null;
  4.  
  5.     HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(this.baseUrl + "/_api/contextinfo");
  6.     request.Method = "POST";
  7.     request.ContentType = "application/json;odata=verbose";
  8.     request.UseDefaultCredentials = true;
  9.     var postBody = string.Empty;
  10.     request.ContentLength = postBody.Length;
  11.  
  12.     byte[] postData = Encoding.ASCII.GetBytes(postBody);
  13.  
  14.     using (Stream requestStream = request.GetRequestStream())
  15.     {
  16.         requestStream.Write(postData, 0, postData.Length);
  17.         requestStream.Close();
  18.  
  19.         HttpWebResponse response = (HttpWebResponse)request.GetResponse();
  20.         using (Stream responseStream = response.GetResponseStream())
  21.         {
  22.             var encoding = ASCIIEncoding.ASCII;
  23.             using (var reader = new StreamReader(response.GetResponseStream(), encoding))
  24.             {
  25.                 // parse the ContextInfo response
  26.                 var resultXml = XDocument.Parse(reader.ReadToEnd());
  27.  
  28.                 // get the form digest value
  29.                 var d = from e in resultXml.Descendants()
  30.                         where e.Name == XName.Get("FormDigestValue", "http://schemas.microsoft.com/ado/2007/08/dataservices&quot;)
  31.                         select e;
  32.                 digest = d.First().Value;
  33.             }
  34.         }
  35.     }
  36.  
  37.     return digest;
  38. }

The POST requests will be prepared by the PreparePostRequest method before execution:

  1. private HttpWebRequest PreparePostRequest(string requestPath, string formDigestValue, string postBody)
  2. {
  3.     HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(this.baseUrl + requestPath);
  4.     request.Method = "POST";
  5.     request.ContentType = "application/json;odata=verbose";
  6.     request.Accept = "application/json;odata=verbose";
  7.     request.Headers.Add("X-RequestDigest", formDigestValue);
  8.     request.UseDefaultCredentials = true;
  9.     request.ContentLength = postBody.Length;
  10.  
  11.     byte[] postData = Encoding.ASCII.GetBytes(postBody);
  12.  
  13.     System.IO.Stream requestStream = request.GetRequestStream();
  14.     requestStream.Write(postData, 0, postData.Length);
  15.     requestStream.Close();
  16.  
  17.     return request;
  18. }

The ExecuteAuthorizedPostRequest gets the form digest and prepares the request using the other two methods, finally sends the request and reads the response returned by the server.

  1. private HttpWebResponse ExecuteAuthorizedPostRequest(string requestPath, string postBody)
  2. {
  3.     string formDigestValue = GetFormDigestValue();
  4.     HttpWebRequest request = PreparePostRequest(requestPath, formDigestValue, postBody);
  5.  
  6.     HttpWebResponse response = (HttpWebResponse)request.GetResponse();
  7.  
  8.     return response;
  9. }

Using the methods above, we can concentrate on the actual “mission”, creating a new enterprise resource. In the code below I’ve included three different versions (the first two of them are commented out).

The first one creates an enterprise resource by setting its Name property only. By default, the new resources are created having EnterpriseResourceType.Work as their ResourceType property.

In the second example we create an enterprise resource by setting its Name property and its ResourceType property to EnterpriseResourceType.Material.

In the third case we create an enterprise resource by setting its Name property and its CostCenter property.

  1. public void CreateEnterpriseResource()
  2. {
  3.     string requestPath = "/_api/ProjectServer/EnterpriseResources";
  4.     // creating a new resource setting only its name
  5.     //string postBody = "{ '__metadata': { 'type': 'PS.EnterpriseResource' }, 'Name':'New Resource' }";
  6.     // creating a new resource setting its name and type ('Material' in this case)
  7.     //string postBody = string.Format("{{ '__metadata': {{ 'type': 'PS.EnterpriseResource' }}, 'Name':'New Material Resource', 'ResourceType': '{0}' }}", (int)EnterpriseResourceType.Material);
  8.     // creating a new resource setting its name and cost center
  9.     string postBody = string.Format("{{ '__metadata': {{ 'type': 'PS.EnterpriseResource' }}, 'Name':'New CC Resource', 'CostCenter': '{0}' }}", "Your Cost Center");
  10.     
  11.     var resp = ExecuteAuthorizedPostRequest(requestPath, postBody);
  12. }

Note, that the type of the object we use in the examples is PS.EnterpriseResource and not PS.EnterpriseResourceCreationInformation one would use to create a new resource via the client object model. Since the EnterpriseResourceCreationInformation object has only a limited set of properties provided by the EnterpriseResource object, we can set a lot more properties on creating the resource, than we could using the client object model. For example, in the last example above we set the cost center of the enterprise resource when creating it. It would not be possible via the EnterpriseResourceCreationInformation object.

If you need to create the resources from JavaScript on a web page instead of C#, it is possible as well.

First, we define a String.format helper function:

  1. String.format = (function () {
  2.     // The string containing the format items (e.g. "{0}")
  3.     // will and always has to be the first argument.
  4.     var theString = arguments[0];
  5.  
  6.     // start with the second argument (i = 1)
  7.     for (var i = 1; i < arguments.length; i++) {
  8.         // "gm" = RegEx options for Global search (more than one instance)
  9.         // and for Multiline search
  10.         var regEx = new RegExp("\\{" + (i – 1) + "\\}", "gm");
  11.         theString = theString.replace(regEx, arguments[i]);
  12.     }
  13.  
  14.     return theString;
  15. });

The JavaScript equivalent (using jQuery) our three examples we saw above in C# the code:

  1. var serverUrl = String.format("{0}//{1}", window.location.protocol, window.location.host);
  2. var pwaServerRelUrl = "/PWA";
  3.  
  4. function startScript() {
  5.     $.ajax({
  6.         url: serverUrl + pwaServerRelUrl + "/_api/ProjectServer/EnterpriseResources",
  7.         type: "POST",
  8.         contentType: "application/json;odata=verbose",
  9.         //data: JSON.stringify(item),
  10.         // creating a new resource setting only its name
  11.         //data: "{ '__metadata': { 'type': 'PS.EnterpriseResource' }, 'Name':'New Resource JS' }",
  12.         // creating a new resource setting its name and type ('Material' in this case)
  13.         //data: "{ '__metadata': { 'type': 'PS.EnterpriseResource' }, 'Name':'New Material Resource JS', 'ResourceType': '2' }",
  14.         // creating a new resource setting its name and cost center
  15.         data: "{ '__metadata': { 'type': 'PS.EnterpriseResource' }, 'Name':'New CC Resource JS', 'CostCenter': 'Your Cost Center' }",
  16.         headers: {
  17.             "Accept": "application/json;odata=verbose",
  18.             "X-RequestDigest": $("#__REQUESTDIGEST").val()
  19.         },
  20.         complete: function (result) {
  21.             var response = JSON.parse(result.responseText);
  22.             if (response.error) {
  23.                 console.log(String.format("Error: {0}\n{1}", response.error.code, response.error.message.value));
  24.             }
  25.             else {
  26.                 console.log(String.format("Resource created with ID: {0}", response.d.Id));
  27.             }
  28.         }
  29.     });
  30. }
  31.  
  32. $(document).ready(startScript);

Assuming this script is saved in the file CreateResource.js in the Site Assets library of the PWA site, and the jquery-1.9.1.min.js can be found in that library as well, you can add a Script Editor web part to your page, and include these two lines in the web part to inject the resource creation functionality into the page:

/PWA/SiteAssets/jquery-1.9.1.min.js
/PWA/SiteAssets/CreateResource.js

Of course, you typically don’t want such automatic resource creation on page load in a real-world scenario. Instead that, you should probably add some text fields for the resource properties (like name, cost center) and a button a page, to submit the data and create the resources using the parameters the user entered in the text fields. The plain goal of the example above is to show how the request for creating a new enterprise resource looks like, and how to send it to the server.

Displaying Enterprise Custom Fields Conditionally on the Project Details Page of Project Server

In my recent blog entry I’ve illustrated how to change the out-of –the-box display order of the enterprise custom fields in the “Details” web part on the Project Details page of Project Server. In this post I go one step further and show, how to display / hide the fields based on conditions. As in the previous case, I use jQuery in this case either to achieve the goal.

For the sake of example, let’s assume you have a few custom fields (in this case “Field1” and “Field2”) that should be displayed only for the members of a special SharePoint group called “Admins”.

The script we need in this case is displayed in the code snippet below:

  1. var adminGroup = "Admins";
  2.  
  3. function isCurrentUserMemberOfGroup(groupName, OnComplete) {
  4.  
  5.     var clientContext = new SP.ClientContext.get_current();
  6.     var currentUser = clientContext.get_web().get_currentUser();
  7.  
  8.     var userGroups = currentUser.get_groups();
  9.     clientContext.load(userGroups);
  10.  
  11.     clientContext.executeQueryAsync(OnSuccess, OnFailure);
  12.  
  13.     function OnSuccess(sender, args) {
  14.         var isMember = false;
  15.         var groupsEnumerator = userGroups.getEnumerator();
  16.         while (groupsEnumerator.moveNext()) {
  17.             var group = groupsEnumerator.get_current();
  18.             if (group.get_title() == groupName) {
  19.                 isMember = true;
  20.                 break;
  21.             }
  22.         }
  23.  
  24.         OnComplete(isMember);
  25.     }
  26.  
  27.     function OnFailure(sender, args) {
  28.         OnComplete(false);
  29.     }
  30. }
  31.  
  32. $(document).ready(startScript);
  33.  
  34. function setFieldVisibility(fields, visible) {
  35.     // hide status / twitter related fields
  36.     $(".ms-accentText").each(function (index) {
  37.         if ($.inArray($(this).text(), fields) != -1) {
  38.             $(this).closest('tr').toggle(visible);
  39.         }
  40.     });
  41. }
  42.  
  43. function startScript() {
  44.     var fieldsToHide = ["Field1", "Field2"];
  45.  
  46.     // hide the fields at page startup
  47.     setFieldVisibility(fieldsToHide, false);
  48.     isCurrentUserMemberOfGroup(adminGroup, function (isCurrentUserInGroup) {
  49.         console.log("Current user is member of group '" + adminGroup + "': " + isCurrentUserInGroup);
  50.         setFieldVisibility(fieldsToHide, isCurrentUserInGroup);
  51.     });
  52. }

We use the setFieldVisibility function to hide / display the fields. On the page load we hide the fields in the first step. To verify if the current user belongs to the group and to perform a custom action after the check I use the isCurrentUserMemberOfGroup function borrowed from Stack Overflow. Finally, we set the visibility of the field in the callback function based on the group membership of the current user.

Assuming this script is saved in the file DisplayFieldsForGroup.js in the Site Assets library of the PWA site, and the jquery-1.9.1.min.js can be found in that library as well, you can add a Script Editor web part to the Project Details page, and include these two lines in the web part to inject the functionality into the page:

/PWA/SiteAssets/jquery-1.9.1.min.js
/PWA/SiteAssets/emDisplayFieldsForGroup/em.js

Setting the Display Order of the Enterprise Custom Fields on the Project Details Page of Project Server

The out-of-the-box version of the Project Details page of Project Server contains two web parts. On the top, there is a web part with the title “Basic Info” (implemented in class Microsoft.Office.Project.PWA.WebParts.ProjectFieldPart, assembly Microsoft.Office.Project.Server.PWA) that displays the core information about the project, like its name and description as well as start and finish date and the name of the project owner.

There is another web part at the bottom of the page. It is the “Details” web part (implemented in class Microsoft.Office.Project.PWA.WebParts.ProjectCustomFieldsPart, assembly Microsoft.Office.Project.Server.PWA), and it displays the enterprise custom fields defined for the Project entity type.

In the “Basic Info” web part you can select, which project fields should be displayed, as well as their order via the web part properties, as displayed on the screenshot below:

image

image

However you don’t have such control on the fields displayed by the “Details” web part, it simply displays all of the project-related enterprise custom fields.

In this post I show you how to control the display order of the fields in the “Details” web part via jQuery, illustrating the possibilities by using two real-world problem as example.

Problem 1

Assume you have defined (among others) the following custom fields for the Project entity:

  • AField
  • AnotherField
  • CompanyComment
  • CompanyName
  • DivisionComment
  • DivisionName
  • JustOneMoreField
  • LastField

What can you do, if you have a business requirement that dictates the following order for these fields?

  • AField
  • AnotherField
  • DivisionName
  • DivisionComment
  • CompanyName
  • CompanyComment
  • JustOneMoreField
  • LastField

The fields highlighted with bold should be in the specified order. The other enterprise fields (including the ones created later) will be displayed before or after the highlighted block and sorted alphabetically.

You have the option to remove the “Details” web part from the page, and include all of the fields you need in the “Basic Info” web part in the required order. This solution seems to be simple at first sight, but it has the drawback, that you lose the original page structure with the two web parts, and what even worse, you should add any new enterprise custom field to the “Basic Info” web part manually in the future.

Alternatively, you can use jQuery to achieve the required results within the “Details” web part.

The changeFieldOrder function below demonstrates how to accomplish this goal. This function invokes the getTRForField function to find the table row for the specified field. It looks up then the row that belongs to the field specified in the first member of the string array (fieldOrder) it receives as parameter, then looks up the other rows one after the other as well, and places them after the previous one.

Optionally, you can define further field order sets and call the changeFieldOrder function repeatedly using these field sets as parameter.

  1. $(document).ready(startScript);
  2.  
  3. function getTRForField(parent, fieldName) {
  4.     var result = parent.find("h3.ms-accentText").filter(function () { return $(this).text() === fieldName; }).closest("tr").first();
  5.     return result;
  6. }
  7.  
  8. function changeFieldOrder(fieldOrder) {
  9.     var wp = $("div[name='WPClass:ProjectFieldPart']").last();
  10.     var tbody = wp.find("tbody").first();
  11.     for (i = 1; i < fieldOrder.length; i++) {
  12.         var firstTR = getTRForField(tbody, fieldOrder[i – 1]);
  13.         var secondTR = getTRForField(tbody, fieldOrder[i]);
  14.         firstTR.after(secondTR);
  15.     }
  16. }
  17.  
  18. function startScript() {
  19.  
  20.     var fieldOrder1 = ["DivisionName", "DivisionComment", "CompanyName", "CompanyComment"];
  21.     // you can define further field orders if you need
  22.     // var fieldOrder2 = ["CustomField2", "CustomField1"];
  23.  
  24.     changeFieldOrder(fieldOrder1);
  25.     // changeFieldOrder(fieldOrder2);
  26.  
  27. }

Assuming this script is saved in the file CustomFieldOrder.js in the Site Assets library of the PWA site, and the jquery-1.9.1.min.js can be found in that library as well, you can add a Script Editor web part to the Project Details page, and include these two lines in the web part to inject the field-sorting functionality into the page:

/PWA/SiteAssets/jquery-1.9.1.min.js
/PWA/SiteAssets/CustomFieldOrder.js

Problem 2

Assume you would like to display the fields in this order:

  • DivisionName
  • DivisionComment
  • CompanyName
  • CompanyComment
  • AField
  • AnotherField
  • JustOneMoreField
  • LastField

The fields highlighted with bold should be at the very top of the field list and in the specified order. The other enterprise fields (including the ones created later) will be displayed after the highlighted block and sorted alphabetically.

Again, you have the option to remove the “Details” web part from the page, and include all of the fields you need in the “Basic Info” web part in the required order, but in this case you have the same drawbacks, as in the first case above.

Instead of this, you can use the setFieldOrder function that invokes the same getTRForField function we saw in the example above. Then it looks up the row that belongs to the field specified in the first member of the string array (fieldOrder) it receives as parameter, put it at the first row of the table, then looks up the other rows one after the other as well, and places them after the previous one.

  1. function getTRForField(parent, fieldName) {
  2.     var result = parent.find("h3.ms-accentText").filter(function () { return $(this).text() === fieldName; }).closest("tr").first();
  3.     return result;
  4. }
  5.  
  6. function setFieldOrder(fieldOrder) {
  7.     var wp = $("div[name='WPClass:ProjectFieldPart']").last();
  8.     var tbody = wp.find("tbody").first();
  9.     var trLast;
  10.     for (i = 0; i < fieldOrder.length; i++) {
  11.         var tr = getTRForField(tbody, fieldOrder[i]);
  12.         if (!trLast) {
  13.             tr.prependTo(tbody);
  14.             trLast = tr;
  15.         }
  16.         else {
  17.             trLast.after(tr);
  18.             trLast = tr;
  19.         }
  20.     }
  21. }
  22.  
  23. function startScript() {
  24.     var fieldOrder = ["DivisionName", "DivisionComment", "CompanyName", "CompanyComment"];
  25.     setFieldOrder(fieldOrder);
  26. }

You can combine the usage of the changeFieldOrder and setFieldOrder functions if you wish.

In the next blog post I plan to stay with the same topic, how the enterprise custom fields get displayed in the “Details” web part, but instead of setting display order, I show you how to hide / display them based on conditions, like the group membership of the current user.

Security Issue with Connection Manager in SQL Server Integration Services Solutions

Filed under: Bugs, SSIS — Tags: , — Peter Holpar @ 21:23

Today I had to import some data from a password protected Access file into an MS SQL database using SSIS. I am working with SQL Server Data Tools in Visual Studio 2013. Might be that the issue I am writing about cannot be reproduced by the Visual Studio 2015 version of the tool.

I’ve started to create a new connection via Connection Manager. As I typed in the password – as one expect it – black dots were displayed instead of the real characters of the password.

image

It is the same if you switch from the Connection page to the All page of the dialog, where you can see and change all properties of your connection.

image

However, if you click the OK button to persist the changes in the connection, the password are revealed under the Data connection properties:

image

I think it should be a bug in the product.

BTW, after you click the OK button in this dialog as well, the password is displayed again masked (this time by asterisk characters instead of the black dots) in the property grid of the connection:

image

Older Posts »

Create a free website or blog at WordPress.com.