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:
- From our Project tenant / instance we need enterprise calendar data
- 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:
- 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).
- Next we populate our html drop-down list with the data using a for loop and some basic jQuery.
- 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:
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
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;
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.
First select the country (cool!), then pick the to and from dates, then click Get Holidays:
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:
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:
Greetings ffom Los angeles! I’m bored too tears aat wokrk so
I decided too chesck out your blog on myy iphone during lunch break.
I love the knowledge you preesent here and can’t wait to take a look when I
get home. I’m shocked at how fat your blg loaded on my pone ..
I’m not even using WIFI, justt 3G .. Anyhow, amazing blog!