Tuesday, October 12, 2010

Implemeting Ajax in SharePoint with JSON services

Background

We wanted to show data from some external web service in to web part on SharePoint 2007. As we were accessing external services outside the domain, it was important to make these controls as Ajax controls on SharePoint.

We had following options to implement Ajax controls in SharePoint.

We decided to use JQuery instead of ASP.Net Ajax. Microsoft and JQuery have a long-term agreement and JQuery is the supported javascript platform for future MS projects. So it was clear to use JQuery for implementing Ajax controls in SharePoint.

Initially I started implementation by using PageMethod approach (Reference - http://encosia.com/2008/05/29/using-jquery-to-directly-call-aspnet-ajax-page-methods/ ). It was a decent approach, but PageMethod returns complete HTML which we have to render in some iframe. If we want to do some change in the way results are displayed on UI, or any small cosmetic change we have to change page method. Due to this it is not possible to use results returned by same page method and render it differently (Like the web service). Furthermore each time we call a PageMethod it creates instance of a page which would cause some performance issues.

Other possible options for Ajax implementation.

I sent pagemethod approach to my chief architects and he suggested following options to for implementing AJAX in SharePoint.

1) ASP.NET AJAX - it now has decent integration into SharePoint, but in general I don't recommend ever using it. I feel strongly that ASP.NET AJAX is not a viable AJAX platform and does not make sense for use even in a standard ASP.NET application. I can go into details sometime for anyone who is curious... In general I find that they've done a really bad job creating a solution "for the web" - rather it's all wrapped up in confusing abstractions for new web developers, which get in the way of anyone who actually understands how to write efficient web apps.

2) Callbacks to PageMethods (your code below) This is a decent alternative to ASP.NET AJAX, as it avoids the client-side junk, although it still depends on ASP.NET AJAX being installed for the back end. There are still some issues, though: in order to call back into a method, you must call back into the page, providing complete View State and re-creating the entire page just so that it can handle the callback. I'd prefer not to do this, because there's no reason to incur this cost.

Both of the above may require some extra steps to get them working in SharePoint. I don't know how much of this you've already done, nor how much may be now unnecessary due to service packs, but in order to add ASP.NET AJAX to SharePoint, see: http://sharepoint.microsoft.com/blogs/mike/Lists/Posts/Post.aspx?ID=3 and http://www.codeplex.com/ajaxifymoss

3) Callbacks to WebServices (an example is linked from the article) This makes more sense, since you are making callbacks into methods that are designed for callback speed. You do not need to provide ViewState or instantiate pages, so there is no extra overhead. On the other hand, it does result in some of the functionality for your callback being put into a separate code space (the web service vs. your control.) This isn't necessarily a bad thing, because 1) you end up with new, shared web services available, and 2) you can share the code in a common library that's used by both the web service and the control. Finally, there is some extra effort to provide web service output as JSON, which is a better format than XML for AJAX callbacks, though not required.

4) Callbacks to custom pages or services: similar to #3, this involves calling back to a "service." In this case the service is just a custom ASPX page that provides results in whatever format you'd like. This is the easiest way to work, but it's a bit non-standard and results in a lot of extra ASPX pages that need to be maintained. Plus these services aren't callable by other consumers, because you've built a custom transport. It's possible to alleviate some of these concerns by implementing an HttpHandler that takes care of the callbacks - in fact, that's what AE does. But I'd recommend #3 over this one.

My suggestion is that you proceed with the #3 option - calling back to services, which respond in JSON. Once you've gotten one service working, we can look for a way to make it easier to create new services, and add some infrastructure to our base code.


Calling xml web service from JQuery
With such a good comparison and possible options I started exploring how to call web service from JQuery. Initially I tried to call existing web services from JQuery. They were returning result as xmldocument.
I used following code snippet to call xml web service from JQuery

$(document).ready(function() {
$.ajax({
type: "POST",
url: "http://rahul-ind01/askme/formservice.asmx/GetForm",
data: "{}",
contentType: "application/json; charset=utf-8",
dataType: "xmlDocument",
success: function(msg) {
$('#RSSContent').removeClass('loading');
$('#RSSContent').html(msg);
}
});
});

