Friday, March 27, 2009

.NET Ajax - Using Sys.Component.create and $create

I recently spent some time getting intimate with the .NET Ajax library v1.0 and found the documentation for Sys.Component.create and $create particularly ambiguous, so I've decided to talk about it here to help other developers facing similar problems. First I'd like to again clarify that for backwards compatibility with the .NET 2.0 app I'm working with I've been using .NET Ajax 1.0 as opposed to the newer 2.0 version.

The doc's provide a similar example in all cases:

$create(MyControl, {id: 'c1', visible: true}, {click: showValue}, null, $get('button1'));

This would be fine, however I've referred to four Microsoft doc sources on the 'create' method and there are two main issues:
  1. The "id" property cannot be set on a Sys.UI.Control object, rather only on a Component or Behavior.
  2. The fourth argument's syntax, references, is never thoroughly explained. This is true even in the newest version of the docs.
It appears that the example is actually a mashup of instantiations for two object types, Sys.Component and Sys.UI.Control.

Issue 1:
If you try to set the id property on a Control object you'll get a JavaScript exception thrown along the lines of:
Error: Sys.InvalidOperationException: The id property can't be set on this object.
You can set the id property without issue on Component objects and I believe Behavior objects as well.

Issue 2:
For the other issue I will simply describe the existing documentation further, since something left incomplete cannot exactly be dubbed wrong. The MS documentation describes the references parameter as follows:
(Optional) A JSON object that describes the properties that are references to other components.

I was also able to turn up some documentation that describes the references parameter a bit further, but is still not quite enough to be useful, http://www.asp.net/AJAX/Documentation/Live/tutorials/CreatingCustomClientControlsTutorial.aspx:
An optional JSON object that contains references to associated components, passed as component name/ID pairs.

The components referred to here are other JavaScript objects created with the $create() method. So if you create an object A and set its id to 'c1', you can create an object B and then refer to object A by passing a property in the references parameter with 'c1' as the value. The undocumented mystery here is that the $create() method will extrapolate an object reference out of the id string value you supply. Figuring this out is made even more difficult when you are seemingly unable to set the id value in the first place if it's a Control (see issue 1). The main question here to answer is what should be passed as an id, a DOM id or object id? When using a Sys.Component object, there is not a corresponding DOM element associated with it and so you must specify an id during object instantiation, which is the id you will refer to for references. When using a Sys.UI.Control object, the id will derive from the required associated DOM element and this is the id value to pass in the references list. Below I've included a really simple example demonstrating the use of the references parameter to refer to another .NET Ajax component.

//------------------
// ContentManager.js
//------------------

/// <reference name="MicrosoftAjax.js"/>

Type.registerNamespace("ContentManager");

ContentManager.TestA = function() {
ContentManager.TestA.initializeBase(this);

this._othercontrol = null;
}

ContentManager.TestA.prototype = {
show: function() {
alert('TestA show function called.\nthis._id: ' + this._id + '\nthis.get_id(): ' + this.get_id());
},
set_othercontrol: function(val) {
this._othercontrol = val;
},
get_othercontrol: function() {
return this._othercontrol;
},
getUsed: function(arg) {
alert("TestA getUsed called, passed this val: " + arg);
}
}

ContentManager.TestA.registerClass('ContentManager.TestA', Sys.Component);

ContentManager.TestB = function(a) {
ContentManager.TestB.initializeBase(this, [a]);

this._othercontrol = null;
}

ContentManager.TestB.prototype = {
show: function() {
alert('TestB show function called.\nthis._id: ' + this._id + '\nthis.get_id(): ' + this.get_id());
},
set_othercontrol: function(val) {
this._othercontrol = val;
},
get_othercontrol: function() {
return this._othercontrol;
},
useOtherControl: function() {
this._othercontrol.getUsed("Passed from TestB!");
},
getUsed: function(arg) {
alert("TestB getUsed called, passed this val: " + arg);
}
}

ContentManager.TestB.registerClass('ContentManager.TestB', Sys.UI.Control);


ContentManager.TestC = function(a) {
ContentManager.TestC.initializeBase(this, [a]);

this._othercontrol = null;
}

ContentManager.TestC.prototype = {
show: function() {
alert('TestC show function called.\nthis._id: ' + this._id + '\nthis.get_id(): ' + this.get_id());
},
set_othercontrol: function(val) {
this._othercontrol = val;
},
get_othercontrol: function() {
return this._othercontrol;
},
useOtherControl: function() {
this._othercontrol.getUsed("Passed from TestC!");
}
}

ContentManager.TestC.registerClass('ContentManager.TestC', Sys.UI.Control);

// Notify the ScriptManager that this is the end of the script.
if (typeof (Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();



//--------------
// AJAXTest.aspx
//--------------

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="AJAXTest.aspx.cs" Inherits="management_AJAXTest" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Ajax Test</title>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server">
<Scripts>
<asp:ScriptReference Path="~/ContentManager.js" />
</Scripts>
</asp:ScriptManager>
<asp:Button ID="Button1" runat="server" Text="Button1" />
<asp:Button ID="Button2" runat="server" Text="Button2" />
<script type="text/javascript">
var app = Sys.Application;
app.add_load(applicationLoadHandler);

function applicationLoadHandler() {
// instantiate a component
var aCtrl = $create(ContentManager.TestA, { id: 'controla' });
// instantiate a control
var bCtrl = $create(ContentManager.TestB, null, null, { othercontrol: 'controla' }, $get('Button1'));
// instantiate a control
var cCtrl = $create(ContentManager.TestC, null, null, { othercontrol: 'Button1' }, $get('Button2'));

// Display alert message from TestA, TestB, and TestC
aCtrl.show();
bCtrl.show();
cCtrl.show();

// Display alert message from TestA (aCtrl var) via TestB (bCtrl var) property referencing TestA.
bCtrl.get_othercontrol().show();
// Display alert message from TestB via TestC property referencing TestB, 'Button2'.
cCtrl.get_othercontrol().show();

// Display alert message calling TestB function that uses TestA behind the scenes.
bCtrl.useOtherControl();
cCtrl.useOtherControl();
}
</script>
</form>
</body>
</html>

Running the example will give you a simple page with two buttons that pops up three alert messages on page load. In a real circumstance it would probably make more sense if I applied these alerts via some onclick handler function, but it gets the point across.

Cheers,
Mike

References: