CICONotCheckedOut queue errors when updating projects via JSOM

I’ve written many times before about working with projects using the JSOM and CSOM API’s, and this is another issue in the API that I’ve had to resolve for one of my apps (in this case Bulk Edit).

Issue

When updating built in fields (I haven’t observed this for custom fields) using the client side library (JSOM or CSOM or REST) in the normal way the following error can be reported in the queue unexpectedly:

CICONotCheckedOut: CICONotCheckedOut (10102). Details: id=’10102′ name=’CICONotCheckedOut’ uid=’a2da51ea-792b-e411-9af1-00155d908811′.

Queue: GeneralQueueJobFailed (26000) - ProjectUpdate.FailIfNotCheckedOutMessage. …

For example the following code from the MSDN article on this topic will intermittently get the above error:

// Get the target project and then check it out. The checkOut function
// returns the draft version of the project.
var project = projects.getById(targetGuid);
var draftProject = project.checkOut();

// Set the new property value and then publish the project.
// Specify "true" to also check the project in.

draftProject.set_startDate("2013-12-31 09:00:00.000");
var publishJob = draftProject.publish(true);

// Register the job that you want to run on the server and specify the
// timeout duration and callback function.
projContext.waitForQueueAsync(publishJob, 10, QueueJobSent);

This is despite the fact that we are obviously doing the check-out of the project in line 4.

Worse still:

  1. The update will still work for most fields (like Status Date but NOT for Project Owner), despite the error indicating otherwise!
  2. The error is not consistent, some updates work without an error.

Cause and Solution

Turns out the issue is one due to the asynchronous nature of the client side libraries, specifically it looks like when performing the “waitForQueueAsync”  we are actually requesting four things:

  1. Check out Project
  2. Update project value (start date above)
  3. Publish Project
  4. Check project back in

However it seems that steps 2 and 4 don’t quite run in the correct order!  Changing line 10 as follows to NOT check-in after publishing results in a successful update and no error:

var publishJob = draftProject.publish(false);

Then we need to add a separate checkIn() call AFTER the completion of the publish (IE in the callback function ‘QueueJobSent‘ above) and then call waitForQueueAsync again.

Looks like a bug although perhaps not as it is important to keep in mind that the queued async jobs are not guaranteed to be done in the correct order although clearly it usually happens in the order expected.

 

BTW, Yes expect an update for Bulk Edit supporting more built-in fields soon!

Correcting the alignment of PDP Web Parts

Recently I spoke at a Microsoft Project Server event here in Switzerland on the topic of Extending Project Server and using small pieces of JavaScript with jQuery to make little changes for big effects. One of my demos was to correct the following annoyance that many of us have probably come across but have no out of the box way to fix.

Project Detail Pages column alignment issues

Example

pdp screen 1

 

In yellow I have highlighted the issue that I’m talking about in case it is not obvious in the screenshot. As you see the alignment is actually based on the length of the longest custom field displayed in the web part, so as in the above example where we have used separate webparts to break up the webpage with headings the width of each column is unpredictable.

jQuery to the rescue

This is a great example of using jQuery as it shows how ridiculously simple some things can be to change! So let’s walk through the solution here if you’ve not done this before, as you will see the possible usage of this kind of “fix” is vast.

“Debugging” in Chrome or IE

The first thing you need to do to here is to identify the html element(s) in question, so the easiest way to do that is to use the “Inspect Element” feature available in both IE and Chrome browsers (and probably all others too), you’ll find it on the right click menu

pdp screen 2

When selected the inspect element will open up the developer console of your browser and focus on the specific element under the mouse, in this case the “Description” field text label.

pdp screen 3

Now you can browse the page source and as you see each element is highlighted, neat!

Now we can start using jQuery to select our columns to modify, start by dynamically loading the jQuery library using the following script:

var jq = document.createElement('script');
jq.src = "//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js";
document.getElementsByTagName('head')[0].appendChild(jq);

Now that we have jQuery available you can again look at the source and think about what we need to ‘select’, in this case we can see that our Description label is in a h3 inside of a table (tbody > tr > td) and specifically it has a class of “ms-formlabel”. Cool so in jQuery we can now select all such elements like this:

$("tr td.ms-formlabel")

Best thing is that being JavaScript all elements are selected and can be used in an array, but even better we can directly update all items like this:

$("tr td.ms-formlabel").width("300px");

Neat hey? If you run that command in the console immediately all the columns will update to be 300px wide!

Permanently applying fix to the page

So now we have our script let’s add it to a Content Editor Web Part (CEWP) on the page itself, to do that we need to wrap our line in some html which loads the jQuery library and runs the script when the page is ready, like this:

<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script type="text/javascript">

$( document ).ready(function() {
	$("tr td.ms-formlabel").width("300px");
})

</script>

Now edit the PDP web part page, and add a Content Editor web part to it, then simply add the above HTML content to the web part HTML source like so:

First add the Content Editor web part and use the Edit Source ribbon option

pdp screen 4

Then paste in our html script

pdp screen 5

Now each time the page is loaded the script is run and all columns are aligned to 300px looking something like this:

pdp screen 6

Enjoy!

Updating Checked Out Projects with JSOM

Another great thing to come out of Project Conf for me was the connections with people working on some of the same problems that I’ve faced, this is one of those; updating Projects which are checked out is not possible using the same old method that was used with the PSI! (Think about the problem this creates for PDP App Parts!)

Fortunately it is possible thanks to the following undocumented method discovered by Martin Petersen on his blog here:
http://projectserverinsights.blogspot.dk/2014/03/update-project-from-apppart.html

Check it out, and thanks Martin for the solution!

AngularJS and SharePoint App Template

One of the best takeaways for me while working on my Exists App which is built on the MEAN stack was getting to learn AngularJS, better yet at SPConf it was immediately obvious that I’m not the only one who thinks that AngularJS and SharePoint Apps are a perfect fit!

If you don’t know much about AngularJS and want to learn how it relates to SharePoint then I’d highly recommend Jeremy Thake’s excellent session on the topic presented at SPConf where he gives a great introduction to what Angular can do and shows some great examples using SharePoint Apps.

AngularJS Visual Studio Project Template

I’ve been meaning to take this to the next level after toying with creating a basic template app building on the default VS SharePoint hosted App template and adding Angular and some example code in a well defined MVC style template. My intention is to convert this into a VS Template to work along side the default SharePoint App template, but for now I’ve just setup a GitHub repository with a sample (non-templated) project, which looks something like the following:

spngtree

Have a look at the source on GitHub below.

SharePoint / AngularJS App Seed Project

https://github.com/martinlaukkanen/spng-seed

This is a test project designed to be a seed project (or eventually a Visual Studio Template) to implement a SharePoint single-page App built using AngularJS with a heavy emphasis on MVC design patterns.

The project structure is built to be very familiar to any ASP.NET MVC developer, while providing a good starting point for a simple (or complex) SharePoint AngularJS based App.

Initial version notes

  • VS2013 SharePoint Hosted App Project Template
  • NUGET AngularJS-Core package
  • Folder structure as per ASP.NET MVC pattern (/Controllers, /Models, /Views, etc)
  • Sample Model, View and Controller including directive and services to recreate the “‘Hello ‘ + user.get_title()” default template functionality.

Feedback Please!

I’m very keen for any feedback on this so either post below or even on GitHub, in particular my choice of using Directives vs Views or Slices or Includes is somewhat arbitrary. Although it is largely based on my view that this SPA (Single Page App) should not need routes and such as I don’t want to think how that could work within an SPAppWeb.

Finally, I am by no means an Angular expert, in fact I’m brand new to it! So all suggestions on best-practices and relevant design patterns are more than welcome.

Extending Project Online and on-premises with JavaScript Apps Part 3

In this final part of the series we’re going to complete the holiday sync app by importing the selected calendar exceptions into Project Server.

We’ll start from where we finished up in Part 2 with our app loading data from both existing project server calendars and our external web service, so to follow this article make sure you start with the solution from the end of Part 2.

Part 3: Importing data into Enterprise Calendars from our App

Now that we have a list of calendar exceptions that we want to import we need to use some JSOM to actually import those exceptions.

To break-down what we need to do here I’m going to review this JSOM in three steps;

Step 1 Prepare context and get objects to update

// Get the Project Server context
var projContext = PS.ProjectContext.get_current();
// Get our Calendar Collection
var eCalColl = projContext.get_calendars();
var eCalendar = eCalColl.getByGuid(calUid);
var eCalBaseExcep = eCalendar.get_baseCalendarExceptions();

In step 1 we instantiate our object variables using the PS.js JSOM library to first get our calendar collection, then get our calendar by GUID from that collection and finally we get our base calendar exceptions from our calendar.

Next we’ll update those collections.

Step 2 Create the exception(s)

// Step 2 Loop through and add each exception
for (var i = 0; i < exceptions.length; i++) {
  var excepInfo = new PS.CalendarExceptionCreationInformation();
  // Set the exception properties
  excepInfo.set_name(exceptions[i].Descriptor);
  excepInfo.set_start(exceptions[i].Date);
  excepInfo.set_finish(exceptions[i].Date);

  // Finally add the exception info to the base calendar object
  eCalBaseExcep.add(excepInfo);
}
// Update the collection
eCalColl.update();

Now we’re going to create each calendar exception using the PS.CalendarExceptionCreationInformation constructor, you’ll find one of those constructors for most of the objects in JSOM, if you want more on this see MSDN; http://msdn.microsoft.com/en-us/library/office/jj669390.aspx.

Once we have our excepInfo object we set the required properties; Name, Start and Finish, then finish by adding that item to our exception collection before moving to the next exception to be added. Finally we update the calendar collection object with all the exceptions created.

Now as we’re working asynchronously the last step is to execute the above update(s);

Step 3 Update the calendar asynchronously

// Finally asynchronously execute the update 
projContext.executeQueryAsync(Function.createDelegate(this, function () {
	// Success update our grid and finish up
	this.grid.setSelectedRows([]);
	// Display the results and remove the progress msg
	SP.UI.Notify.addNotification("Exceptions added successfully", false);
   }), Function.createDelegate(this, function (call, error) {
	// Handle Error
	alert(error.get_message());
}));

So using the JSOM executeQueryAsync function we execute the change and handle the result, here we simply want to notify the user of the result.

Full code block follows, paste this into the bottom of the App.js:

// Function to add calendar exceptions via JSOM
HolidaySync.prototype.addCalendarException = function (calUid, exceptions) {
    // Show a progress message
    this.notifyMsg = SP.UI.Notify.addNotification('<img src="/_layouts/images/loadingcirclests16.gif" style="vertical-align: top;"/> Importing...', true);

    // Step 1 Get the Project Server context and objects
    var projContext = PS.ProjectContext.get_current();

    // Get our Calendar Collection
    var eCalColl = projContext.get_calendars();
    var eCalendar = eCalColl.getByGuid(calUid);
    var eCalBaseExcep = eCalendar.get_baseCalendarExceptions();
    //CSOM Ref (no JSOM): http://msdn.microsoft.com/en-us/library/office/microsoft.projectserver.client.calendarexceptioncollection_di_pj14mref_members.aspx

    // Step 2 Loop through and add each exception
    for (var i = 0; i < exceptions.length; i++) {
        // Create our Calendar Exception Info
        //http://msdn.microsoft.com/en-us/library/office/jj669390.aspx
        var excepInfo = new PS.CalendarExceptionCreationInformation();

        // Append the year to the name to prevent future duplicates
        var exName = exceptions[i].Descriptor + " " + new Date(exceptions[i].Date).getFullYear();

        // Set the exception properties
        excepInfo.set_name(exName);
        excepInfo.set_start(exceptions[i].Date);
        excepInfo.set_finish(exceptions[i].Date);

        // Finally add the exception to the collection
        eCalBaseExcep.add(excepInfo);
    }

    // Update the collection
    eCalColl.update();

    // Step 3 Asynchronously execute the update 
    projContext.executeQueryAsync(Function.createDelegate(this, function () {
        // Success update our grid and finish up
        this.grid.setSelectedRows([]);

        // Display the results and remove the progress msg
        SP.UI.Notify.addNotification("Exceptions added successfully", false);
        SP.UI.Notify.removeNotification(this.notifyMsg);

    }), Function.createDelegate(this, function (call, error) {
        // Handle Error
        SP.UI.Notify.removeNotification(this.notifyMsg);
        alert(error.get_message());
    }));
};

 

