Tuesday, 21 April 2015

Using the Microsoft Client-side People Picker as an AngularJS Directive

Last year I wrote about using the Microsoft Client Side People Picker with an AngularJS app (here).

In this post, I'm building on that idea, and I'm going to demonstrate how to use the People Picker within an AngularJS Directive. Using a directive simplifies the code and makes it much more re-usable!

I'm going to break the post up into parts. This part will outline how to use the directive. The next post will cover a bit more about how the directive was created and how it works.

The code example is up on Gitbit, here: AngularJS-Directive-for-SharePoint-People-Picker

Let's get into it!

1. Add the script references to your apps html page.

You need to add a reference to the JS file the directive is declared in, as well as all of the Microsoft Scripts that the SharePoint People Picker requires

<!-- Load the constant variables -->
<script type="text/ecmascript" src="../angularjs-peoplepicker/config/config.constants.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/models/models.js"></script>
<!-- Load third party scripts required by the people picker -->
<script type="text/ecmascript" src="/_layouts/15/SP.UI.Controls.js"></script>
<script type="text/ecmascript" src="/_layouts/15/clienttemplates.js"></script>
<script type="text/ecmascript" src="/_layouts/15/clientforms.js"></script>
<script type="text/ecmascript" src="/_layouts/15/clientpeoplepicker.js"></script>
<script type="text/ecmascript" src="/_layouts/15/autofill.js"></script>
<script type="text/ecmascript" src="/_layouts/15/sp.RequestExecutor.js"></script>
<!-- AngularJS, Sanitize, resource -->
<script type="text/ecmascript" src="../angularjs-peoplepicker/scripts/angular.min.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/scripts/angular-sanitize.min.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/scripts/angular-resource.min.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/scripts/angular-route.min.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/scripts/ui-bootstrap.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/scripts/xml2json.min.js"></script>
<!-- All of this scripts are used to create the app. -->
<script type="text/ecmascript" src="../angularjs-peoplepicker/app.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/config/config.js"></script>
<!-- ****** The following script contains the people picker directive. ****** -->
<script type="text/ecmascript" src="../angularjs-peoplepicker/config/config.peoplepicker.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/common/common.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/common/logging.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/services/dataservices.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/controllers/controllers.js"></script>

2. Add the directive as a dependency to your app.

(function () {
    'use strict';        
    var app = angular.module('app', [
    //inject other Angular Modules
    'ngSanitize',
    'ngResource', 
    'ui.bootstrap',
    //inject the People Picker directive
    'ui.People',
    //inject App modules
    'common'
    ]);
})();

3. Add the directive to the page (you can add multiple instances of it).

When add the directive, it has several attributes that can be set to control the behaviour of the people picker.

