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);
}
}
}