Final bit: Import Button

Okay we’re almost done, just one last thing to do and that is to handle the click of the import button. A bit of jQuery will handle that for us, and in which we need to do just one more thing which is to check for any duplicates being imported to prevent import errors.

Paste the following in the main code block of App.js (should be right after the ‘$(“#importBtn”).click(…’ function:

// Button to import selected exceptions
$("#importBtn").click(Function.createDelegate(this, function () {
	var selectedRows = holidaySync.grid.getSelectedRows();

	// Use helper function to check for any duplicates before importing
	var exceptionsToImport = Helpers.removeDuplicates(selectedRows, holidaySync.data);

	// Import the exceptions
	if (exceptionsToImport.length > 0) {
		holidaySync.addCalendarException(holidaySync.data.calendarId, exceptionsToImport);
	}
	else {
		// Mark all existing
		holidaySync.grid.setSelectedRows([]);
		alert("All selected exceptions already exist.");
	}
}));

What we need to do in that function is call our removeDuplicates helper function which was created back in part 1 of this series. The function will return a filtered array of exceptions that we can then pass as a parameter to our addCalendarException function.

Now we should be able to test it and see the following:

pic1

 

All Done

This app documented here is now available on the SharePoint App store, so please rate it if you use it!

Source Download / Repository

You can browse or download the full source code for the completed app on the following GitHub repository:

https://github.com/martinlaukkanen/holidaysync

Extending Project Online and on-premises with JavaScript Apps Part 2

Following on from Part 1 of this series in which we setup our Holiday Sync app solution in Visual Studio including all pre-requisites, we now have a basic app web page looking the part and containing all controls functioning but without any actual data. In this next part we are going to look into getting that data into our app.

Part 2: Getting data into our Holiday Sync App

The Holiday Sync App is designed to update existing Project Server enterprise calendars with data retrieved from an external web service, so specifically our app will require the following data:

  1. From our Project tenant / instance we need enterprise calendar data
  2. From an external web service we require holiday data

Let’s look at each in turn to see what’s needed to retrieve and utilise the data.

Retrieving Enterprise Calendar Data

In order to update our Enterprise Calendars we firstly need to know some details about them including the names, GUIDs, etc. But secondly as mentioned in part 1 of this series when we looked at our helper script, we need to first remove any duplicates else our import will fail, so to do that we need to get a list of existing base calendar exceptions to compare against.

Getting enterprise data via REST endpoint

Now when working with Project Server data be it online or on-premises an extremely useful resource in 2013 is the REST endpoint, similar to the ODATA endpoints they enable you to quickly view the back-end data in Project Server or SharePoint.

To demonstrate this open your PWA tenant and browse to the following URL (note the _api/ProjectServer/ part which differs from the OData URL):

https://[mytenantname]/sites/devpwa/_api/ProjectServer/Calendars

And what you should see is something like this:

<?xml version="1.0" encoding="utf-8" ?>
<feed xml:base="https://*****.sharepoint.com/sites/devpwa/_api/" xmlns="http://www.w3.org/2005/Atom" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns:georss="http://www.georss.org/georss" xmlns:gml="http://www.opengis.net/gml">
<id>194a1d9e-fae6-4db7-8ef3-330508bd7ee2</id>
<title />
<updated>2014-02-02T21:48:33Z</updated>
<entry>
<id>https://****.sharepoint.com/sites/devpwa/_api/ProjectServer/Calendars('fb512eeb-027f-e311-be84-c48508b296a1')</id>
<category term="PS.Calendar" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
<link rel="edit" href="ProjectServer/Calendars('fb512eeb-027f-e311-be84-c48508b296a1')" />
<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/BaseCalendarExceptions" type="application/atom+xml;type=feed" title="BaseCalendarExceptions" href="ProjectServer/Calendars('fb512eeb-027f-e311-be84-c48508b296a1')/BaseCalendarExceptions" />
<title />
<updated>2014-02-02T21:48:33Z</updated>
<author>
<name />
</author>
<content type="application/xml">
<m:properties>
	<d:Created m:type="Edm.DateTime">2014-01-16T15:07:18.94</d:Created>
	<d:Id m:type="Edm.Guid">fb512eeb-027f-e311-be84-c48508b296a1</d:Id>
	<d:IsStandardCalendar m:type="Edm.Boolean">false</d:IsStandardCalendar>
	<d:Modified m:type="Edm.DateTime">2014-02-02T10:30:18.743</d:Modified>
	<d:Name>Night Shift</d:Name>
</m:properties>
</content>
</entry>
<entry>
<id>https://****.sharepoint.com/sites/devpwa/_api/ProjectServer/Calendars('b6635b2e-e747-4771-a78b-24f7509629d0')</id>
<category term="PS.Calendar" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
<link rel="edit" href="ProjectServer/Calendars('b6635b2e-e747-4771-a78b-24f7509629d0')" />
<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/BaseCalendarExceptions" type="application/atom+xml;type=feed" title="BaseCalendarExceptions" href="ProjectServer/Calendars('b6635b2e-e747-4771-a78b-24f7509629d0')/BaseCalendarExceptions" />
<title />
<updated>2014-02-02T21:48:33Z</updated>
<author>
<name />
</author>
<content type="application/xml">
<m:properties>
	<d:Created m:type="Edm.DateTime">2007-04-26T19:15:13</d:Created>
	<d:Id m:type="Edm.Guid">b6635b2e-e747-4771-a78b-24f7509629d0</d:Id>
	<d:IsStandardCalendar m:type="Edm.Boolean">true</d:IsStandardCalendar>
	<d:Modified m:type="Edm.DateTime">2014-02-02T09:57:25.097</d:Modified>
	<d:Name>Standard</d:Name>
</m:properties>
</content>
</entry>
</feed>

This shows the calendars in this particular instance of PWA, in short this is what you have in the published database in Project Server (TIP: A neat troubleshooting tool for any Admin as a matter of fact!). However one thing that’s missing above is the “BaseCalendarExceptions” data which is referenced as a link in the XML, for example “ProjectServer/Calendars(‘b6635b2e-e747-4771-a78b-24f7509629d0′)/BaseCalendarExceptions”.

Fortunately it’s easy enough to also get that in one query using the $expand REST option:

https://[mytenantname]/sites/devpwa/_api/ProjectServer/Calendars?$expand=BaseCalendarExceptions

Below is a snippet of an exception that is returned in addition to the above XML:

[snip]
<entry>
<id>https://****.sharepoint.com/sites/devpwa/_api/ProjectServer/Calendars('fb512eeb-027f-e311-be84-c48508b296a1')/BaseCalendarExceptions(0)</id>
<category term="PS.BaseCalendarException" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
<link rel="edit" href="ProjectServer/Calendars('fb512eeb-027f-e311-be84-c48508b296a1')/BaseCalendarExceptions(0)" />
<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Calendar" type="application/atom+xml;type=entry" title="Calendar" href="ProjectServer/Calendars('fb512eeb-027f-e311-be84-c48508b296a1')/BaseCalendarExceptions(0)/Calendar" />
<title />
<updated>2014-02-02T22:44:25Z</updated>
<author>
<name />
</author>
<content type="application/xml">
<m:properties>
	<d:Finish m:type="Edm.DateTime">2013-12-25T00:00:00</d:Finish>
	<d:Id m:type="Edm.Int32">0</d:Id>
	<d:Name>Christmas 2013</d:Name>
	<d:RecurrenceDays m:type="Edm.Int32">0</d:RecurrenceDays>
	<d:RecurrenceFrequency m:type="Edm.Int32">1</d:RecurrenceFrequency>
	<d:RecurrenceMonth m:type="Edm.Int32">0</d:RecurrenceMonth>
	<d:RecurrenceMonthDay m:type="Edm.Int32">0</d:RecurrenceMonthDay>
	<d:RecurrenceType m:type="Edm.Int32">0</d:RecurrenceType>
	<d:RecurrenceWeek m:type="Edm.Int32">0</d:RecurrenceWeek>
	<d:Shift1Finish m:type="Edm.Int32">0</d:Shift1Finish>
	<d:Shift1Start m:type="Edm.Int32">0</d:Shift1Start>
	<d:Shift2Finish m:type="Edm.Int32">0</d:Shift2Finish>
	<d:Shift2Start m:type="Edm.Int32">0</d:Shift2Start>
	<d:Shift3Finish m:type="Edm.Int32">0</d:Shift3Finish>
	<d:Shift3Start m:type="Edm.Int32">0</d:Shift3Start>
	<d:Shift4Finish m:type="Edm.Int32">0</d:Shift4Finish>
	<d:Shift4Start m:type="Edm.Int32">0</d:Shift4Start>
	<d:Shift5Finish m:type="Edm.Int32">0</d:Shift5Finish>
	<d:Shift5Start m:type="Edm.Int32">0</d:Shift5Start>
	<d:Start m:type="Edm.DateTime">2013-12-25T00:00:00</d:Start>
</m:properties>
</content>
</entry>
[snip]

Once again that’s basically the raw data that we need in one simple URL request, simple isn’t it? Okay so how do you use that in the app?

Using a jQuery AJAX request to retrieve data

Now we need to use a bit of jQuery where-by we can create an AJAX request to simply download that data directly.

// Function to retrieve a list of Enterprise calendars and exceptions
HolidaySync.prototype.getEntCalData = function () {
    //Example: http://server/PWA/_api/ProjectServer/Calendars?$expand=BaseCalendarExceptions

    var url = _spPageContextInfo.webServerRelativeUrl + "/_api/ProjectServer/Calendars?$expand=BaseCalendarExceptions";

    $.ajax({
        url: url,
        type: "GET",
        context: this,
        contentType: "application/json",
        headers: { "ACCEPT": "application/json;odata=verbose" },
        success: function (data, status, xhr) {
            // Save our data
            this.data.entCalendars = data.d.results;

            // Populate our html dropdown list of calendars when data is ready
            for (var i = 0; i < this.data.entCalendars.length; i++) {
                $("#eCalendarSelect").append($('<option>', {
                    value: this.data.entCalendars[i].Id,
                    text: this.data.entCalendars[i].Name
                }));
            }
            // Also set the default value
            $("#eCalendarSelect").trigger("change");
        },
        error: function (error) {
            // Handle error
            alert("Error: " + error.statusText + " loading Enterprise Calendars");
        }
    });
};

The ajax function takes a few parameters, importantly:

  • The URL – Which is identical to what we used above, ONLY that we have replaced our server name with the global property _spPageContextInfo.webServerRelativeUrl which equates to our App Domain (Not the tenant domain!)
  • The content type which we set to JSON (more on this in a sec)
  • And critically the success / failure handler functions to deal with the result

If we drill into the success function, we can see the following happening:

  1. Firstly we save our data back to the datamodel, notice here that because we can request JSON data we don’t need to do anything to the result but save it to our array previously instantiated (in part 1).
  2. Next we populate our html drop-down list with the data using a for loop and some basic jQuery.
  3. Finally we trigger the change event on the control to ensure that the view and model update.

Finally before we can preview we just have to start the above function in the appropriate place in our app, for that add the below line into the existing main code block (before the closing “});”) in the App.js file.

holidaySync.getEntCalData();

Now hit F5 to preview:

pic04

Our first drop-down is populated!

Retrieving External Web Service Data

Next we need to work with our external data provider, in this case I am using www.holidaywebservice.com for our holiday data, the service is free and provided as-is and suits our needs here (assuming you’re in one of the supported countries!). However in order to utilise a 3rd party web service we have to consider cross-domain scripting restrictions inherent to JavaScript.

SharePoint cross-domain scripting library

pic05

By design JavaScript which is running in a site in for example http://myapp.domain.com cannot communicate with data in something like http://someservice.com. This is to protect against a common vulnerability known as Cross-site scripting (XSS), so in order to use an external service we need to take extra steps.

Fortunately SharePoint provides us with a JSOM library that we can use to securely work with this restriction, it’s known as the Cross-domain scripting library, essentially it allows for our connection to be proxied via an endpoint in our app domain (in a SharePoint REST endpoint), therefore our request never leaves the current domain.

So the next piece of code we need to add to our App.js file is the following function;

// SharePoint Cross domain library helper function
HolidaySync.prototype.crossDomainCall = function (SPHostUrl, callUrl, successCallback, failureCallback) {
    // Use the Cross Domain library 
    // Source: http://blogs.msdn.com/b/officeapps/archive/2012/11/29/solving-cross-domain-problems-in-apps-for-sharepoint.aspx
    $.getScript(SPHostUrl + "/_layouts/15/" + "SP.RequestExecutor.js", Function.createDelegate(this, function () {

        // First construct our JSOM request
        var clientContext = new SP.ClientContext.get_current();

        var crossDomainRequest = new SP.WebRequestInfo();

        crossDomainRequest.set_url(callUrl);
        crossDomainRequest.set_method("GET");

        var response = SP.WebProxy.invoke(clientContext, crossDomainRequest);

        // Execute our request with a callback function
        clientContext.executeQueryAsync(Function.createDelegate(this, function () {
            var statusCode = response.get_statusCode();

            // HTTP status success / failure determines which callback function to send our results to
            if (statusCode === 200) {
                // JavaScript functions are first-class objects (how cool!)
                successCallback(response.get_body());
            }
            else {
                failureCallback(statusCode, response.get_body());
            }
        }));
    }));
};