Most are mandatory, but you must add the data model (via ng-Model), and you must add the pp-ready-to-load attribute (which tells the directive the data model has updated, and is ready to be used.

Possible attributes and they're values
AttributePossible valuesDefault valueRequired?
data-ng-modelAn array, containing a list of users, in the following format:

var userArray = [{ 'Name': object.Name, 'Title': object.Title, 'Id': object.Id }]

Where, name is a claims based user principal, and Title is the display name of the prinicpal
nullYes (you must supply a model to the data-n-model attribute, but it contain a null value)
data-pp-ready-to-loadtrue | falsefalseYes (the people picker will not render until this value is set to true)
data-pp-is-multiusertrue | falsefalseNo
data-pp-widthAny numerical value, followed by 'px'220pxNo
data-pp-account-typeAny compination of the following values, seperated by a comma;
User,DL,SecGroup,SPGroup
User,DL,SecGroup,SPGroupNo

An example of adding a single user picker;

<div ui-People ng-model="vm.data.su" pp-ready-to-load="{{vm.loadPeoplePickers}}" pp-is-multiuser="{{false}}" pp-width="220px" pp-account-type="User" id="peoplePickerDivAT"></div>

An example of adding a multiple user picker:

<div ui-People ng-model="vm.data.mu" pp-ready-to-load="{{vm.loadPeoplePickers}}" pp-is-multiuser="{{true}}" id="peoplePickerDivAP"></div>

4. Load the data for the model.

Before initialising the directive (via changing the value of the property used in the data-pp-ready-to-load attribute to true), ensure you up the data model used for the people picker with the current data (if any).

For example, you might perform a REST call to get the current values in a list item. You would then create the model for the user picker when data from the REST call is returned.

After updating the data in the people pickers model, you would then change the value of the property used in the data-pp-ready-to-load attribute to true.

The code below shows an example of doing this.

function init() {   
   //Pre-populate the single user field
   //Normally you would get this information from a REST call to Office 365 / SharePoint
   vm.data.su = populatePickerModel({
      Name:'i:0#.f|membership|someone@sometenant.onmicrosoft.com',
      Id:'19', 
      Title:'Matthew Yarlett'});
   vm.loadPeoplePickers = true;
   if (!$scope.$root.$$phase) {
      $scope.$apply();
   }
};

That's it - as far as initialising and using the control goes.

To use the value(s) of a the people picker directive while the page is open, simple reference the people picker model.

E.g.

<p>
    <span data-ng-repeat="r in vm.data.su track by $index">
        <span data-ng-bind-html="vm.getPresence(r.Name, r.Title)"></span>&nbsp;
    </span>
</p>

The example above uses a function on the controller to return the users name including the presence icon, It's just some standard html that I've exported (and slightly tweaked) from a SharePoint user field. Note that it uses a "constweb" variable to get the weburl - you would need to set this!

function getPresence(userId, userTitle) {
   if (userId && userTitle) {
      return '<span class="ms-noWrap"><span class="ms-spimn-presenceLink"><span class="ms-spimn-presenceWrapper ms-imnImg ms-spimn-imgSize-10x10"><img class="ms-spimn-img ms-spimn-presence-disconnected-10x10x32" src="'+constWeb+'/_layouts/15/images/spimn.png?rev=23"  alt="" /></span></span><span class="ms-noWrap ms-imnSpan"><span class="ms-spimn-presenceLink"><img class="ms-hide" src="'+constWeb+'/_layouts/15/images/blank.gif?rev=23"  alt="" /></span><a class="ms-subtleLink" onclick="GoToLinkOrDialogNewWindow(this);return false;" href="'+constWeb+'/_layouts/15/userdisp.aspx?ID=' + userId + '">' + userTitle + '</a></span></span>';
   }
      return '<span></span>';
}

Persisting the value(s) of the a people picker control is just as easy. You use the data in the people pickers model to write back to the server.

In the example below, I have a model used in a REST call for updating a list item that has a people picker field. Data from the people picker directive's model is formatted for the REST call. The ID (SPUser.Id) of each resolved principal (user or group) is added to an array of ID's, which is passed to the server via a REST call.

var sr = sr || {};
sr.models = sr.models || {};

sr.models.listItemModel = function (id) {
   this.Id = id ? id : -1; 
   this.srSingleUserField = null;
   this.srMultipleUserField = {
      results: []
   };   
   this.__metadata = {
      //The type is unique to your list. You can find out the type byte
      //looking at the list using a REST via the browser
      //E.g. http://my.site.com/_api/web/lists/getlistbytitle("YourListName")
      type: 'SP.Data.SrListItem'
   };
}

function populatelistItemModel(srcModel, tagsTerms) {
   var dstModel = new sr.models.listItemModel(srcModel.Id);            
 //Single user field
 if(srcModel.su){
  if(srcModel.su.length == 0){
   dstModel.srSingleUserField = null;
  }
  else{
   var user = srcModel.su[0];
   dstModel.srSingleUserField = user.Id
  }
 } 
   //Mutli user field
   if (srcModel.mu) {
      if(srcModel.mu.length == 0){
         dstModel.srMultipleUserField.results = [];
      }
      else{
         for(var i = 0; i < srcModel.mu.length; i++){
            var user = srcModel.mu[i];
            dstModel.srMultipleUserField.results.push(user.Id);
         }
      }
   }
   dstModel.__metadata.etag = srcModel.__metadata.etag;
   dstModel.__metadata.id = srcModel.__metadata.id;
   dstModel.__metadata.uri = srcModel.__metadata.uri;
   return dstModel;
}

I also added some CSS to override a few of the People Pickers classes, that fixes a few style issues. It's optional, but I've added it below for completeness.

The CSS classes are prepended with div#strk (the parent DIV of my sample), to ensure these classes only affect the app.


/* People Picker Modifications */
div#strk .sp-peoplepicker-autoFillContainer{
 z-index: 20;
 background-color:#fff;
}
div#strk .sp-peoplepicker-topLevel{
 background-color:#fff;
}
div#strk .sp-peoplepicker-topLevel{
 min-height:34px;
}

