Pseudo Synchronous Call Queuing in Angular with promises

March 31st, 2015 No comments

One of the things I needed to do was to update a load of data in may page, but rather than fire off loads of ajax update calls and let jquery/the available browser threads just go at it, I wanted to fire off small blocks of calls to update my data and on success continue to the next block and so on until completion.

I could have just done synchronous calls and waited for each one to complete, slowing down the overall process and blocking the UI thread, well that was out, a set size of 1 that blocks the UI.

So I had to discover how to call sets of async calls and wait for them all to complete and then continue with another block,  you cant just do a for loop, it all has to managed with promises.

The key to the processing loop is recursion and not a for loop, because the processing of the outer loop has to be contained within the promise system and loops well you can’t.  Once I’d got that figured out the rest was a case of utilising the $q.all() and the $q.then() functions to manage single and multiple promises.

forget the structure of the code I need to move the manageasync function out of the Service.

So here is the outline of the algorithm its in angular but could easily be adapted to jQuery with say the $q  library.


'use strict';

//In my original which this is hacked to pieces from I was using ngGrid and Bootstrap
var app = angular.module('MyApp', ['ngGrid', 'ui.bootstrap']);

//You will need $q it does the magic
app.controller('MyController', ['$scope', '$MyDataService', '$q', function ($scope, $MyDataService, $q) {

	$scope.status = "";
    $scope.inProcess = true;
    $scope.needProgressBar = true;

    var myData = {
        items: [],
    };

    $scope.myData = myData;

	$scope.initialise = function initialise() {

        $scope.status = "Fetching Data";

        //I havent documented getMasterData but it fills the Items up with stuff
        var promise = $MyDataService.getMasterData($scope);

        promise.then(
    		function (data) {
    			//put status and or progress indicator completion
    			//set the data
    		    $scope.myData.items = data;
    		},
			function () {
				//set status and or progress indicator on fail
				//if this bit fails well nothing else is gonna work
			}
	    );
    }

	//This is called from an event that starts the managed Async Calls.
    $scope.callAsyncCallManagement = function () {

    	//I made these things up but they are possible things you would do
        $scope.status = "Processing";
        $scope.inProcess = true;
        $scope.needProgressBar = true;

        //This is the Start of it all, we have to make a Call into a Recursive function in order to manage
        //blocks of async calls

        //Remember as all of this stuff is async, this function call will not block, it will run thru
        //and you are in the hands of the promise gods.
        //This ensures that the UI thread isnt blocked
        $scope.recurseCall(0);

    };

    $scope.recurseCall = function (i) {

        if (i < $scope.myData.items.length) {

            item = $scope.myData.items[i];

            //Call into the function that calls other async functions, but 5 at a time
            $MyDataService.manageASetOfAsyncCalls($scope, indexOffset)
			.then(
				function () {
					//Here is where you can calculate progress
				    $scope.progressCurr = Math.floor((i / $scope.myData.items.length) * 100);

				    //OK so the manage function completed all its sub calls sucessfully,
				    //We know that it is doing blocks of 5 so we increment the ordinal pointer by 5
				    //and call into this function again so that we can do the next block

				    $scope.recurseCall(i + 5);
				},
				function () {

				    //At this point something broke, so stop dont process any more sub things
				    //maybe clean up the mess which could be tricky perhaps a status on each item is required
	                $scope.inProcess = false;
		            $scope.needProgressBar = false;
		            $scope.itsBorked = true;

				});
        }
        else {
        	//yay we are all done, remember, its ok to do this here, because
        	//only when we hit the end of the processing will this get called because
        	// of the .then()
            $scope.inProcess = false;
            $scope.needProgressBar = false;
        }
    };

    //run Initialise process at beginning
    $scope.initialise();

}]);

app.service('$MyDataService', function ($q, $http) {

	this.getMasterData = function ($scope) {
		//get some data and return it, you can figure this bit out surely

	}

    this.manageASetOfAsyncCalls = function ($scope, indexOffset) {

        var ajaxCalls = [];
        var deferred = $q.defer();

        for (var j = 0; j < 5; j++) {
            //Handle the fact that the list may not be a multiple of 5 :-)
            if (indexOffset + j  < $scope.myData.items.length)
            {
        		//This Executes a block of 5
            	ajaxCalls.push( this.anAsyncCall($scope, indexOffset + j) );
            }
        }

        //I love this, Wait for all of the async calls to complete before telling the parent caller
        //they are done or borked, if even a single sub call fails the block is marked as failed
        $q.all(ajaxCalls)
        	.then(
        	  function () {
        	      deferred.resolve()
        	  },
	          function () {
	          	//depending on the reason you may or may not want to reject here, it depends on the status
	          	//of each processed item, and what you want to do about it. or check the resulting value of the all call
	          	//which holds the array of promises
	              deferred.reject();
	          }
		);

        return deferred.promise;

    }

    this.anAsyncCall = function ($scope, indexValue) {

        var deferred = $q.defer();

        //Some Soap request (change for rest and use $http if you like but change all the relevant bits)
        var soapEnv = "A Soap Request" + indexValue ;

        $.ajax({
            url: "/sites/AFancyURL/_vti_bin/lists.asmx",
            type: "POST",
            async: true,
            dataType: "xml",
            data: soapEnv,
            contentType: "text/xml; charset=\"utf-8\""
        })
	    	.done(function (xmlResponse) {

	    		//Do some processing here, likly update values on the scope and set it as Dirty

	    		$scope.MessWithMyValues[indexValue] = "true";

	    	    $scope.$apply(deferred.resolve());

	    	})
	    	.fail(function () {
	    		//Perhaps a log of fails rather than setting the main one here, as lots of them could fail and
	    		//a better way is probabyl inspect the returned promises from the $q.all call
	    	    $scope.MessWithMyValues[indexValue].status = "Error failed";
	    	    $scope.$apply(deferred.reject());
	    	});

        return deferred.promise;

    };
});

Categories: Uncategorized Tags:

Posting to Pushbullet via ThingSpeak

March 29th, 2015 No comments
ESP8266-01

ESP8266-01

I’ve been messing with the ESP8266 chips, a really cheap Wifi SoC that can be used with Arduino, but also with the right firmware can be used on their own without a master control processor.

There are lots of examples of doing this, I use the nodeMCU firmware, along with ESPlorer to write the code in lua to program the ESP8266.

NOTE:These 8266-01 versions are cheap and everywhere but do yourself a favour and look for an 03 or better the 12, they have more pins available and capable of deepsleep and are just as cheap.

The downside to all this is that the current firmware has no capacity for https connections, and much of the IoT web relies on https, else any crackpot could connect to your service as the API keys are being sent in the clear.  I really hope the nodeMCU people can add the http/sockets/https libs for lua into the firmware cos I failed, no time to ramp up knowledge to that level yet.

thingspeak

ThingSpeak, however lets you collect data using an API key but its done the in the clear, not exactly safe, but for many IoT devices a necessary evil.

And example of a temperature logger is on instructables.

My boy wants a gizmo that will notify him when someone goes into his room.  He has an android phone.  Rather than get my Ximian on and google notification services and all that painful stuff I just need some kind of Android Growl service.  In the past I used NotifyMyAndroid,  but since then I have been using a much better system called

pushbullet

you can send notifications to it without having the pain of writing your own App and notification hooks.  PushBullet is actually so much cooler than just notifications, I use it because it can examine notifications on your phone and relay them to a chrome plugin on the desktop, so I never miss texts and stuff when I cant hear my phone.

So the point of this article is to figure out home to send information to PushBullet via ThingSpeak.  This is really a part two to an article I haven’t written yet, but as the instructable article has already described how to program an 8266 and post to ThingSpeak I will expect you to have done that and we continue from there.

TSChannelsI have created a channel called PushBulletTrigger. It doesn’t really do anything except take a “Field1″ input, which I only ever send a value of “1” to.  Because In my instance I only ever log something to ThingSpeak if the detector sees someone in the room.

Of course on your project you can use any existing channel and check any field for a value in that to trigger a notification.

 

 

 

Next we need to create the ThingSpeak App based on “ThingHTTP”. Click on Apps in the Menu bar and then the thinghttp

 

 

ThingHttp Icon

 

Apps - ThingSpeak

You will need your pushbullet API key here, but the rest I’ve already figured out from the API docs so you can copy pretty much all of this.

Give a good name I chose PushBulletAppSend, and set the URL that you need to connect to in our case

https://api.pushbullet.com/v2/pushes

Set the Method to POST

