Off course that is in some of the cases and not all of them. AJAX is a very beautifull framework and has some great possibilities. Sometimes however a simple lightweighted callback is all that's needed to perform a simple task. (Read #9 over here). It is, no let me rephrase that, it WAS not the easiest task to implement such a lightweighted callback however. That's where this new CallbackController control comes around the corner. First outline the challenge I was facing.
Right now I'm in the middle of a technical design for a big, interactive Sharepoint 2007 Portal. The problem I encountered though is not very Sharepoint specific. I needed to change control #1 on a specific client event of control #2. Let me explain this a little further. We have a slideshow control that shows images about a region, but as soon as a customer hovers a region on an imagemap, the slideshow must show different images. We have a Google Maps control, and as soon as a client hovers on a specific menu item, the Google maps control needs to show some different pointers. Both the images and pointers however come from a database(in this case a SPList).
I could go for AJAX right away, but my callbacks are very very simple. I do not need to maintain - and thus send back and forth - any viewstate. I do not need to rerender complete controls such as grids, since both mentioned controls are Javascript controls. So all I have to do is set some Javascript properties or call some javascript functions, but i DO need some data from the server so I got to have some way to retrieve this data. Another huge problem is that I can't register a ScriptMethod which lives inside a control. I must be a static method on the page or a webservice. In sharepoint however I can't add any methods to the page. All I can do is program some webparts that contain custom controls and methods.
ASP.Net 2.0 comes with callbacks and the ICallbackEventHandler interface and if you look around on the internet you'll find a lot of examples of how to implement the interface on a Page. Nice BUT AGAIN in Sharepoint we work with webparts containing controls and we can't implement interfaces on the page. So that won't work for us. We'll need to implement the interface on a control. With the Callback system we CAN get a GetCallbackEventReference to a control method! So I outlined a few requirements.
As always I have a lot of code here for you guys. The complete source and a sample web using the CallbackController is included in the download. This is just V0.1 and it's written as a proof of concept for my technical design but it does it's job pretty well so far.
using System;
using System.Web.Script.Serialization;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
namespace WMB {
/// <summary>
/// Summary description for MyCallBackControl
/// </summary>
[ParseChildren(typeof(CallbackTrigger), ChildrenAsProperties = true, DefaultProperty = "Triggers")]
public class CallbackController : Control, ICallbackEventHandler {
/// <summary>
/// Initializes a new instance of the <see cref="CallbackController"/> class.
/// </summary>
public CallbackController() {
}
/// <summary>
/// Gets or sets the callback script.
/// </summary>
/// <value>The callback script.</value>
public string CallbackScript { get; set; }
private CallbackTriggerCollection _triggers;
[DefaultValue((string)null), PersistenceMode(PersistenceMode.InnerDefaultProperty), MergableProperty(false)]
public CallbackTriggerCollection Triggers {
get {
if (_triggers == null) {
_triggers = new CallbackTriggerCollection();
}
return _triggers;
}
}
/// <summary>
/// Returns a JSON representation of the object. Just for an ease of use.
/// </summary>
/// <param name="value">The value.</param>
/// <returns></returns>
public static string JsonSerialize(object value) {
return new JavaScriptSerializer().Serialize(value);
}
/// <summary>
/// Raises the <see cref="E:System.Web.UI.Control.PreRender"/> event.
/// </summary>
/// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
protected override void OnPreRender(EventArgs e) {
Type callbackControlerType = typeof(CallbackController);
if (!Page.ClientScript.IsClientScriptBlockRegistered(callbackControlerType, this.ClientID)) {
string clientScript = string.Format("function Trigger_{0}(a,c){{{1}}};\n",
this.ClientID,
Page.ClientScript.GetCallbackEventReference(this, "a", CallbackScript, "c"));
Page.ClientScript.RegisterClientScriptBlock(callbackControlerType,
this.ClientID,
clientScript,
true);
}
if(!Page.ClientScript.IsStartupScriptRegistered(callbackControlerType, this.ClientID)){
string startupScript = string.Empty;
foreach(CallbackTrigger trigger in Triggers){
startupScript += "\n" + trigger.GetTriggerScript(this);
}
Page.ClientScript.RegisterStartupScript(callbackControlerType, this.ClientID, startupScript, true);
}
base.OnPreRender(e);
}
/// <summary>
/// Occurs when the <see cref="E:OnCallback"/> method gets called. The <see cref="E:RaiseCallbackEvent"/> does so.
/// </summary>
public event EventHandler<CallbackEventArgs> Callback;
/// <summary>
/// Raises the <see cref="E:Callback"/> event.
/// </summary>
/// <param name="e">The <see cref="WMB.CallbackEventArgs"/> instance containing the event data.</param>
protected virtual void OnCallback(CallbackEventArgs e) {
EventHandler<CallbackEventArgs> temp = Callback;
if (temp != null) {
temp(this, e);
}
}
#region ICallbackEventHandler Members
private string result;
/// <summary>
/// Returns the results of a callback event that targets a control.
/// </summary>
/// <returns>The result of the callback.</returns>
public string GetCallbackResult() {
return result;
}
/// <summary>
/// Processes a callback event that targets a control.
/// </summary>
/// <param name="eventArgument">A string that represents an event argument to pass to the event handler.</param>
public void RaiseCallbackEvent(string eventArgument) {
CallbackEventArgs e = new CallbackEventArgs(eventArgument);
OnCallback(e);
result = e.Result;
}
#endregion
}
/// <summary>
///
/// </summary>
public class CallbackEventArgs : EventArgs {
/// <summary>
/// Initializes a new instance of the <see cref="CallbackEventArgs"/> class.
/// </summary>
/// <param name="argument">The argument.</param>
public CallbackEventArgs(string argument) {
this.Argument = argument;
}
/// <summary>
/// Gets or sets the result.
/// </summary>
/// <value>The result.</value>
public string Result { get; set; }
/// <summary>
/// Gets the argument.
/// </summary>
/// <value>The argument.</value>
public string Argument { get; private set; }
}
}
And a page declaration:
1: <div>
2: <asp:Button ID="TimeButton" runat="server" Text="Click me and I'll write the server time. Right click me and I'll write the client time." />
3: <WMB:CallbackController
4: ID="CallbackController1"
5: CallbackScript="WriteTheResultToResultDiv"
6: OnCallback="CallbackController1_OnCallback"
7: runat="server">
8: <WMB:CallbackTrigger
9: ControlID="TimeButton"
10: ClientEvent="oncontextmenu"
11: Argument="new Date()"
12: CancleBubble="true" />
13: <WMB:CallbackTrigger
14: ControlID="TimeButton"
15: ClientEvent="onclick"
16: CancleBubble="true" />
17: </WMB:CallbackController>
18: </div>
It's now very simple to add callbacks to your page! I do not have the time to explain all details of the code. Simply download the solution and have a look at it. If you do have any questions, please feel free to ask.
Cheers,
Wes