Yesturday I blogged on the MS AJAX framework's PageMethod implementation falling short when it comes to control developers. Before I completely gave up on this, I decided to investigate some of the toolkit's controls to see how they handle these callbacks. Two controls came up in my search: Rating and ReorderList. I spent some time investigating the Rating control, which seems to utilize the classic ASP.NET 2.0 callback mechanism. The only issue I saw is that the control did not use the script returned to it by the GetCallbackEventReference method, rather it had hardcoded its call into its js file. While I don't anticipate that Microsoft will ever change this function declaration, I wouldn't want to bet on it. The method I am referring to is WebForm_DoCallback. I decided to search the entire toolkit for any other controls utilizing this approach. I didn't find any other controls but I did find a script called BaseScripts which really peaked my interest.
_invoke : function(name, args, cb) {
/// invokes a callback method on the server controlif (!this._callbackTarget) {
Seeing that this method is private, or at least meant to be since it is prefixed with an underscore, I tried to find where it is called from. I had found nothing. Perhaps it was only a partial implementation. I decided not to give up, rather, I did another search to see how a control's _callbackTarget property could be set. This search yielded this code in the ScriptObjectBuilder class
// determine if we should describe methods
foreach (MethodInfo method in instance.GetType().GetMethods(
BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public))
{
ExtenderControlMethodAttribute methAttr =
(ExtenderControlMethodAttribute)Attribute.GetCustomAttribute(
method, typeof(ExtenderControlMethodAttribute));
if (methAttr == null || !methAttr.IsScriptMethod)
{
continue;
}
// We only need to support emitting the callback target and registering
//the WebForms.js script if there is at least one valid method
Control control = instance as Control;
if (control != null)
{
// Force WebForms.js
control.Page.ClientScript.GetCallbackEventReference(control, null, null, null);
// Add the callback target
descriptor.AddProperty("_callbackTarget", control.UniqueID);
}
break;
}
This was very interesting to me, for it seems that if we denote an internal method as being a ScriptMethod and as long as the method is public, static, or an instance method we are going to be "hooked up".
Note: It seems odd that this is an or... looks like a bug. I would not assume you can have a private instance method and it still work.
I decided to make a simple control to try out this seemingly undocumented functionality.
My control was very simple. It inherits from the ScriptControlBase class, renders simple div tag with text, and has two methods marked as ExtenderControlMethod.
using System.Web.UI;
using System.ComponentModel;
using AjaxControlToolkit;
[assembly: WebResource("MyAjaxControls.SimpleCallback.SimpleCallback.js",
"application/x-javascript")]
namespace MyAjaxControls
{
[ClientScriptResource("MyAjaxControls.SimpleCallback",
"MyAjaxControls.SimpleCallback.SimpleCallback.js")]
public class SimpleCallback : ScriptControlBase
{
public SimpleCallback()
: base(false) {}
[DefaultValue("")]
[Category("Appearance")]
public string Text
{
get { return (string)(ViewState["Text"] ?? string.Empty); }
set { ViewState["Text"] = value; }
}
protected override void Render(HtmlTextWriter writer)
{
writer.AddAttribute(HtmlTextWriterAttribute.Id, ClientID);
writer.RenderBeginTag(HtmlTextWriterTag.Div);
writer.Write(this.Text);
writer.RenderEndTag();
//required to hook up element on client
ScriptManager.RegisterScriptDescriptors(this);
}
[ExtenderControlMethod]
public string HelloWorld(string s)
{
return "hello: " + s;
}
[ExtenderControlMethod]
public int Add(int x, int y)
{
return x + y;
}
}
}
The client side library was also simple. It simply hooked up the click event of the element and invoked our callback
Type.registerNamespace("MyAjaxControls");
MyAjaxControls.SimpleCallback = function(element) {
MyAjaxControls.SimpleCallback.initializeBase(this, [element]);
this._onclick$delegate = Function.createDelegate(this, this._onclick);
this._cbcomplete$delegate = Function.createDelegate(this, this._cbcomplete);
}
MyAjaxControls.SimpleCallback.prototype = {
initialize : function() {
MyAjaxControls.SimpleCallback.callBaseMethod(this, "initialize");
var element = this.get_element();
$addHandler(element, "click", this._onclick$delegate);
},
dispose : function() {
var element = this.get_element();
if(element) {
$removeHandler(element, 'click', this._onclick$delegate);
}
MyAjaxControls.SimpleCallback.callBaseMethod(this, "dispose");
},
_onclick : function(sender, e) {
//this._invoke('HelloWorld', ['hi'], this._cbcomplete$delegate);
this._invoke('Add', [20, 3], this._cbcomplete$delegate);
},
_cbcomplete: function(result, ctx)
{
alert(result);
}
}
MyAjaxControls.SimpleCallback.registerClass("MyAjaxControls.SimpleCallback", AjaxControlToolkit.ControlBase);
I must say that I am really pleased to find this implementation. Even though under the covers it is using a classic ASP.NET 2.0 callback, all the plumbing work to allow our multiple parameters along with any number of functions to be invoked is all handled for us. I am not sure why the _invoke is private, or if this is even functionality that should be used by control developers. Searching the internet was not helpful at all. The only reference I found on the subject is this blog.
ScriptControlBase implements some great new features that hopefully soon will make their way into ExtenderControlBase before the next real release. These include:
- Load/Save ClientState methods similar to Load/Save ViewState on the server and client-side
- ASP.NET 2.0 Callbacks
- ScriptUserControl to create custom AJAX controls using .ascx files. Facilitates creating reusable mashup controls when building web applications
- ControlBase.prototype.findElement() client method to find child objects using .NET Naming Containers (ScriptControlBase is an INamingContainer)
If anyone out there can shed some light on this functionality I would really appreciate it.