Above code was returning result as XmlDocument, But we needed Json objects so that we can use results of web service easily in JQuery. Some libraries (http://www.json.org/json.js.) are available which does convert web service output to json format. But I it would be like another wrapper around web service call. some posts on net says that we need to add “ScriptService” attribute to the service and “[ScriptMethod(ResponseFormat = ResponseFormat.Json)]” attribute to the web method to return json output. But I was still not getting Json output. I was missing some .net 3.5 config entries in my web service config.

Web Service returning JSON as well as SOAP objects
Finally I could modify AE web services so that they return SOAP as well as JSON response.

Here are the changes that needs to be done in the web service.
1. Add [ScriptService] attribute for web service class as follows.

[WebService(Namespace = "http://www.askme.com/webservices")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
[ScriptService]
public class FormService : AskMeContentService
{
}
2. Add [ScriptMethod(ResponseFormat = ResponseFormat.Json)] attribute for web method as follows

[WebMethod(Description = "Gets a specific Form given the ID. The sub-forms are not included.")]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public Form GetForm(int formID)

There are multiple config entries related to .Net framework 3.5 which needs to be added in web.config of application which calls these web service from JQuery. Appendix contains additional entries added in web.config.

Following is the sample Jquery code which calls this web service which returns JSON objects.

$(function() {
$.ajax({
type: "POST",
url: "http://rahul-ind01/askme/formservice.asmx/GetForm",
data: '{formID:4991}',
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(msg) {
alert(msg.d);
$('#RSSContent').removeClass('loading').html(msg.d.BodyHTML);
},
failure: function(msg) {
debugger;
alert(msg.d);
}
});
});

Actual implementation started.

With web services returning JSON objects I started making changes in ASP pages that are deployed on SharePoint server to call web services using Jquery. I faced following problems.
1. Since web service was deployed on different server (AE server) and ASP pages were deployed on SharePoint server I started getting “Permission Denied” error. Problem was browser enforces that you cannot make javascript calls across servers. When you load one server page from “Server1” that creates Javascript, the browser enforces that it can’t make an AJAX callback to Server2, Only back to the original server (pages don't matter, but the server address must be the same.)
2. Our API returns user Id in AE. We find corresponding user in SharePoint by matching name and render image and profile links for that user. To get that user profile in JQuery we will have to call sharepoint profile web services in Jquery and also we need to make these services JSON enabled.

To overcome these issues we needed to have another set of web services which will be deployed in SharePoint server. These services will call AE services, map AE users with SharePoint users profile and return these objects as JSON objects. Web parts and pages deployed in SharePoint will call these services deployed in SharePoint and will render controls using JQuery.



ASP pages, deployed on SharePoint server calls JSON enabled AE services deployed on Same SharePoint server using Jquery. These AE services on SharePoint server calls AE services on AE server, adds user profile and any other required data from SharePoint and returns JSON output to JQuery call, which then renders results in ASP pages.


Web.Config Entries for JSON service.
Following are the config entries that need to be added in SharePoint web.config file for ASP AJAX controls to work properly on MOSS 2007. These are .net framework 3.5 related entries required for JSON enabled web services to return JSON data.

Following entries added under root <configuration> section.

<configSections>
<sectionGroup name="system.web.extensions" type="System.Web.Configuration.SystemWebExtensionsSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<sectionGroup name="scripting" type="System.Web.Configuration.ScriptingSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<section name="scriptResourceHandler" type="System.Web.Configuration.ScriptingScriptResourceHandlerSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="MachineToApplication"/>
<sectionGroup name="webServices" type="System.Web.Configuration.ScriptingWebServicesSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<section name="jsonSerialization" type="System.Web.Configuration.ScriptingJsonSerializationSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="Everywhere"/>
<section name="profileService" type="System.Web.Configuration.ScriptingProfileServiceSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="MachineToApplication"/><section name="authenticationService" type="System.Web.Configuration.ScriptingAuthenticationServiceSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="MachineToApplication"/>
<section name="roleService" type="System.Web.Configuration.ScriptingRoleServiceSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="MachineToApplication"/>
</sectionGroup>
</sectionGroup>
</sectionGroup>
</configSections>

Following 4 entries under <assemblies> section
<add assembly="System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
<add assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add assembly="System.Data.DataSetExtensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
<add assembly="System.Xml.Linq, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>

Following 3 entries under <httpHandlers> section

<add verb="*" path="*.asmx" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add verb="*" path="*_AppService.axd" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" validate="false"/>

No comments:

Post a Comment