When it's all said and done, it looks like this;



Monday, 30 March 2015

Saving a Managed Metadata (Taxonomy) Field Value to Office 365 using REST

In this post I want to demonstrate how to save a Managed Metadata (Taxonomy) field value (single or multiple terms) to a Managed Metadata (Taxonomy) Field in Office 365 via the REST API.

The Hidden Managed Metadata Field

The trick to being able to save a taxonomy field value via REST, is knowing the name of the hidden text field that gets created when you create a Managed Metadata field.

When you make a REST call to update the Managed Metadata field, you need to pass a string representation of the managed metadata terms that you're saving into this hidden field.

The string representation of the terms is in the format of "termName|termId;".

In the screen shot below, you can see the field value of a hidden Managed Metadata field (the hidden field name is pa28754972f84ff7a6bf3e4762f23e23).


Finding the Name of the Hidden Field

Finding the name of the hidden text field of a Managed Metadata field is easy. The fastest way to do it, is by using the REST API via a browser.

1. Open Internet Explorer
2. In the address bar, enter the URL for your Office 365, followed by the REST API endpoint for to list the fields in your list.

https://matthewyarlett.sharepoint.com/sites/appcenter/_api/web/lists/getbytitle('srShipRegister')/fields

3. Use the browsers find function (Ctrl+F) to locate your field name in the xml


4. Look at the TextField property, and copy the copy
5. Use the browsers find function (Ctrl+F) to locate your field name in the xml, by searching for the guid
6. Make note of the hidden fields static name


Formatting a taxonomy field value

Now that you know the name of the hdden Taxonomy text field, you need to format the term values you want to save.

The terms should be formatted in the format of "termName|termId;".

An example model for updating the list in the above screens could look like this:
Loading ....
... and this is what the JSON looks like when you post it back to Office 365



Thursday, 5 March 2015

Drag'n'drop Error within a DocumentSet

I came across a strange bug yesterday. I couldn't drag files into the Documents webpart within a DocumentSet.

When I tried dragging documents onto the page, the normal "Drop here..." dialog wasn't shown, and dropping the document wouldn't upload the file to SharePoint.

After a little experimenting (on both Office 365 and SharePoint 2013 on prem) it seems to be a bug in the SharePoint CSS.

The bug is manifested only when you change the webpart chrome from Default to Title Only or Title and Border for the documents webpart.

(Why would you do that anyway? I had a requirement to display the documents webpart title, showing the label "Supporting Documents".)

To workaround this bug, add some CSS to a Content Editor webpart, that re-positions the "Drop here..." div. If you only want that CSS, skip to bottom.

So here is how you work around it... mostly in pictures!

1. Drag'n'drop in a normal Document Set - works fine



2. Change the Webpart chrome to display the Webpart Title





3. Try the drag'n'drop again... Ahhgg! It doesn't work anymore!



4. If you zoom the page out, you'll notice that the Drag'n'drop box is further down the page and off to the right... what the?



5. With a little element inspection, we can see that Drag'n'drop div has some values set for the Top and Left properties.



These values are causing the div the be misplaced when the parent div (for the webpart) contains another child div, which appears in the HTML layout flow before the Webparts contents div (where the Drag'n'drop div lives).

In this case, that child div is the Webpart title container.




6. To fix this issue, all we need to do is:

a). Change the position CSS property for the Webparts "contents" div to Relative. This is requred because the Drag'n'drop div has a position value of absolute - an absolute position element is positioned relative to the first parent element that has a position other than static. See http://www.w3schools.com/css/css_positioning.asp for more info.

