Azure hosted SharePoint apps using AngularJS and WebAPI – Part 3


This is final post of a three part series on exploring Azure hosted SharePoint apps using AngularJS and WebAPI.

Part 1: Why Cloud and AppModel?

Part 2: SQLAzure data via WebAPI:

Part 3: Azure hosted apps using AngularJS and WebAPI

Why AngularJS:

 Traditionally managed server-side programming used to develop enterprise apps. But now this pattern is changing in the enterprise application development industry.  It’s a change away from logic on the server side, and towards logic on the client side. To address these complex business logic Javascript should evolve from just a language to make DOM manipulations to powerful client side MVC/MVVM framework. AngularJS framework makes this shift at ease.

AngularVSKnckout

 

 

 

There are also few other frameworks on the same course but AngularJS is a clear leader owing to these facts:

1)       Dependency injection:

This is huge and my personal favorite. Using this, custom modules can be injected just like adding assemblies on the server side code. This makes life little easier design MVC/MVVM based projects with separation of concern, modularity etc. An Analogy to server side code is represented below

Step38_ServerClient

 

 

2)      HTML templating:

Over the past few years, Single Page App’s (SPA’s) are gaining of a lot of traction from front end developers and end users. HTML templating feature in Angular makes it possible to easily switch the HTML in SPA’s.

 3)      Two way binding:

Fellow Silverlight developers can understand the complexity of two-way binding using propertychange events back in the old days. But AngularJS databinding makes this right OOB

 4)      Community support:

Google continues to develop and maintain the library. Large community of developers has embraced the framework so ample support is available on Stackoverflow and AngularJS official site.

5)      Advance error handling and logging features

Why AngularJS on Provider hosted App:

 Instead of using server side code on Provider hosted app, client side code is used for a sole reason to utilize AngularJS framework. Some of the advantages include

  •  Single Page App (SPA)
  • Clean URL for easy navigation
  • Responsive design
  • No server side code
  • Scalable for further enhancements

Future enhancements may include:

1)       Adding Chrome controls to give SharePoint look and feel

2)      Authentication for WebAPI

3)      Functions to delete/update O365 and WebApi Data. etc

 

Design of this app is based on Hot Towel app from John Papa and the pluralsight course from Andrew Connell.

It requires a bit of learning to understand the AngularJS. Go through these resources for deep understanding

http://pluralsight.com/training/courses/TableOfContents?courseName=building-sharepoint-apps-spa-angularjs

http://pluralsight.com/training/courses/TableOfContents?courseName=build-apps-angular-breeze

http://www.johnpapa.net/hot-towel-angular/

SPAngular APP:

1)       Create a new provider host app project in VS2013

2)      New Project -> App for SharePoint -> Provider Hosted App -> ASP.NET Web Forms Application -> Use Windows Azure Access Control Service ->finish

Step39_ProviderHosted

3)      Now click the web project -> Manage NuGet Packages

Nugets

4)      Search “hot towel” and install HotTowel.Angular.

Nugets_HotTowel

 

5)      Main benefit of using this project template is all the heavy lifting of implementing MVC project structure, logging, exception handling, angularJS, configuration comes OOB. This makes this bootstrap project easily extensible for custom functionalities.

Step25_App3

6)      By default App project creates Query String tokens to store URL’s of Appweb , hostweb etc for reference in the future.

Step35_AppXML

7)      AngularJS navigation is based on the URL manipulations. Its little hard to implement the navigation with these long URL’s. So the idea is to use a applauncherpage to store the Querystring values to cookies for future use and redirect to the main page with clean URL.

8)      Create applauncher.html and applauncher.js. Create an empty controller and all the JS references from index.html.


<!DOCTYPEhtml><!--// ** Step 1: Add AppLauncher:  **//-->

<htmldata-ng-app="app">

<head>

<title>App Launcher</title>

</head>

<bodydata-ng-controller="applauncher as vm">

 

<!--// ** Step 2: Add Angular libs:  **//-->

<!-- Vendor Scripts -->

<scriptsrc="../scripts/jquery-2.1.1.js"></script>

<scriptsrc="../Scripts/jquery.extensions.js"></script>

<scriptsrc="../scripts/jquery.cookie.js"></script>

<scriptsrc="../scripts/angular.js"></script>

<scriptsrc="../scripts/angular-animate.js"></script>

<scriptsrc="../scripts/angular-resource.js"></script>

<scriptsrc="../scripts/angular-route.js"></script>

<scriptsrc="../scripts/angular-sanitize.js"></script>

<scriptsrc="../scripts/bootstrap.js"></script>

<scriptsrc="../scripts/toastr.js"></script>

<scriptsrc="../scripts/moment.js"></script>

<scriptsrc="../scripts/ui-bootstrap-tpls-0.10.0.js"></script>

<scriptsrc="../scripts/spin.js"></script>

 

<!-- Bootstrapping -->

<scriptsrc="../app/app.js"></script>

<scriptsrc="../app/config.js"></script>

<scriptsrc="../app/config.exceptionHandler.js"></script>

<scriptsrc="../app/config.route.js"></script>

 

<!-- common Modules -->

<scriptsrc="../app/common/common.js"></script>

<scriptsrc="../app/common/logger.js"></script>

<scriptsrc="../app/common/spinner.js"></script>

 

<!-- common.bootstrap Modules -->

<scriptsrc="../app/common/bootstrap/bootstrap.dialog.js"></script>

 

<!-- app -->

<scriptsrc="../app/admin/admin.js"></script>

<scriptsrc="../app/dashboard/dashboard.js"></script>

<scriptsrc="../app/layout/shell.js"></script>

<scriptsrc="../app/layout/sidebar.js"></script>

 

<!--// ** Step 5: Create Add references **// -->

<!-- app Services -->

<scriptsrc="../app/applauncher.js"></script>

<scriptsrc="../app/services/spappcontext.js"></script>

 

</body>

</html>

 

 

9)      In the applauncher.js call spappcontext angular service.


// ** Step 3: Add AppLauncher JS:  **//

(function(){

'use strict';

var controllerId ='applauncher';

var app = angular.module('app');

app.controller(controllerId,['common','spappcontext', applauncher]);

 

function applauncher(common, spappcontext){

var getLogFn = common.logger.getLogFn;

var log = getLogFn(controllerId);

activate();

function activate(){

common.activateController([], controllerId)

.then(function(){ log('Activated App Launcher View');});

}

 

}

})();

10)   Create SPappcontext.js and create createSPAppContext()and loadSPAppContext() functions to read the query string and store it to cookies. This also redirects to \index.html, a Single page app.


(function(){

'use strict';

var serviceId ='spappcontext';

var app = angular.module('app');

app.service(serviceId,['common','$window','$location', spappcontext]);

function spappcontext(common, $window, $location){

var getLogFn = common.logger.getLogFn;

var log = getLogFn(serviceId);

var service =this;

var spweb ={

providerUrl:'',

SPAppWebUrl:'',

SPHostUrl:''

};

service.hostWeb = spweb;

init();

function init(){

var test = jQuery.getQueryStringValue('SPHostUrl');

if(decodeURIComponent(jQuery.getQueryStringValue('SPHostUrl'))==="undefined")

{

loadSPAppContext();

}

else

{

createSPAppContext();

}

}

function loadSPAppContext(){

log('loading spcontext cookie');

service.hostWeb.providerUrl = $.cookie("ProviderUrl");

service.hostWeb.SPAppWebUrl = $.cookie("SPAppWebUrl");

service.hostWeb.SPHostUrl = $.cookie("SPHostUrl");

}

function createSPAppContext(){

log('writing spcontext cookie');

var ProviderUrl = $window.location.protocol +"//"+ $window.location.host;

$.cookie("ProviderUrl", ProviderUrl,{ path:'/'});

var appWebUrl = decodeURIComponent(jQuery.getQueryStringValue('SPAppWebUrl'));

$.cookie("SPAppWebUrl", appWebUrl,{ path:'/'});

var url = decodeURIComponent(jQuery.getQueryStringValue('SPHostUrl'));

$.cookie('SPHostUrl', url,{ path:'/'});

$window.location.href = ProviderUrl +"/index.html";

}

}

})();

11)    Now add host web SP JS libraries to index.html to for make JSOM calls against host web.


<!--// ** Step 7: Add SharePoint libs for JSOM calls **// -->

<!--SharePoint-->

<scriptsrc="https://spbreed.sharepoint.com/sites/dev/_layouts/15/SP.RunTime.js"></script>