Set the Content Type to “application/json”

The HTTP version is 1.1

The host is api.pushbullet.com

We need to add a header name = Authorisation

and the value for that is

“Bearer [PushBulletAPI]”

No quotations and the space is important.

Lastly the Body

{“type”: “note”, “title”: “IOT Message”, “body”: “Detector Triggered”}

You can set the title value and body value to what you want.  I haven’t figured out how to replace the values so if you want different messages and title then create different ones per message type.

So now we can log into ThingSpeak and we have something that can call into PushBullet which will send a notification to an Android OR iPhone.

Lastly we need to tie these things together and we do this also inside ThingSpeak.

react

To do this click on the Apps menu option at the top

and then on the React Icon

 

 

Apps - ThingSpeak (1)Name the React, ReactToPushBulletTrigger

We are testing a string so the Condition Type is String

We are testing on every Insertion, other options are on timers

Our condition is Channel “PushBulletTrigger” this is the Channel that logs data to thingspeak.

Then we test Field1 = 1

if this is true we call into the ThingHttp by setting the Action to ThingHttp and selecting our React “PushBulletAppSend”

Save it.

That’s it.

Now whenever your IoT device sends a piece of data into ThingSpeak, ThingSpeak will automatically relay a post to PushBullet and in turn notify your devices.

So thing to remember here is that ThingSpeak API allows for a data post every 15 seconds,  so if you’re dumb enough to post every 15 seconds and have tied a react event to it, you could very well swamp your mobile with notifications and drive yourself insane.

In my version of the 8266 code, I have utilised the tmr events to only notify a maximum of once every 30 minutes, if the rooms been empty for 10 then it will notify immediately.  But as I haven’t built the circuit board properly I’m waiting before I do a post on this, as I have to yet source 3.3v regulators, figure out how to boot the device with an active low state on the GPIO pins without sending the 8266 into firmware mode.  It may be I have to wait till I get my 8266-12’s  instead of the 01’s I’ve been using.

 

Categories: Uncategorized Tags:

SharePoint 2013 App – AngularJs, BootStrap and ngGrid example

October 7th, 2014 Comments off

I’ve created a SP2013 App including all the code and published it on Codeplex so anyone can take a look and steal the code get ideas.

It has an example of two data fetch techniques (see my earlier blog post for the third external data using the proxy)

using $http to get REST data or $.ajax to get legacy WebService Data.  With the appropriate angular code to handle either, no antipatterns here.

Perhaps I should have used SPServices instead of $.ajax it would have made it easier and I would not have blatantly nicked Marcs getZrows function (thanks Marc ;-)  )

https://spappnggrid.codeplex.com/

Capture

Last Update:14/10/2014
Organised into folders and separate files for controllers and services. Changed Service to Factory.
(Service code left in project for example purposes)

 

 

Categories: Uncategorized Tags:

Detecting the Language in a DVWP

September 17th, 2014 Comments off

I have been hunting all morning on how to do this.  I can do it in a CQWP with parameterbinding set correctly but no-one, no-one has put any example code on how to do this in a DVWP.  I’ve read many an article talking about ddwrt functions lists of variables which are not accessible even if you include the xslt namespace.

Eventually I figured something out.  It’s not pretty but then again, what SharePoint is doing isn’t exactly too far removed.

In order for SharePoint to know what browser is being used and what language the browser has to send this information via http headers so we have all the info we need.

 

In you DVWP start by adding a Parameter (I called mine Param1)

image

Set it to a Server Variable

Type in HTTP_ACCEPT_LANGUAGE

and give a a default value, mine says none, but set it to your default language e.g. “en-GB”

That will get you the language codes you need into your XSLT.

 

However those strings can be quite large, e.g.

    en-GB,de-DE, {lots of other stuff}

The bit your interested in, well that’s up to you in my case I care about language, and being British there are no such things as regions just ours and everyone else that’s doing it wrong.

So I have in my XSLT

<xsl:variable name=”lang” select=”substring($Param1, 0, 3)” />

This grabs the “en” from the string.  If I switch to German then I get “de”

So now I have my language code I can do conditional XSLT based on a language code.

 

