The Development Stage
In my last post How To Create
a Windows 8 App For SharePoint Part 1 – The Planning Stage we discussed about the reason I chose Windows 8 HTML5 as my development environment and why it’s the best and
the most intuitive environment for SharePoint developers who plan to start creating Win8 apps.
Today we will focus on developing our Win8 app and integratinge it with
SharePoint Online 2013. The app will serve as a search and use The new Search
REST API to pull data from SharePoint 2013 Search engine (FAST) and display
it in our app.
When I decided to connect my Win8 app to SharePoint 2013 Online, it seemed like a straightforward task, all I wanted is to programmatically
connect to SharePoint Online services but reality shows otherwise. I thought the answer could be found in one of WinRT’s
capabilities integrating with online web services.
After exploring this API for several days and even posting a question on Windows Store apps Forums asking
“How to call SharePoint Online 2013's REST API from
Windows 8 HTML5 App?” unfortunately for me, still haven't got any answer. then I decided it's time to take matters in hand and develop my own custom connector to SharePoint Online services.
Before starting developing any solution, it’s important to understand how SharePoint Online authentication works. The authentication "mechanism"
is called Claims based authentication. First we request the token from the STS, we pass the username
and password. then the STS returns a security token. after we got our security token we sent it to SharePoint and at last we get our two cookies called “FedAuth”
and “rtFa” that we need to pass every time we want to request something from SharePoint.
Luckily for me I found this great article
by Wictor Wilén explaining how to do active authentication to Office 365 and SharePoint Online using WCF.
Since the code sample provided was targeting framework 3.5, I tweaked it a little bit to work with framework 4.5, called it Office365ClaimsConnector and
it worked like a charm.
So let’s get to work
Step 1: creating a blank application
Start VS2012 > Other Languages >JavaScript > Blank App. Name it “Win8AppForSharePoint”
Step 2: Creating the Windows Storage & Navigation for our pages
WinRT has a great Storage capabilities and we have access to the following types:
- local - data but only on the current device
- roaming - data that is available to all devices the user has your app installed on
- temporary - data that can be removed by the system at any point after your app is closed
Since our app will only be installed on one device we’ll steak with the local Storage.
Open the “default.html” and add <div id="contentHost"></div> we’ll use this div later for navigation purposes.
Open the “default.js” file and add the following JavaScript code.
(function () {
"use strict";
var app = WinJS.Application;
var activation = Windows.ApplicationModel.Activation;
var localSettings = Windows.Storage.ApplicationData.current.localSettings;
var containerName = "cookieContainer1";
WinJS.strictProcessing();
app.onactivated = function (args) {
if (args.detail.kind === activation.ActivationKind.launch) {
args.setPromise(WinJS.UI.processAll().then(function () {
if (isAuthenticated())
return WinJS.Navigation.navigate("/pages/Search/search.html");
else
return WinJS.Navigation.navigate("/pages/Login/login.html");
}));
}
};
WinJS.Navigation.addEventListener("navigated", function (eventObject) {
var url = eventObject.detail.location;
var host = document.getElementById("contentHost");
WinJS.Utilities.empty(host);
eventObject.detail.setPromise(WinJS.UI.Pages.render(url, host,
eventObject.detail.state).then(function () {
WinJS.Application.sessionState.lastUrl = url;
}));
});
function isAuthenticated() {
if (localSettings.containers.hasKey(containerName)) {
return true;
}
return false;
}
app.start();
})();
In the isAuthenticated
function we're using the Windows.Storage.ApplicationData
object to check if our data (user name, password, url) is already stored and navigate to appropriate page. if Authenticated, navigate straight to Search page, else go through the login page.
Step 3: Creating WCF Service
Create a WCF Service Application Project, Name it “Office365ClaimsService”.
Create two classes, first a static class called DAL, second class called
QueryStringHelper
, we’ll use this class later for query formatting.
Open Ioffice365ClaimsService.cs and add the following code .
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
namespace Office365ClaimsService
{
[ServiceContract]
public interface IOffice365ClaimsService
{
[OperationContract]
[WebInvoke(Method = "POST", ResponseFormat = WebMessageFormat.Json,
RequestFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.WrappedRequest,
UriTemplate = "/authentication")]
TokenContainer Authentication(string url, string userName, string password);
[OperationContract]
[WebInvoke(Method = "POST", ResponseFormat = WebMessageFormat.Json,
RequestFormat=WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.WrappedRequest,
UriTemplate = "/search")]
string GetSearchData(string FedAuth, string RtFa, string url, string query);
}
[DataContract]
public class TokenContainer
{
[DataMember]
public string FedAuth { get; set; }
[DataMember]
public string RtFa { get; set; }
}
}
We’ll have twp Services, First is an Authentication service that receives the following parameters : url, userName, password.
The second service is the search service receiving the following parameters : FedAuth, RtFa, url, query
Since we’re working with JavaScript and handle some sensitive data, The 2 Services use JSON as the Request Format & Response format and the type method is POST.
Open the DAL.cs file and add the following code.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web;
using Office365ClaimsConnector;
namespace Office365ClaimsService
{
public static class DAL
{
public static TokenContainer getTokens(string url, string userName, string password)
{
TokenContainer tc = new TokenContainer();
MsOnlineClaimsHelper claimsHelper = new MsOnlineClaimsHelper(url, userName,password);
CookieContainer cc=claimsHelper.GetCookieContainer();
tc.FedAuth = cc.GetCookies(new Uri(url))["FedAuth"].Value;
tc.RtFa = cc.GetCookies(new Uri(url))["rtFa"].Value;
return tc;
}
public static string GetDataFromSP(string FedAuth,string RtFa,
string url, QueryType type, string query)
{
string responseJson = string.Empty;
var uri = QueryStringHelper.BuildQuery(QueryType.Search, url, query);
CookieContainer cc = GetCookieContainer(FedAuth, RtFa, new Uri(url));
Uri queryUri = new Uri(uri.AbsolutePath);
var request = HttpWebRequest.CreateHttp(uri);
request.Method = "GET";
var accept = "application/json";
if (accept != null)
request.Accept = accept;
request.CookieContainer=cc;
var response = request.GetResponse();
Stream res = response.GetResponseStream();
using (var reader = new StreamReader(res))
{
responseJson = reader.ReadToEnd();
}
return responseJson;
}
private static CookieContainer GetCookieContainer(string FedAuth, string rtFa, Uri uri)
{
CookieContainer _cachedCookieContainer = null;
DateTime _expires = DateTime.MinValue;
CookieContainer cc = new CookieContainer();
if (_cachedCookieContainer == null || DateTime.Now > _expires)
{
Cookie samlAuth = new Cookie("FedAuth", FedAuth)
{
Expires = _expires,
Path = "/",
Secure = uri.Scheme == "https",
HttpOnly = true,
Domain = uri.Host
};
cc.Add(samlAuth);
Cookie rtFaCookie = new Cookie("rtFA", rtFa)
{
Expires = _expires,
Path = "/",
Secure = uri.Scheme == "https",
HttpOnly = true,
Domain = uri.Host
};
cc.Add(rtFaCookie);
}
_cachedCookieContainer = cc;
return cc;
}
}
}
You’ll need to add a reference to Office365ClaimsConnector project. as a reminder, Office365ClaimsConnector is the code sample I found in this great article by Wictor Wilén that actually does all the Authentication work using WCF bindings and contracts.
Using method GetToken we pass the following parameters: url, username and password to our Office365ClaimsConnector.MsOnlineClaimsHelper class that eventually will return our tokens to the service and back to our client.
The method GetDataFromSP is responsible for our query request, it accepts the following parameters: FedAuth,RtFa, url, search type and query. basically we pass the 2 tokens to the CookieContainer object and finally to the HttpWebRequest object along with the url and the query. If our request succeeded we’ll get our search results in a nice JSON format.
Open the QueryStringHelper.cs file and add the following code.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Office365ClaimsService
{
public enum QueryType
{
Search,
Lists,
}
public static class QueryStringHelper
{
public static Uri BuildQuery(QueryType type,string url,string query)
{
UriBuilder bldr = new UriBuilder(url);
switch (type)
{
case QueryType.Search:
string restQuery = "/_api/search/query";
bldr.Path +=restQuery;
bldr.Query = "querytext='" + query + "'";
break;
case QueryType.Lists:
break;
default:
break;
}
return bldr.Uri;
}
}
}
I created this class for future implementation for other scenarios besides Search, like CRUD operations for lists etc..
Step 4: creating Login page
in “Win8AppForSharePoint" project Create a folder called pages and inside that folder create another folder called Login with 3 files: login.css ,login.html and login.js
Open the “login.html” file and add the following HTML:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>Connect to Sharepoint Online</title>
<link rel="stylesheet" href="http://www.codeproject.com/Microsoft.WinJS.1.0.RC/css/ui-light.css" />
<script src="http://www.codeproject.com/Microsoft.WinJS.1.0.RC/js/base.js"></script> 1:
2: <script src="http://www.codeproject.com/Microsoft.WinJS.1.0.RC/js/ui.js">
1: </script>
2:
3: <link rel="stylesheet" href="http://www.codeproject.com/pages/Login/login.css" />
4: <script src="http://www.codeproject.com/pages/Login/login.js">
</script>
</head>
<body>
<div class="detailPage fragment">
<header role="banner" aria-label="Header content">
<div class="titleArea">
<h1 class="pageTitle win-type-xx-large"></h1>
</div>
</header>
<section role="main" aria-label="Main content">
<article>
<div>
<header>
<h2 class="title win-type-x-large"></h2>
</header>
<div class="image"></div>
<div class="content"></div>
<table border="0" style="width: 252px; height: 60px">
<tr>
<td><label>Address:</label></td>
<td><input id="address" type="text"/></td>
</tr>
<tr>
<td><label>Customer ID:</label></td>
<td><input id="username" type="text"/></td>
</tr>
<tr>
<td><label>Password:</label></td>
<td style="height: 40px"><input id="password" type="password"/></td>
</tr>
<tr>
<td>
<label>
<input class="submitButton" type="submit" name="Submit" value="Submit" />
</label>
</td>
</tr>
<tr>
<td>
<progress class="win-ring win-large progress"></progress>
</td>
</tr>
</table>
</div>
</article>
</section>
</div>
</body>
</html>
The HTML file holds a simple table containing the login box that has Address , User id and Password. it also containing a progress control (out of the box),
I also added another class called ‘progress’ to hide the control and when the Login button is pressed and our request is sent to a remote server to get the access tokens,
we’ll display the loading image.
Open the “login.css” file and add the following css.
.detailPage section[role=main] {
-ms-grid-row: 2;
display: block;
width: 100%;
height: 100%;
overflow-x: auto;
}
.detailPage section[role=main] article {
column-fill: auto;
column-width: 800px;
column-gap: 80px;
height: auto;
width: 800px;
margin-left: 120px;
}
.detailPage section[role=main] h1 {
margin-top: 0px;
margin-bottom: 20px;
}
.detailPage section[role=main] .image {
width: 800px;
height: 100px;
}
.detailPage section[role=main] p {
margin-right: 20px;
}
@media screen and (-ms-view-state: snapped) {
.detailPage .win-contentTitle {
font-size: 11pt;
line-height: 15pt;
}
.detailPage header[role=banner] {
display: -ms-grid;
-ms-grid-columns: 44px 1fr;
-ms-grid-rows: 1fr;
}
.detailPage section[role=main] .image {
width: 260px;
height: 140px;
}
.detailPage section[role=main] article {
display: -ms-grid;
-ms-grid-columns: 290px 1fr;
-ms-grid-rows: 1fr;
-ms-grid-row: 2;
width: 290px;
margin-left: 20px;
overflow-y: auto;
overflow-x: hidden;
}
}
.progress {
display:none;
}
The Login box:
Open the “login.js” file and add the following JavaScript code.
(function () {
var loggedIn;
var localSettings = Windows.Storage.ApplicationData.current.localSettings;
var containerName = "exampleContainer1";
var nav = WinJS.Navigation;
var container;
var FedAuth;
var RtFa;
var address;
var username;
var password;
"use strict";
var page = WinJS.UI.Pages.define("/pages/Login/login.html", {
ready: function (element, options) {
element.querySelector('.submitButton').addEventListener('click', submitLogin, false);
}
});
function submitLogin() {
try {
if (address|| username|| password)
{
var messagedialogpopup = new Windows.UI.Popups.MessageDialog(
"Input can not be empty!", "Error");
messagedialogpopup.showAsync();
return;
}
document.querySelector(".progress").style.display = "block";
address = document.getElementById("address").value;
username = document.getElementById("username").value;
password = document.getElementById("password").value;
var json = JSON.stringify({ "url": address,
"userName": username, "password": password });
}
catch (err) {
}
WinJS.xhr({
type: "POST",
url: "http://localhost:2738/Office365ClaimsService.svc/Authentication",
headers: { "content-type": "application/json; charset=utf-8" },
data: json,
}).done(loginSuccess, loginFaliure, loginProgress);
};
function loginSuccess(request)
{
var obtainedData = window.JSON.parse(request.responseText);
var container = localSettings.createContainer(containerName,
Windows.Storage.ApplicationDataCreateDisposition.always);
if (localSettings.containers.hasKey(containerName)) {
localSettings.containers.lookup(containerName).values["FedAuth"] = obtainedData.FedAuth;
localSettings.containers.lookup(containerName).values["RtFa"] = obtainedData.RtFa;
localSettings.containers.lookup(containerName).values["Url"] = address;
}
WinJS.Navigation.navigate('/pages/Search/search.html');
}
function loginFaliure(request)
{
document.querySelector(".progress").style.display = "none";
var messagedialogpopup = new Windows.UI.Popups.MessageDialog(
"An error occurred!", "Error");
messagedialogpopup.showAsync();
return false;
}
function loginProgress(request) {
}
})();
Using the WinJS.UI.Pages.define object we basically get all HTML elements located
on the page we requested. using querySelector we get the Submit button element and add the EventListener for our button click. if one of the fields is empty we add
MessageDialog telling the user that his input is empty. After collecting the data (address, username, password) from the Login box, we create a JSON text using JSON.stringify and pass it to our WCF Authentication service, using WinJS.xhr function we get a Promise(callback). if we successfully logged-in we’ll get our two tokens/cookies “FedAuth” and “rtFa” that we’ll use for all of ours requests from SharePoint Online.
It’s worth mentioning that you can’t use “alert” for debugging or any pop-up notifications anymore as WinJS doesn't support it. for debugging use console.log() and for notification use MessageDialog.
We create a key for the Storage object we talked about, pass our tokens and url to the Storage object and navigate to Search page.
Step 5: creating Search page, Template and Binding
in “Win8AppForSharePoint" project create a folder called Search with 3 files: search.css ,search.html and search.js
Open the “search.html” file and add the following html.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>SharePoint Search</title>
<link href="http://www.codeproject.com/Microsoft.WinJS.1.0.RC/css/ui-light.css" rel="stylesheet" />
<script src="http://www.codeproject.com/Microsoft.WinJS.1.0.RC/js/base.js"></script> 1:
2: <script src="http://www.codeproject.com/Microsoft.WinJS.1.0.RC/js/ui.js">
1: </script>
2:
3:
4: <link href="http://www.codeproject.com/css/default.css" rel="stylesheet" />
5: <link rel="stylesheet" href="http://www.codeproject.com/pages/Search/search.css" />
6: <script src="http://www.codeproject.com/pages/Search/search.js">
1: </script>
2: <script src="http://www.codeproject.com/js/default.js">
1: </script>
2: <script src="http://www.codeproject.com/js/navigator.js">
</script>
</head>
<body>
<div class="content" aria-label="Main content" role="main">
<img class="spLogo" src="../../images/sharepoint-logo.jpg" />
<div id="searchLbl">SharePoint Online Search</div>
<input id="searchBox" name="search" type="text" placeholder="Search..." />
<button id="searchBtn" type="button">Search</button>
<div><progress class="win-ring win-large progress"></progress></div>
<div id="searchResultsTemplate" data-win-control="WinJS.Binding.Template">
<div class="itemResult">
<div><h2><a class="resultTitleLink" data-win-bind="innerText: title;source: url"></a></h2></div>
<span> <a class="resultUrl" data-win-bind="innerText: url;source: url" ></a>
<span class="resultDate" data-win-bind="innerText: date"></span>
</span>
<div class="resultContent" data-win-bind="innerText: content"></div>
</div>
</div>
<div id="searchListView" data-win-control="WinJS.UI.ListView"
data-win-options="{itemTemplate:searchResultsTemplate,selectionMode:'none', layout: { type: WinJS.UI.ListLayout }}"></div>
</div>
</body>
</html>
The HTML will hold 3 section:
- HTML elements containing our Search box and the “Search” button.
- DIV marked with data-win-control="WinJS.Binding.Template" attribute tells WinJS to treat it like a template, element marked with data-win-bind=" " attribute inside the template will help the binding engine to know which JavaScript properties from the data source to map to the appropriate HTML. read more about it
here.
- DIV marked with data-win-control="WinJS.UI.ListView" attribute
*transforms this simple DIV to JavaScript ListView control. inside the ListView
control we have another attribute called
data-win-options="{}" that we’ll use to tell the control which template to bind, which Layout etc.. read more about it
here.
*This operation is done thanks to a JavaScript code: WinJS.UI.processAll();.
Since we added the ListView to a Page control,
we don't need to call WinJS.UI.processAll because the Page control does it for us.
Open the “search.css” file and add the following CSS:
#searchBox {
width: 500px;
height: 35px;
padding: 10px 20px 10px 10px;
margin: 1px auto 50px;
}
#searchLbl {
font-family:'Segoe UI Symbol';
font-weight: bold;
}
.content
{
text-align: center; padding-top: 50px;
}
.pagetitle{
}
.resultTitleLink{
text-decoration: underline;
}
.resultAuthor{
}
.resultDate{
color:#777;
font-size: small;
}
.resultUrl{
color:#388222;
font-size: small;
}
.resultContent {
font-size: small;
}
.spLogo {
height: 103px;
}
.progress {
display:none;
margin: 1px auto 50px;
}
I also added an Office image and the Search box got a nice and simple search based UI :
Open the “search.js” file and add the following JavaScript code.
(function () {
"use strict";
var localSettings = Windows.Storage.ApplicationData.current.localSettings;
var containerName = "exampleContainer1";
var searchResults = new Array();
var page = WinJS.UI.Pages.define("/pages/Search/search.html", {
ready: function (element, options) {
document.getElementById("searchBtn").addEventListener("click", getSearchData, false);
}
});
function getSearchData()
{
if (localSettings.containers.hasKey(containerName)) {
var FedAuth = localSettings.containers.lookup(containerName).values["FedAuth"];
var RtFa = localSettings.containers.lookup(containerName).values["RtFa"];
var Url = localSettings.containers.lookup(containerName).values["Url"];
var Query = document.getElementById("searchBox").value;
if (Query == "") {
var messagedialogpopup = new Windows.UI.Popups.MessageDialog("Input can not be empty!","Error");
messagedialogpopup.showAsync();
return;
}
var lstView = document.getElementById("searchListView").winControl;
searchResults = [];
var dataList = new WinJS.Binding.List(searchResults);
lstView.itemDataSource = dataList.dataSource;
document.querySelector(".progress").style.display = "block";
var json = JSON.stringify({ "FedAuth": FedAuth, "RtFa": RtFa, "url": Url, "query": Query });
WinJS.xhr({
type: "POST",
url: "http://localhost:2738/Office365ClaimsService.svc/search",
headers: { "content-type": "application/json; charset=utf-8" },
data: json,
}).done(processData, dataError);
}
}
function processData(response) {
document.querySelector(".progress").style.display = "none";
var data = JSON.parse(response.response);
data = JSON.parse(data);
var results = data.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results;
for (var i = 0, len = results.length; i < len; i++) {
var item = results[i].Cells;
var date = new Date(item.results[8].Value);
var resultItem = {
author: item.results[4].Value,
title: item.results[3].Value,
date: date.toDateString(),
url: item.results[6].Value,
content: item.results[10].Value,
};
searchResults.push(resultItem);
}
var lstView = document.getElementById("searchListView").winControl;
var dataList = new WinJS.Binding.List(searchResults);
lstView.itemDataSource = dataList.dataSource;
}
function dataError(data)
{
document.querySelector(".progress").style.display = "none";
var messagedialogpopup = new Windows.UI.Popups.MessageDialog("Error!", "Error");
messagedialogpopup.showAsync();
var d = data;
}; }
})();
After we got all the html elements using WinJS.UI.Pages.define object. we get the Search button element and add the EventListener for our button click. We grab all the data from our storage object using a key , of course if our query is empty we’ll throw a pop-up notifying the user .
Since our search data will change every time we click our search button we need to send an empty array list to ListView control to clear the data. we use a binding list that excepts our JavaScript array and connecting it to ListView itemDataSource to display the data. after collecting the data ( FedAuth,RtFa, url, query ) from the storage object, we create a JSON text using JSON.stringify and pass it to our WCF Search service we talked about earlier to pull data from SharePoint Online using Search REST API. if the operation succeeded we get our JSON string, then we parse it and start populating the JavaScript list array, init our binding list with our data and connect it to ListView.itemDataSource to display the content that will be showed in a template we created on the HTML page.
The final result:
Step 6: Summary
Today we successfully created our Windows 8 App integrated with SharePoint Online services. We programmatically connected to SharePoint using Claims-based Authentication, got our data with the help of SharePoint search REST API and displayed our search results. of course for displaying the search results I could iterate the JSON object and start appending the HTML elements but instead we used the Template and the Binding capabilities to render our data in an easy and simple way that WinJS offers us.
We've only seen the tip of the iceberg, Windows 8 app development is a rich and exciting world so I strongly advise you to start exploring it .
Soon we’ll try to extend this app to interact with the operation system.
You can download the full project on CodePlex.
I hope this post was helpful .
CodeProject