This helper function is one I sourced / adapted from Humberto Lezama’s blog Solving cross-domain problems in apps for SharePoint, essentially I have just parametized Humbertos’ work into a single function so that we can re-use it in the following functions in our solution.

Enable the Remote Endpoint in App Manifest

Before we can use the above function there is one thing we need to do, which is to add our external URL (http://www.holidaywebservice.com) into the app manifest under Remote Endpoints, as follows;

pic06

 

Retrieve a list of Countries from the external service

Next we need to populate our Select Country drop-down with a list of countries available from our service, to do this paste in the following function which uses our cross domain helper;

// Get available countries from the web service
// Source: http://www.holidaywebservice.com/
HolidaySync.prototype.getCountries = function () {
    // Requires http://www.holidaywebservice.com in AppManifest Remote Endpoints

    var url = "http://www.holidaywebservice.com/HolidayService_v2/HolidayService2.asmx/GetCountriesAvailable";

    // Use the Cross Domain Helper
    this.crossDomainCall(this.urlTokens.SPHostUrl, url, Function.createDelegate(this, function (response) {
        // Save our data
        var xmlData = $.parseXML(response);

        //Using plugin: http://www.fyneworks.com/jquery/xml-to-json/
        var jsonData = $.xml2json(xmlData);
        this.data.countryCodes = jsonData.CountryCode;

        // Populate our dropdown list of Countries when data is ready
        for (var i = 0; i < this.data.countryCodes.length; i++) {
            $("#countrySelect").append($('<option>', {
                value: this.data.countryCodes[i].Code,
                text: this.data.countryCodes[i].Description
            }));
        }
        // Also set the default value
        $("#countrySelect").trigger("change");

    }), Function.createDelegate(this, function (status, error) {
        // Handle failures
        alert(error);
    }));
};

So the above function looks very similar to the jQuery AJAX function used previously, in short we are passing the function some parameters including;

  • Our SharePoint host URL – not the App web, this time we need the actual tenant URL
  • Our full external URL including the SOAP call (see www.holidaywebservice.com for more info on that)
  • Then finally we have our success and failure handlers.

Now in this case our web service is returning XML data, so we need to do some conversion, for which I’ve used a simple jQuery plugin (http://www.fyneworks.com/jquery/xml-to-json/) once converted we populate our drop-down and trigger change to set our default value.

Again as with our getEntCalData function we add the following to the end of our main code block to trigger the function on start:

holidaySync.getCountries();

 

Retrieve the Holiday Data from external service

Now we’re ready to get the holiday data itself, paste in the following function;

// Get Holidays from Web Service
HolidaySync.prototype.getHolidaysForDates = function (fromDate, toDate, country) {
    var startDate = new Date(fromDate);
    var endDate = new Date(toDate);

    var url = "http://www.holidaywebservice.com/HolidayService_v2/HolidayService2.asmx/GetHolidaysForDateRange" +
        "?countryCode=" + country + "&startDate=" + startDate.toISOString() + "&endDate=" + endDate.toISOString();

    // Show a loading message
    this.notifyMsg = SP.UI.Notify.addNotification('<img src="/_layouts/images/loadingcirclests16.gif" style="vertical-align: top;"/> Loading...', true);

    this.crossDomainCall(this.urlTokens.SPHostUrl, url, Function.createDelegate(this, function (response) {
        // Save our data
        var xmlData = $.parseXML(response);

        //Using plugin: http://www.fyneworks.com/jquery/xml-to-json/
        var jsonData = $.xml2json(xmlData);

        // Update the grid contents
        Helpers.updateGridContents(jsonData.Holiday, this.data.holidayData, this.grid);    

        // Remove the notification msg
        SP.UI.Notify.removeNotification(this.notifyMsg);

    }), Function.createDelegate(this, function (status, error) {
        // Handle failures
        SP.UI.Notify.removeNotification(this.notifyMsg);
        alert(error);
    }));
};

You can see a few things happening there, firstly we are taking some parameters which will be populated with our to and from dates as well as our selected country, then once again we call the cross domain helper function with our constructed SOAP request.

Just one extra thing I have added to this function is the notification handlers, that’s using the SharePoint JSOM to display and remove a neat notification message during retrieval, so we don’t have any uncomfortable pauses in our UI.

Lastly handle our button click and preview!

Almost done now, only one thing left and that is to call the above function when we click the Get Holidays button! Paste the following event handler into our Main function;

// Button to get calendar exceptions
$("#getDataBtn").click(Function.createDelegate(this, function () {
	if (!!holidaySync.data.fromDate && !!holidaySync.data.toDate && !!holidaySync.data.country)
		holidaySync.getHolidaysForDates(holidaySync.data.fromDate, holidaySync.data.toDate, holidaySync.data.country);
	else
		alert("Please select dates.");
}));

Other than handling the click event we’re just doing some error checking to ensure all parameters are selected.

Done! Hit F5 to preview.

pic07

 

First select the country (cool!), then pick the to and from dates, then click Get Holidays:

pic08

 

Nice.

Next up, part 3: Importing data into Enterprise Calendars from our App

Now that we have our holiday data we can finally update our enterprise calendars, check back really soon to see the final part of this series!

 

Source Download / Repository

Download the complete source to the above code here:

HolidaySyncDemo2.zip
HolidaySyncDemo2.zip

 

Additionally for those of you who like to skip to the end of this series, you can browse or download the full source code for the completed app on the following GitHub repository:

https://github.com/martinlaukkanen/holidaysync