b). Override the Left and Top CSS properties on the Drag'n'drop div, with new values.

We can achieve this through a couple of CSS selectors that target these two div's. All we need to do is add the CSS to the page.

7. Add the CSS the page by adding a new Content Editor webpart, editing the source, and adding the new CSS styles.



Don't forget to hide the webparts title, by changing the Chrome property to None



8. Save the changes

9. Presto... problem fixed!



The CSS:

Tuesday, 23 December 2014

Using SharePoints SpellCheck Webservice with TinyMCE and AngularJS

In this post I want to demonstrate how to use SharePoint's SpellCheck webservice with the TinyMCE Richtext editor.

There are many reasons why you might choose to use the TinyMCE richtext editor over the Office365/SharePoint richtext editor. One of those reasons is if you're building a clientside app that needs a richtext editor, and you want the richtext controls to be inside the app, and not on the ribbon.

TinyMCE is a great richtext editor with good cross browser support. Among the functionality it has, it provides a way to integrate with a spell checking service.

In the example below I'm going to demonstrate how to integrate the SharePoint spellcheck.asmx webservice with the a TinyMCE richtext editor, in an AngularJS app, hosted in an Office365 or SharePoint site.

The files for this example can be downloaded from the MSDN TechNet Gallery, here: Technet Gallery. I suggest you download the files and look through the code to fully understand the example. To use the example, follow the instructions in the readme.txt file included in the zip file.

What I want to focus on here is how to call the webservice, and then how to interpret the results.

First, I'll start with a screenshot of what this looks like when it's running (the screen shots are from SharePoint 2013, but this works exactly the same with Office365):




The code to get this running looks like this  (remember, the full source can be downloaded here: TechNet Gallery):

First, the app's html file. It's pretty simple, containing a few script references and a textarea.

<!-- There's not much to the HTML file. A div that references my AngularJS controller, and then a textarea with the data-ui-tinymce attribute. -->
<div id="ng-app" data-ng-app="app" data-ng-cloak >
    <div data-ng-controller="spellCheckExampleCtrl as vm" data-ng-cloak>
        <textarea data-ui-tinymce id="eBriefTiming" data-ng-model="vm.richTextContent"></textarea>
    </div>
</div>
<!-- Load the scripts -->
<!-- XML2JSON is used to transfor the XML based response from the Spellcheck webservice to JSON -->
<script type="text/ecmascript" src="../tinymce/xml2json.js"></script>
<!-- AngularJS, Sanitize, resource and tinymce -->
<script type="text/ecmascript" src="../tinymce/angular.js"></script>
<script type="text/ecmascript" src="../tinymce/angular-sanitize.js"></script>
<script type="text/ecmascript" src="../tinymce/angular-resource.js"></script>
<script type="text/ecmascript" src="../tinymce/tinymce/tinymce.min.js"></script>
<!-- My scripts. All of this scripts are used to create the app. -->
<script type="text/ecmascript" src="../tinymce/config.tinymce.js"></script>
<script type="text/ecmascript" src="../tinymce/app.js"></script>
<script type="text/ecmascript" src="../tinymce/controllers.js"></script>
<script type="text/ecmascript" src="../tinymce/services.js"></script>

The app and controller code. Again, this is pretty simple; it's not really doing much in this simple app.

(function () {
    'use strict';
    var app = angular.module('app', [
    'ngSanitize',
    'ngResource',
    'ui.tinymce'
    ]);
    app.run();
})();

//App Controller
(function () {
    'use strict';
    //define the controller
    var controllerId = 'spellCheckExampleCtrl';
    angular.module('app').controller(controllerId, ['$scope', '$q', spellCheckExampleCtrl]);
    function spellCheckExampleCtrl($scope, $q) {
        var vm = this;
        vm.richTextContent = null;     
        init();
        function init() {           
        };
    }
})();

This next bit of code initialises the TinyMCE directive with AngularJS. The main point of interest in this code snippet is the spellchecker_callback function. Example the function, specifically how the results from the spelling webservice are interpreted.

(function () {
//pass in the remoteServices factory. This factory contains the method for querying the SharePoint Spellcheck webservice
angular.module('ui.tinymce', [])
.value('uiTinymceConfig', {})
.directive('uiTinymce', ['uiTinymceConfig', 'remoteServices', function (uiTinymceConfig, remoteServices) {
uiTinymceConfig = uiTinymceConfig || {};
var generatedIds = 0;
return {
require: 'ngModel',
priority: 10,
link: function (scope, elm, attrs, ngModel) {
var expression, options, tinyInstance;
var updateView = function () {
ngModel.$setViewValue(elm.val());
if (!scope.$root.$$phase) {
scope.$apply();
}
};
if (attrs.uiTinymce) {
expression = scope.$eval(attrs.uiTinymce);
} else {
expression = {};
}
if (expression.setup) {
var configSetup = expression.setup;
delete expression.setup;
}
// generate an ID if not present
if (!attrs.id) {
attrs.$set('id', 'uiTinymce' + generatedIds++);
}
options = {
// Update model when calling setContent (such as from the source editor popup)
setup: function (ed) {
ed.on('init', function (args) {
ngModel.$render();
ngModel.$setPristine();
});
// Update model on button click
ed.on('ExecCommand', function (e) {
ed.save();
updateView();
});
// Update model on keypress
ed.on('KeyUp', function (e) {
ed.save();
updateView();
});
// Update model on change, i.e. copy/pasted text, plugins altering content
ed.on('SetContent', function (e) {
if (!e.initial && ngModel.$viewValue !== e.content) {
ed.save();
updateView();
}
});
// Update model when an object has been resized (table, image)
ed.on('ObjectResized', function (e) {
ed.save();
updateView();
});
if (configSetup) {
configSetup(ed);
}
},
mode: 'exact',
elements: attrs.id,
inline_styles: true,
plugins: [
"advlist autolink lists link charmap hr pagebreak",
"searchreplace wordcount visualblocks visualchars code fullscreen",
"insertdatetime nonbreaking table contextmenu",
"paste textcolor spellchecker"
],
//override the default spellchecker (which, OOTB, doesn't work with SharePoint). Instead we'll use SharePoints spellcheck webservice.
//This method is documented here: http://www.tinymce.com/wiki.php/Configuration:spellchecker_callback
spellchecker_callback: function (method, text, success, failure) {
if (method == "spellcheck") {
//Call the checkSpelling method (this is a method we've defined in another file, documented in the next script block).
//This method will query the SharePoint spellcheck webservice. The query contains the full text from the
//TinyMCE richtext editor.
//When the response comes back, we need to create an array of spelling errors and suggestions
remoteServices.checkSpelling(text).then(function (data) {
var wordCollection = data;
var suggestions = [];
//***
//Check for spelling errors
//***
//Get the array of flagged words that are errors
var spellingErrors = null;
if (data.spellingErrors.SpellingErrors && data.spellingErrors.SpellingErrors.flaggedWords !== 'undefined') {
    spellingErrors = data.spellingErrors.SpellingErrors.flaggedWords.FlaggedWord;
}
//Check if an array of items was returned
if (spellingErrors instanceof Array == true) {
    for (var wi = 0; wi < spellingErrors.length; wi++) {
        var w = spellingErrors[wi];
        suggestions[w.word] = [];
    }
}
//Check if a single item was returned
else if (spellingErrors && spellingErrors.word) {
    suggestions[spellingErrors.word] = [];
}
//***
//Check for spelling suggestions
//***
//Get the array of flagged words with suggestions
var spellingSuggestions = null;
if (data.spellingSuggestions.SpellingSuggestions) {
    spellingSuggestions = data.spellingSuggestions.SpellingSuggestions;
};
//Check if an array of items was returned
if (spellingSuggestions instanceof Array == true) {
    for (var wi = 0; wi < spellingSuggestions.length; wi++) {
        var w = spellingSuggestions[wi];
        //Check if there is a single spelling suggestion, or an array of suggestions.
        //Then add it to suggestions array for the current word
        suggestions[w.word] = (w.sug.string instanceof Array == true) ? w.sug.string : [w.sug.string];
    }
}
//Check if a single item was returned
else if (spellingSuggestions && spellingSuggestions.word) {
    //Check if there is a single spelling suggestion, or an array of suggestions.
    //Then add it to suggestions array for the current word
    suggestions[spellingSuggestions.word] = (spellingSuggestions.sug.string instanceof Array == true) ? spellingSuggestions.sug.string : [spellingSuggestions.sug.string];
}
//Return the list of suggestions to the success handler.
success(suggestions);
})["catch"](function (error) {
//in my testing, failure doesn't seem to work. So I'm sending back Success with a null value.
success(null);
});
}
},
toolbar: "styleselect | bold italic | bullist numlist outdent indent | link | spellchecker",
fontsize_formats: "9pt 10pt 11pt 12pt 14pt 16pt 18pt 20pt 22pt 24pt",
menubar: true,
statusbar: false,
height: 300,
width: 620
};
angular.extend(options, uiTinymceConfig, expression);
setTimeout(function () {
tinymce.init(options);
});
ngModel.$render = function () {
if (!tinyInstance) {
tinyInstance = tinymce.get(attrs.id);
}
if (tinyInstance) {
tinyInstance.setContent(ngModel.$viewValue || '');
ngModel.$setPristine();
}
};
scope.$on('$destroy', function () {
if (!tinyInstance) { tinyInstance = tinymce.get(attrs.id); }
if (tinyInstance) {
tinyInstance.remove();
tinyInstance = null;
}
});
}
};
}]);
})();

