Call Server-side Code from Javascript

Sunday, February 08, 2015

3

Server-side code and postbacks already seem like remnants of a bygone era.  The next generation of SharePoint developers will likely know no more about them than punched tape computers.  Yet, there are still scenarios where you just can't avoid server-side operations.

I recently needed a way to asynchronously execute a block of server-side code from client-side Javascript.  It turns out there is an ICallbackEventHandler interface for precisely that purpose!

There's a detailed MSDN article on it, which I did not find very easy to digest.  In this post I'll try to boil it down to the essentials by going over a SharePoint example.

Background

I was building a visual webpart to let users update their Quick Links and other User Profile properties.  I stubbed out my methods, whipped up some JSOM to grab the properties, and threw in a snazzy jQuery drag-and-drop control.

Then, I tried to implement the Save button.  Disaster struck.  User profiles are read-only from all client-side APIs.

The Big Picture

This diagram illustrates the flow of execution from client to server and back.  I'll go over each piece in detail, but this is how they all fit together.



Client Side

We'll start from the Javascript side, as that is where the user action begins.  ICallbackEventHandler lets us asynchronously fire off a server side method, to which we can pass a single string parameter.

Let's say my Quick Links are stored in <div>s on the page:

<div class="link" data-title="Google" data-url="http://www.google.com" />
<div class="link" data-title="Bing" data-url="http://www.bing.com" />
<div class="link" data-title="MSDN" data-url="http://www.msdn.com" />

We can push this information into a JSON object, serialize it into a string, and pass it server-side to be saved.

Invoke server-side code


At this point, the ExecuteServerSide method below that will invoke our server-side code isn't defined yet. We will wire it up later from the code-behind.

function save() {
    var links = [];

    // Create JSON object with link data
    $('.link').each(function () {
        var title = $(this).data('title');
        var url = $(this).data('url');

        links.push({ 'Title': title, 'Url': url });
    });

    // Serialize the object to a string
    var strLinks = JSON.stringify(links);

    // Invoke server-side code.  Will wire up later.
    ExecuteServerSide(strLinks);
}

Completion Handler


Next, let's add the method that will be called when the server-side operation completes.  The server can return a single string back to the page.

function ServerSideDone(arg, context) {
    alert('Message from server: ' + arg);
}

Server Side

Begin by adding the ICallbackEventHandler interface to the webpart's code-behind.  This interface has two methods that need to be implemented: RaiseCallbackEvent and GetCallbackResult.

public partial class QuickLinksEditor : WebPart, ICallbackEventHandler

RaiseCallbackEvent


RaiseCallbackEvent( string eventArg ) is the entry point.  It's what's called by ExecuteServerSide in line #16 of the Javascript above.  eventArg is the string passed from the client side (e.g. the serialized link data)  In most scenarios, you would save this string in a class variable and use it later to perform whatever server-side operation.

We could parse out the links manually, but it's cleaner to make a simple data model class:

[DataContract]
public class QuickLink
{
    [DataMember]
    public string Title;

    [DataMember]
    public string Url;
}

Then we can take advantage of DataContractJsonSerializer to convert the JSON string to a List of QuickLink objects:

public void RaiseCallbackEvent(string eventArg)
{
    // Instantiate deserializer
    DataContractJsonSerializer serializer =
       new DataContractJsonSerializer(typeof(List<QuickLink>));

    // Deserialize
    MemoryStream stream =
       new MemoryStream(System.Text.ASCIIEncoding.ASCII.GetBytes(eventArg));

    // Save input data to class variable for later processing
    this.Links = (List<QuickLink>)serializer.ReadObject(stream);
}

GetCallbackResult


This method is where we execute the server-side operation.

It returns a string, which is how information gets passed back to the client-side.  That return value is the ServerSideDone method's arg parameter in line #18 in the Javascript code above.

public string GetCallbackResult()
{
    try
    {
        // Get User Profile Manager and update Quick Links
        // Full code at bottom of article
    } 
    catch(Exception ex)
    {
        return ex.Message;
    }

    return "Success!";
}

Wire Up

Finally, we wire up the two client-side functions, ExecuteServerSide and ServerSideDone.  That is done from Page_Load in the code-behind:

protected void Page_Load(object sender, EventArgs e)
{
    ClientScriptManager scriptMgr = Page.ClientScript;

    // Completion handler
    String callbackRef = scriptMgr.GetCallbackEventReference(this, 
        "arg", "ServerSideDone", "");

    // Invoke server-side call
    String callbackScript = 
        "function ExecuteServerSide(arg, context) {" + 
        callbackRef + 
        "; }";

    // Register callback
    scriptMgr.RegisterClientScriptBlock(this.GetType(), 
       "ExecuteServerSide", callbackScript, true);
}

Again, note that line #7 here defines the method signature in line #18 of the Javascript.  And line #11 here defines the method called in line #16 of the Javascript.

That's it!

Complete Code-behind


using System;
using System.ComponentModel;
using System.Web.UI;
using System.Web.UI.WebControls.WebParts;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Collections.Generic;
using System.IO;
using System.Text;

using Microsoft.Office.Server.UserProfiles;
using Microsoft.Office.Server;

using Microsoft.SharePoint;

namespace SharePointificate.QuickLinksEditor
{
    [DataContract]
    public class QuickLink
    {
        [DataMember]
        public string Title;

        [DataMember]
        public string Url;
    }

    [ToolboxItemAttribute(false)]
    public partial class LegacyQuickLinks : WebPart
    {
        private List<QuickLink> Links;

        public LegacyQuickLinks()
        {
        }

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            InitializeControl();
        }

        protected void Page_Load(object sender, EventArgs e)
        {
            ClientScriptManager scriptMgr = Page.ClientScript;

            String callbackRef = scriptMgr.GetCallbackEventReference(this, 
                "arg", "ServerSideDone", "");

            String callbackScript = 
                "function ExecuteServerSide(arg, context) {" + 
                callbackRef + 
                "; }";

            scriptMgr.RegisterClientScriptBlock(this.GetType(), 
                "ExecuteServerSide", callbackScript, true);
        }

        public string GetCallbackResult()
        {
            try
            {
                SPServiceContext serviceContext = 
                    SPServiceContext.GetContext(SPContext.Current.Site);

                UserProfileManager userProfileManager = 
                    new UserProfileManager(serviceContext);

                UserProfile currentUser = 
                    userProfileManager.GetUserProfile(true);

                QuickLinkManager quickLinkManager = currentUser.QuickLinks;

                quickLinkManager.DeleteAll();

                foreach (QuickLink link in this.Links)
                {
                    quickLinkManager.Create(link.Title, link.Url, 
                        QuickLinkGroupType.General, null, Privacy.Public);
                }
            } 
            catch(Exception ex)
            {
                return ex.Message;
            }

            return "Success!";
        }

        public void RaiseCallbackEvent(string eventArgument)
        {
            DataContractJsonSerializer serializer = 
                new DataContractJsonSerializer(typeof(List<QuickLink>));

            MemoryStream stream = 
                new MemoryStream(ASCIIEncoding.ASCII.GetBytes(eventArgument));

            this.Links = (List<QuickLink>)serializer.ReadObject(stream);
        }
    }
}


3 comments:

Thanks, your article is very helpful.
I'm trying to call ExecuteServerSide method from my content editor web part javascript code in the same page, but it gives me "ExecuteServerSide is not defined" error.
Where does your javascript code reside in?

This comment has been removed by the author.

I didn't try to do this, but looking at the post, it suggest that the ExecuteServeSide method is placed in the Page_Load code-behind.

My guess is if it is in the page load definitions, it should be available to your CEWP, but I'm not an expert. Perhaps there is a misspelling?

Post a Comment