<scriptsrc="https://spbreed.sharepoint.com/sites/dev/_layouts/15/SP.js"></script>

<scriptsrc="https://spbreed.sharepoint.com/sites/dev/_layouts/15/SP.RequestExecutor.js"></script>

Now add the following code to web.config remove the static file issue on described here http://rainerat.spirit.de/2012/09/03/SharePoint-2013-App-HTTP-Error-405.0/

<!--// ** Step: 8: To Resolve IIS Static file issue**//-->

<system.webServer>

<modulesrunAllManagedModulesForAllRequests="true"/>

<handlers>

<addname="AspNetStaticFileHandler" path="*" verb="*" type="System.Web.StaticFileHandler" />

<addname="StaticHTML" path="*.html" verb="GET,HEAD,POST,DEBUG,TRACE" modules="StaticFileModule" resourceType="File" requireAccess="Read" />

</handlers>

</system.webServer>

[/xml]

12)   Now use JSOM to call host web and REST calls to WebAPI. $q.defer() objects are used to  pass promise to the calling function. This enables clients to complete the DOM structure without waiting for results.( Host web contains a demo contact list with title “DemoCustomer”
<pre>

// ** Step 6: Create Datacontext Service **//

(function(){

'use strict';

&nbsp;

var serviceId ='datacontext';

angular.module('app').factory(serviceId,['$rootScope','$resource','spappcontext','common', datacontext]);

&nbsp;

function datacontext($rootScope,$resource, spappcontext,common){

var $q = common.$q;

&nbsp;

&nbsp;

var service ={

getPeople: getPeople,

getMessageCount: getMessageCount,

getUserName: getUserName,

getCustomers: getCustomers,

getSPContacts: getSPContacts

};

&nbsp;

return service;

&nbsp;

function getCustomers(){

&nbsp;

var dfd = $q.defer();

var webApi = $resource('http://customerwebapi.cloudapp.net/api/Customer/:Id',{ Id:"@Id"},{ update:{ method:'PUT'}});

//   var webApi = $resource('http://localhost/AppWeb/api/Customer/:Id', { Id: "@Id" }, { update: { method: 'PUT' } });

webApi.query(function(data){

var customers = data;

customers.splice(4,847)

dfd.resolve(customers);

},function(error){

console.log(error);

dfd.reject(error);

});

return dfd.promise;

}

&nbsp;

function getUserName(){

&nbsp;

var dfd = $q.defer();

&nbsp;

// Standard function to get hostweb

var ctx =new SP.ClientContext(spappcontext.hostWeb.SPAppWebUrl);

var factory =new SP.ProxyWebRequestExecutorFactory(spappcontext.hostWeb.SPAppWebUrl);

ctx.set_webRequestExecutorFactory(factory);

var hostWebctx =new SP.AppContextSite(ctx, spappcontext.hostWeb.SPHostUrl);

&nbsp;

var appWeb = ctx.get_web();

var hostWeb = hostWebctx.get_web();

var currentAppWebUser = appWeb.get_currentUser();

var currentHostWebUser = hostWeb.get_currentUser();

&nbsp;

ctx.load(appWeb);

ctx.load(hostWeb);

ctx.load(currentAppWebUser);

ctx.load(currentHostWebUser);

&nbsp;

ctx.executeQueryAsync(function(){

var userName = currentAppWebUser.get_title();

dfd.resolve(userName);

},function(sender, args){

console.log(args.get_message()+" "+ args.get_stackTrace());

dfd.reject(args.get_message());

});

&nbsp;

return dfd.promise;

}

&nbsp;

function getSPContacts()

{

var dfd = $q.defer();

// Standard function to get hostweb

var ctx =new SP.ClientContext(spappcontext.hostWeb.SPAppWebUrl);

var factory =new SP.ProxyWebRequestExecutorFactory(spappcontext.hostWeb.SPAppWebUrl);

ctx.set_webRequestExecutorFactory(factory);

var hostWebctx =new SP.AppContextSite(ctx, spappcontext.hostWeb.SPHostUrl);

&nbsp;

var appWeb = ctx.get_web();

var hostWeb = hostWebctx.get_web();

&nbsp;

var ContactList = hostWeb.get_lists().getByTitle('DemoCustomer');

var contactListItems = ContactList.getItems(SP.CamlQuery.createAllItemsQuery());

ctx.load(ContactList);

ctx.load(contactListItems);

ctx.executeQueryAsync(function(){

var enumerator = contactListItems.getEnumerator();

var SPContacts =[];

while(enumerator.moveNext()){

var currentItem = enumerator.get_current();

SPContacts.push({"FirstName": currentItem.get_item('FirstName'),"LastName": currentItem.get_item('Title')});

}

var SPContactsObj = SPContacts;

&nbsp;

dfd.resolve(SPContactsObj);

},function(sender, args){

console.log(args.get_message()+" "+ args.get_stackTrace());

dfd.reject(args.get_message());

});

return dfd.promise;

}

}

})();

13)   Now modify dashboard.js to call data service functions defined above and add spinner while waiting for results.


(function(){

'use strict';

var controllerId ='dashboard';

angular.module('app').controller(controllerId,['common','datacontext', dashboard]);

&nbsp;

function dashboard(common, datacontext){

var getLogFn = common.logger.getLogFn;

var log = getLogFn(controllerId);

&nbsp;

var vm =this;

vm.news ={

title:'Hot Towel Angular',

description:'Hot Towel Angular is a SPA template for Angular developers.'

};

vm.messageCount =0;

vm.people =[];

vm.SPContacts =[];

vm.Customers =[];

vm.title ='Dashboard';

vm.busyMessage ='Please wait ...';

vm.isBusy =true;

vm.spinnerOptions ={

radius:40,

lines:7,

length:0,

width:30,

speed:1.7,

corners:1.0,

trail:100,

color:'#F58A00'

};

&nbsp;

activate();

&nbsp;

function activate(){

var promises =[getMessageCount(), getPeople(), getSPContacts(), getCustomers()];

common.activateController(promises, controllerId)

.then(function(){ log('Activated Dashboard View');});

}

&nbsp;

function getCustomers(){

toggleSpinner(true);

return datacontext.getCustomers().then(function(data){

toggleSpinner(false);

return vm.Customers = data;

});

}

&nbsp;

function getMessageCount(){

return datacontext.getMessageCount().then(function(data){

return vm.messageCount = data;

});

}

&nbsp;

function getPeople(){

return datacontext.getPeople().then(function(data){

return vm.people = data;

});

&nbsp;

}

&nbsp;

function getSPContacts(){

&nbsp;

return datacontext.getSPContacts().then(function(data){

return vm.SPContacts = data;

});

&nbsp;

}

&nbsp;

function toggleSpinner(on){ vm.isBusy = on;}

}

})();

14)   Now add remote endpoint http://customerwebapi.cloudapp.net to the appmanifest.xml

Step36_AppXML_Remoteendpoint

15)   Now navigate to /_layouts/15/appregnew.aspx to generate client Id and client secret. Note down these values.

Step28_AppregNew

16)   Now right click the web project and select publish to select a new publishing profile

Step29_PublishingProfile

17)   Seed the values of the ClientID and Client Secret noted earlier.

Step30_PublishingProfile

18)   Now select the created profile and deploy to Azure Website.

Step31_PublishingProfile

19)   Add a site name and click create

Step32_PublishingProfile

20)   Keep the default settings and click publish.

Step33_PublishingProfile

21)    Now package the app and upload the app to the Office 365 site.

Step34_PublishingProfile

22)   Now clicking the app will navigate to the provider hosted app with displays data from both Office 365 and WebAPI.

Step2_Responsive

This project is hosted in codeplex for download: https://spangular.codeplex.com/ 

Advertisements

One thought on “Azure hosted SharePoint apps using AngularJS and WebAPI – Part 3

  1. Hi Kathik,
    I have tried to implement your walkthrough so as to gain some insight into SP provider hosted apps and the HotTowel template but each time time this line of code (app.service(serviceId, [‘common’,’$window’,’$location’, spappcontext]);) in the spappcontext file is run I get an error. The error is: Error: $injector:unpr Unknown provider: $$asyncCallbackProvider <- $$asyncCallback <- $animate <- $compile)
    I checked the error on the Angular error website and the explanation is: a particular dependency is failing to be injected. I have tried several times to figure out what the problem might be but each time I fail. I am new to Angular and SP hosted apps. Can you help me out on this please.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s