Finally, the code for the remoteServices factory. This code is responsible for making the call to SharePoint's Spellchecker webservice.

(function () {
'use strict';
var serviceId = 'remoteServices';
angular.module('app').factory(serviceId, ['$resource','$q', remoteServices]);
function remoteServices($resource, $q) {
    var service = this;       
    init();
    //service signature
    return {           
        checkSpelling: checkSpelling
    };
    function init() {           
    }
    //This function returns a resource used to query
    //the spellcheck webservice. It contains the HTTP method,
    //headers and will transform the response from XML to JSON
    function getSpellCheckerResource() {
        return $resource('/_vti_bin/spellcheck.asmx',
        {}, {
            post: {
                method: 'POST',
                params: {
                    'op': 'SpellCheck'
                },
                headers: {
                    'Content-Type': 'text/xml; charset=UTF-8'
                },
                transformResponse: function (data) {
                    // convert the response data to JSON
                    // before returning it
                    var x2js = new X2JS();
                    var json = x2js.xml_str2json(data);
                    return json;
                }
            }
        });
    }
             
    //This is the public function the TinyMCE editor will
    //call when the check spelling button is clicked.
    //The functions takes a block of text (or words) as input
    //and returns the spellcheck results
    function checkSpelling(words) {
    //Convert an array of words into a single string
    var wordstring = "";
    for(var i = 0; i < words.length; i++)
    {
        wordstring += (words[i] + ' ');
    }          
    //build the SOAP request
    var soapData = '<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><SpellCheck xmlns="http://schemas.microsoft.com/sharepoint/publishing/spelling/"><chunksToSpell><string>' + wordstring + '</string></chunksToSpell><declaredLanguage>3081</declaredLanguage><useLad>false</useLad></SpellCheck></soap:Body></soap:Envelope>'
    //Get the resource (defined in a function above) used to query the webservice
    var resource = getSpellCheckerResource();
    var deferred = $q.defer();
    //Post the data (the string of words) to the webservice 
    //and wait for a response!
    resource.post(soapData, function (data) {
        //successful callback          
        deferred.resolve(data.Envelope.Body.SpellCheckResponse.SpellCheckResult);
    }, function (error) {
        //error callback
        var message = 'Failed to queried the SharePoint SpellCheck webservice. Error: ' + error.message;
        deferred.reject(message);
    });
        return deferred.promise;
    }
}
})();

The screenshots below (taken from Fiddler and Chrome) show the request being sent and the data that is received back.

Request.


Response (showing the errors).


Response (showing the suggestions).



You can inspect the array of spelling suggestions and errors received back from the webservice by putting a break in the code.




Hpapy Spallnig!

References: