Monday, September 29, 2014

Quick and Dirty AJAX Functionality for SharePoint 2010 Web Parts

Mike Mcdermott, Senior Solutions Architect

Even as more and more organizations migrate to SharePoint 2013 or to Office 365, there is still plenty of SharePoint 2010 work to be done. There remains production maintenance work, enhancements to existing SharePoint sites, and new development for organizations who absolutely need the functionality, but aren’t ready yet to migrate to 2013 and beyond.

It was this this last scenario where I was asked to come in to quickly implement a web part to provide completely custom search and sort functionality for an existing SP list containing data on in-house publications.

My requirements were simple: develop a web part that implemented searching, filtering, and sorting on a list of publications, which would be provided for me via a data access layer (against SharePoint using CAML queries) written by another developer. Because of questions around the eventual deployment of the web part, it needed to be completely stand-alone and I would not be able to rely on an application page or leverage SP web services. All I would have were any properties I defined on the web part and the data access layer given to me.

Full disclosure: I am fairly recent to SharePoint development, the majority of my career on the Microsoft stack has been spent developing ASP.NET and MVC web applications with some web services thrown in for flavor. I’m sure, like everything else Microsoft, that there are a million and one ways to solve this problem but I wanted to provide a user experience akin to that which I could provide when developing in MVC: I wanted the user experience to be quick and light weight with a lot of client side AJAX calls for data retrieval and manipulation. The problem was that I wasn’t sure where or how to host the end points for the AJAX calls.

Not knowing where to start, our own Jonathan Rupp pointed me to a web part for our internal projects site where he had similar limitations and leveraged the ICallbackEventHandler to achieve the same client driven look and feel I was looking for.

Per the MSDN page, the ICallbackEventHandler is used to indicate that a control can be the target of a callback event on the server. Since a SharePoint 2010 visual web part is really just a user control, this was exactly what I was looking for.

The implementation is not exactly intuitive, especially if you’re used to implementing client callbacks using jQuery in ASP.NET or MVC, so here is an example of my implementation.

Note: this solution leverages jQuery and json2.js which may, or may not be available to you at the site level.  You could minimize them inline, but I don’t necessarily recommend that for reasons beyond the scope of this article.  Another option if those two libraries aren’t available and you don’t want to inline them would be to use vanilla JavaScript and plain string based messages.

For starters, create a new visual web part and have it implement ICallbackEventHandler.  That entails implementing 2 methods, string GetCallbackResult() and void RaiseCallbackEvent:

using System;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Web.UI.WebControls.WebParts;

using System.Web.Script.Serialization;

using Microsoft.SharePoint.Utilities;



namespace CallbackExample.WebParts.CallbackExample

{

    public partial class CallbackExampleUserControl : UserControl, ICallbackEventHandler

    {

        private SomeDataObject _callbackResult = null;



        protected void Page_Load(object sender, EventArgs e)

        {

            if (!IsPostBack)

            {

                ddlCallback1.Items.Add(new ListItem("Item 1", "1"));

                ddlCallback1.Items.Add(new ListItem("Item 2", "2"));

                ddlCallback1.Items.Add(new ListItem("Item 3", "3"));

            }

        }





        protected override void CreateChildControls()

        {

            base.CreateChildControls();

            try

            {

                // set up our javascript to call the server

                var script = Page.ClientScript;



                var callback1 = script.GetCallbackEventReference(this, "a", "Callback1_RecieveData", "ctx", "Callback1_ServerError", false);

                var callback1ServerName = string.Format("Callback1Server_{0}", this.ClientID);

                script.RegisterClientScriptBlock(this.GetType(), callback1ServerName, "function " + callback1ServerName + "(a, ctx) { " + callback1 + "; }", true);

                ddlCallback1.Attributes["onchange"] = "Callback1_DoServerCall(" + callback1ServerName + ", jQuery('#" + ddlCallback1.ClientID + "')); return false;";

                btnCallback1.Attributes["onclick"] = "Callback1_DoServerCall(" + callback1ServerName + ", jQuery('#" + btnCallback1.ClientID + "')); return false;";





                // wire the event handler in case the JS breaks or the user has it disabled

                //btnCallback1.Click += new EventHandler(btnCallback1_Click);

            }

            catch (Exception ex)

            {

                lblError.Visible = true;

                lblError.Text = string.Format("Error rendering webpart: {0}", ex.Message);

            }

        }





        protected string ICallbackEventHandler.GetCallbackResult()

        {

            var jsSerializer = new JavaScriptSerializer();

            jsSerializer.MaxJsonLength = Int32.MaxValue;

            var results = jsSerializer.Serialize(_callbackResult);

            return results;

        }





        protected void ICallbackEventHandler.RaiseCallbackEvent(string eventArgument)

        {

            SPUtility.ValidateFormDigest();

            

            //eventArgument is the string posted from the client.  In this example, it is a serialized SomeDataObject

            var someDataObject = new JavaScriptSerializer().Deserialize<SomeDataObject>(eventArgument)



            //Call your business logic here

            var results = someDataObject;

            results.ID++;



            //Assign the return message, if applicable, back to _callBackResult

            _callbackResult = results

        }

    }



    public class SomeDataObject

    {

        public int ID { get; set; }

        public string Name { get; set; }

    }

}

 

In CreateChildControls you’ll want to setup the JavaScript callbacks and register them.  You may also want to have fallback click event handlers in the event that the JavaScript calls aren’t supported in the client browser.

RaiseCallbackEvent is the initial entry point by the client side code.  The method takes a single string parameter and this is the value passed by the client.  This can be a simple string value, or for more complicated solutions, it can be a JSON serialized object or collection of objects.  Here is where you want to call out to your business log to act on the message, and if you want to return a value back to the client, you’ll want to assign it to _callbackResult.

GetCallbackResult() is responsible for serializing the contents of _callbackResult and returning it to the client.

In addtion to the C# code above, you’ll need some JavaScript methods wired up client side.  These method names should match what is referenced in CreateChildControls and will correspond one to one with the 2 ICallbackEventHandler methods – one to pass data to the server, one to receive it.

 

function Callback1_DoServerCall(fn, ctx) {



    //Create the object that will be serialized to the server.  This is the message you're posting back to the web control.

    var someData = new Object();

    someData.ID = $('.ID').val();

    someData.Name = $('.Name').val();



    //Serialize the object and send it to the server

    fn(JSON.stringify(someData), ctx);

}



function Callback1_RecieveData(a, ctx) {

    ExecuteOrDelayUntilScriptLoaded(function () {

        var data = jQuery.parseJSON(a);



        //Do something with the data, like assign a value to a DOM element

        $('.ID').val(data.ID);





    }, "sp.js");

}



function Search_ServerError() {

    //Show an error message

}

 

Lastly, you can have as many of these send->receive pairs as you like to accomplish different goals, just be sure to follow the pattern and make sure you get your naming conventions correct.

 

While this solution is not nearly as clean and easy to understand as a jQuery AJAX call to an MVC or WebAPI controller, the end product was a rich and robust web part that is very flexible with respect to where and how it could be deployed to the SharePoint farm, but still provided the quick and seamless user experience that can be provided with new MVC applications.  It’s a quick and dirty AJAX style client server interaction when SharePoint 2013 isn’t an option