<div class=”ms-WPBody” width=”100%” >
<xsl:choose>
    <xsl:when test=”$lang = ‘de'” >
        <xsl:value-of select=”@German” disable-output-escaping=”yes”/>
    </xsl:when>
    <xsl:otherwise  >
        <xsl:value-of select=”@English” disable-output-escaping=”yes”/>
    </xsl:otherwise >
</xsl:choose>
</div>

This has allowed me to create multi lingual Content web parts in WSS 3. Which is handy.

It’s simple, if someone can point me at a better way please do, I haven’t tested this thoroughly with lots of different browsers and languages so I may need to get a bit cleverer that this simple example.  But for the many of you searching for how to do it, this might be an option for you.

 

Technorati Tags:
Categories: Uncategorized Tags:

List of JS Libraries I’m using / looking into

July 3rd, 2014 Comments off

This is more of a note to myself so that I can remember the JavaScript libraries I’m interested in and what they do and maybe a related article.  These are mainly in dealing with SharePoint but could be anything really.  Not everything is compatible with everything else remember :-)

 

KnockoutJs – Data binding library, works with jQuery, great for a true single page app, IE no routing just data binding.  Yes I know SPA’s pages are called views.

AngularJS – Proper SPA framework, there’s Durandal but I’?m not going there even though I love knockout.

BreezeJs

    - Linq Like queries, does need server side integration, not for local objects

Bootstrap – For that nice CSS layout grids and things.

Bootstrap UI – Directives for Angular using bootstrap

Angular Maps – Google maps directives for angular, something I’ve used already.

 

LinqJs – Linq for JavaScript, will work on JS objects, doesn’t do fetch for you

Js Loaders – http://requirejs.org/  and  http://labjs.com/

jQuery – Well how could I not mention it.

jQuery form validators

Sliders – http://slippry.com/ , http://bxslider.com/, http://workshop.rs/projects/coin-slider/

fontawesome – No idea, well something to do with a shed load of CSS icons, vectors (no IE7)

Charts – jqPlot, http://www.flotcharts.org/, hicharts

Spinner - svg/vml Spinner, easier than a gif ?

 

Stuff to investigate

  • d3js – Don’t know why or what this gives me over anything else yet.
  • threejs – 3d library, gotta have some fun sometime.
  • Mobile Angular frameworks
    • http://mobileangularui.com/
    • http://showcase.ionicframework.com/

 

Not a library but a list of handy SP2013 dev tools

Zimmergrens SP2013 Tools Page

SharePoint Designer 2013 – Working round some problems

June 19th, 2014 Comments off

I’ve been trying to do some customizations I did easily 2007 and 2010 but in 2013, it’s been a bloody nightmare.

I can hear you saying “Ahh missing designer mode”, well no not really I like xslt so the designer mode I can live without, although there are some things that it did make so much easier, like setting a form field to a textbox via a menu, all done in code now, no menu available like in this old article https://t.co/bpbAimITHe, which is an antiquated method anyway and best to do it in javascript.

So here are some of the problems.

First DVWP.  Nightmare.  Trying to customize this, I haven’t the time to figure out all that’s wrong with this one.  Just simple things, like inserting it, convert to xslt (all of it) then ripping the guts out of it to just be what I wanted, and then having it ignore all subsequent xchanges after the initial save.  Something very strange going on so I bailed on this, to revist later.

So I used the Item Forms instead, as they are built for form entry but also lists and paging, much like I expect the dvwp to work, only they work better-ish.

So the problems.

1. Where’s my button.

Once in the code you place the cursor in the white area (not yellow) where you want your webpart and nothings working, no buttons.  This is probably well known by now but do this.

In the Code View Tools menu click the Parse HTML button.

p1

This usually sorts out the ribbon bar giving you what you need.  If it doesn’t, your cursor is in the wrong place.  Click round and about, when I wanted to add a CEWP to a page I had to select in the text of the end of a previous part, not in the white space after it for the add web part button to be active

2. Make it Easy in the XSLT

Insert your Display Form Part, then change the design to simple list mode.  It will change the xslt to something usable that you can extend without all the voodoo.

p2   p2a

 

3. Filtering

When you want to edit the query you are probably used to clicking filter and setting the menu items.  In this version I have had numerous problems when those filters are id’s or lookup fields.

I wanted to filter by a parent Id on a lookup field on my list.  So I added the display part and set the xslt to easy.  Next I know the Id of the parent is on the QueryString as pid param.

So I added the Parameter by clicking the Parameters ribbon button and setting the various values the name and the type(QS) and its name.

p3

p3a

Great it inserted it into the ParamBindings and Params of the XSLT.  Now when I click the filter button it appears in the drop-down for selection.  If you save the page and run this it will not work. it will return everything or it might return one row, that’s another problem though, but its not filtering.

p3b

This is because the SPDataSource has had its query modified but it has not inserted the SelectParameter relating to the param you are querying against.  I had to manually enter it.

<SelectParameters>

<asp:QueryStringParameter QueryStringField=”ID” DefaultValue=”1″ Name=”ParentIdeaId”></asp:QueryStringParameter>

</SelectParameters>

Major bug there MS.

So I saved that and run the page, did it filter.  “Did it eck as like“.  Because the Query that this set up was querying for text not ID, the filter box doesn’t ask or figure out what you are trying to filter on so you have to edit the query as well to get the types right, in this case for a lookup field.  This is what I set mine to.

This is unencoded here, the string is encoded but that would be a pig to read. Changed elements are in bold.

<FieldRef Name=”ParentSubmission”  LookupId=”TRUE”/><Value Type=”Lookup“>{ParentItemId}</Value>

 

4. Returning one item

You have inserted your display form item, ran it and it shows 1 result. You look at the paging option in the menu and it says all items.  It’s lying.  Select this option and watch the xslt change.  you might find it shows all the options then.

p4

 

Technorati Tags:
Categories: Uncategorized Tags:

How to get the SiteCollection Url in XSLT

June 17th, 2014 Comments off

I found this way to get the root site collection url into a DVWP

There are Global parameters available but you have to mess about with the bindings to use them.

 

So for this one you need

<ParameterBinding Name=”RootSiteUrl ” Location=”WPProperty[RootSiteUrl]”/>

 

Then at the top of the XSL (make it global to all templates)

<xsl:param name=”RootSiteUrl”></xsl:param>

 

Then in your XSL you can use it

<xsl:value-of select=”$RootSiteUrl” />

 

That’s it. Well it worked for me, but SP2013 and designers is so damn flakey it might not work for you, this was done on a WPPage not a wiki page that causes all manner of hell using webpatrs in those things.

Categories: Uncategorized Tags:

Set Fiddlers Icon based on Connected or Not

June 5th, 2014 Comments off

Thanks to @ericlaw for the info on this, since version 2.4.7.2 of fiddler you can set the Tray Icon to whatever you want.

I like to know at a glance whether fiddler is connected or not, I leave mine running al the time, I have various issues with proxy servers and virtuals, sometimes it’s just easier to manage the hosts file in fiddler perhaps cos I?’m too lazy to go into admin mode and edit the real one.

fiddlerOff fiddlerOn

I used a simple red / green system you can guess which is which :-)

To do this copy the images to your system somewhere.

Then in Fiddler open the Rules Editor (CTRL-R) and make these changes, just point at your path not mine.

static function OnAttach() {
    // MessageBox.Show(“Fiddler is now the system proxy”);
    FiddlerApplication.UI.SetTrayIcon(“D:\\dev\\fiddlerOn.png”)
}

static function OnDetach() {
    // MessageBox.Show(“Fiddler is no longer the system proxy”);
    FiddlerApplication.UI.SetTrayIcon(“D:\\dev\\fiddlerOff.png”)
}

Save it and that’s it.

If they would just give me the option now so a left click on the tray toggles between connected or not and a dbl click open fiddler. Because for me its more than just a debugger now.

Whilst I’m talking changes some more tray options (right mouse menu) that I would love, hope your listening :-)

  •     Toggle Https decryption
  • Select Gateway option (something I have to do all the time, stupid pac file wont work in fiddler)
  • Autoresponder toggle
Categories: Uncategorized Tags:

God Ted, I love fiddler – or using fiddler to address individual servers on a HNSC)

April 25th, 2014 Comments off

This tool just gets better for me with every little bit more I learn about it. I use it as a proxy server for my VM’s to keep them of the main network, but have internet access. I use it for speed tests, I use it for dynamic debugging of javascript of sites I have no access too.  It’s wonders do not cease.

Yesterday I had an problem with a Host Named Site Collection, or rather it appears that one of the servers appeared to be slow.  But being a HNSC no AAM’s were created for it and with four front ends to test how the hell do you hit each one sseperately.

Chap running the servers was kind enough to give me the IP addresses and of course I can then edit the hosts file and test each one from the main address.

Well that sucks.

Enter Fiddler, did I say how much I love fiddler ?

In fiddler is a rules file,   CTRL-R will get you there. In that we can do marvelous things, in this case I wanted to handle my HNSC problem by having 4 separate urls I could hit and test each server in a load test script.

My host was something like

http://hnsc.binaryjam.com

and I wanted to hit

http://hnsc1.binaryjam.com

http://hnsc2.binaryjam.com

http://hnsc3.binaryjam.com

http://hnsc4.binaryjam.com

 

Here’s how to do it in fiddler.  Edit the rules file and find the function OnBeforeRequest, its all javascript dontcha know.

Add this bit of code at the bottom of the function

<pre>         if (oSession.hostname == "hnsc1.binaryjam.com"){
             oSession.hostname = "hnsc.binaryjam.com";
             oSession["x-overrideHost"] = "192.168.1.10"; // <-- Server IP here!
         }
         if (oSession.hostname == "hnsc2.binaryjam.com"){
             oSession.hostname = "hnsc.binaryjam.com";
             oSession["x-overrideHost"] = "192.168.1.11"; // <-- Server IP here!
         }         if (oSession.hostname == "hnsc3.binaryjam.com"){
             oSession.hostname = "hnsc.binaryjam.com";
             oSession["x-overrideHost"] = "192.168.1.12"; // <-- Server IP here!
         }         if (oSession.hostname == "hnsc4.binaryjam.com"){
             oSession.hostname = "hnsc.binaryjam.com";
             oSession["x-overrideHost"] = "192.168.1.13"; // <-- Server IP here!
         }

Save it and do a test.

I even ran WireShark to see what was actually happening on the network stack, and it works, it was routing requests for specific servers to each individual server, and because we set the hostname to what it should be any code JS or server using the server name works fine too.

Makes me really want to crack open the fiddler manual and find out what other voodoo I can do.

Categories: development, Internet, Sharepoint Tags:

SharePoint Apps and AngularJS – SP.WebProxy Invocation using promises

April 17th, 2014 1 comment

Recently I’ve been trying to create a SharePoint 2013 app using AngularJS and angularui-bootstrap.

It’s been going well thanks to Jeremy Thake’s (@jthake) original demo.

But I wanted more! More than just access to the local site, I wanted access to the web, but I wanted to do it in the right AngularJs way, using the service so later when I figure out how to do DI testing I can, using promises too, which of course SharePoint doesn’t return in a way we can use in AngularJS.

So this code snippet (not complete app) will give you a clue on a way of doing it, there may be a better way to do this in AngularJS Im no expert, I see references to moduile.Factory calls in other examples, but I’m sticking to Jeremy’s starter code for now.

This example uses UK police data, details of the license are here http://www.nationalarchives.gov.uk/doc/open-government-licence/version/2

'use strict';
var myApp = angular.module('myApp', []);

function myAppCtrl($scope, $myDataService) {
    $scope.getPoliceClick = function () {

        //Whoops, edited this line as it was wrong from original post
        var promise = $myDataService.getPolice($scope);
        promise.then(
            //Success callback, check the status = 200 and go mad
            function (response) {....
            },
            //Epic Fail callback
            function (response) {....
            }
        );
    }
}

//this method signature is same as Jeremy's thats why the $http is there, I use it for lists as well
//just not in this code sample
myApp.service('$myDataService) ', function ($q, $http) {
    this.getPolice = function ($scope) {
        var deferred = $q.defer();
        var context = SP.ClientContext.get_current();
        var request = new SP.WebRequestInfo();
        request.set_url("http://data.police.uk/api/bedfordshire/57");
        request.set_method("GET");
        // We need the response formatted as JSON.
        request.set_headers({
            "Accept": "application/json;odata=verbose"
        });
        var response = SP.WebProxy.invoke(context, request);
        context.executeQueryAsync(
            function () {
                //Notice the scope apply, because the async call is made from outside of angular
                //just resolving will not work
                $scope.$apply(deferred.resolve(response));
            },
            function () {
                $scope.$apply(deferred.reject(response));
            }
        );
        return deferred.promise;
    }
});
Categories: development, Sharepoint Tags: