Getting JavaScript and ASP.NET talking (outside of AJAX)
Passing values between ASP.NET and JavaScript code is messy
A lot of effort has gone into simplifying the AJAX pattern, in which your JavaScript code calls methods on the server without requiring a full page post. You don't hear much about the synchronous case (SJAX?), though - how does your server-side code set values in JavaScript, and how do you set values in JavaScript and pass them back to the server when the page is submitted? In the diagram below - we've got AJAX covered pretty well, but what about the Rendered JavaScript case?
Parade of the Ugly Hacks
Until recently, if you had some settings in your server side code that you needed to act on in JavaScript code, you ended up using ugly hacks.
Ugly Hack #1 - Emit JavaScript strings
Emitting JavaScript code with global variables in a startup script. This requires writing out JavaScript as strings in your server-side code, which is ugly as hell.
string primateVariables = string.Format( "<script type='text/JavaScript>" + "var primateAddress='{0}';" + "var primateCreditRating='{1}';" + "</script>", SelectedPrimate.PrimateAddress, SelectedPrimate.PrimateRating );
Ugly Hack #2 - Make Unnecessary AJAX Calls
Using unnecessary client callbacks (a.k.a. AJAX webservices calls). The code may look clean, and it's the path of least resistance, but it's a very lazy and inefficient solution to the problem. There's no reason for your client-side code to get a value from the server via a webservice call when the value is known at the time the page is rendered. AJAX calls should be made when a user's interaction with the webpage requires you to communicate with the server. If there's information that's known when you're writing out the page, you shouldn't be making another call back to the server just because the code's easier.
Ugly Hack #3 - Muck Around With Hidden Form Fields
The idea here is that you stash values in hidden form fields which are read via JavaScript. This is probably the least ugly of the above, as it does allow for a two-way communication mechanism (as the JavaScript can modify the form value to pass it back to the server. First, we'll set the field value in our server-side code:
ClientScriptManager.RegisterHiddenField("primateViews", PrimateViews.ToString());
var primateViews; if($get("primateViews") && $get("primateViews").value != ""){ $get("primateViews").value = parseInt($get("primateViews").value) + 1; } else { $get("primateViews").value = 1; }
A Better Solution - IScriptControl
ASP.NET AJAX solves this problem with the IScriptControl interface. This solution cleans up the code on both the client and server. Rather than injecting values into JavaScript strings on the server, the IScriptControl.ScriptDescriptor mechanism gives you a simple way to pass information to the client as properties of a custom JavaScript object. Now that you're able to do that, there's not reason to modify your JavaScript code at runtime, so your script can be a static file - essentially a resource. I was deep into figuring this out on my own when I got the January edition of MSDN Magazine, which included Fritz Onion's article titled Encapsulate Silverlight with ASP.NET Controls. It's a great article (although you need keep in mind that he's building on the Silverlight controls included in the ASP.NET Futures release which has since been replaced by the controls in the ASP.NET 3.5 Extensions Preview). It helped quite a a bit, but I still had to bang on it for a while to get it working. In my experience, the IScriptControl interface works well when it's working, but it's a little tough to set up.
Works on both Server Controls and User Controls
While the common usage is to create or extend a WebControl (a.k.a. a Server Control), you can also implement the IScriptControl interface on a UserControl. Don't just take my word for it, though - take a look at the ScriptUserControl, a part of the AjaxControlToolkit. Here's the class signature:
public class ScriptUserControl : UserControl, IScriptControl, IControlResolver, IPostBackDataHandler, ICallbackEventHandler, IClientStateManager
About The Sample Code
My project extends the ASP.NET Silverlight controls, which are some of the extra-special goodness that is the ASP.NET 3.5 Extensions Preview. I'm going to base my sample code on the MSDN walkthrough on Adding ASP.NET AJAX Client Capabilities to a Web Server Control, though, to keep it simple. I'm going to change a few things in the MSDN walkthrough, but it's pretty similar.
Using ScriptDescriptor
First, you'll need to do some work on the server side to implement the ScriptDescriptor interface. That's done by implementing (or overriding, if you're inheriting from a base control) the IScriptControl.GetScriptDescriptors() method, which returns an IEnumerable<ScriptDescriptor>. The simple case to demonstrate is implementing (rather than extending) a control. For this sample, let's assume we're going to extend a WebControl with a loading image, so we'll be including three control properties which will be utilized in our client-side JavaScript: ShowLoadingImage (boolean), LoadingImage (string), and DisplayTime (int). So, first, we'll set up those control properties:
[Category("Behavior"), DefaultValue(true), Browsable(true)] public virtual bool ShowLoadingImage { get { return (bool)(ViewState["ShowLoadingImage"] ?? true); } set { this.ViewState["ShowLoadingImage"] = value; } } [Category("Behavior"), DefaultValue(true), Browsable(true)] public virtual string LoadingImage { get { return (string)(ViewState["LoadingImage"] ?? string.Empty); } set { this.ViewState["LoadingImage"] = value; } } [Category("Behavior"), Browsable(true), DefaultValue((int)5)] public virtual int DisplayTime { get { object DisplayTimeSetting = this.ViewState["DisplayTime"]; if (DisplayTimeSetting != null) { return (int)DisplayTimeSetting; } return 5; } set { if (value < 1) { throw new ArgumentOutOfRangeException("value", value, "DisplayTime must a positive integer representing the time duration in seconds."); } this.ViewState["DisplayTime"] = value; } }
Great, now we need to tell the ScriptControl that the values of the above properties will need to be exposed to our JavaScript object. We'll do that by implementing IScriptControl.GetScriptDescriptors():
protected virtual IEnumerable<ScriptDescriptor> GetScriptDescriptors() { ScriptControlDescriptor descriptor = new ScriptControlDescriptor("SampleCode.SampleScriptControl", this.ClientID); descriptor.AddProperty("loadingImage", this.LoadingImage); descriptor.AddProperty("showLoadingImage", this.ShowLoadingImage); descriptor.AddProperty("displayTime", this.DisplayTime); return new ScriptDescriptor[] { descriptor }; } IEnumerable<ScriptDescriptor> IScriptControl.GetScriptDescriptors() { return GetScriptDescriptors(); }
The when the ScriptControl is rendered, it writes out JavaScript call to $create(), and if the ScriptControl includes any ScriptDescriptors, those are included in the $create call. Here's how that looks in action:
- ScriptControl renders a JavaScript call to Sys$Component$create(type, properties, events, references, element), passing all your setting in the properties parameter in JSON syntax. Here's how that JavaScript would look if we'd added an instance of the control to our page, named it MySampleControl1, and set some property values
Sys.Application.add_init(function() { $create(SampleCode.SampleScriptControl, { "showLoadingImage": true, "loadingImage": "http://s3.amazonaws.com/sample/load.png", "displayTime": 5 }, null, null, $get("MySampleControl1")); });
It's important to keep in mind that you're not writing the above JavaScript; it's being rendered by the ScriptControl. - $create() instantiates your object and calls _setProperties() which calls the setters for all properties which were passed in during the $create() call.
ScriptReference just loads JavaScript files
Since the ScriptDescriptors are doing the work of passing the property values to your JavaScript controls, the ScriptReference mechanism has a pretty simple job - load JavaScript files. Since we don't have to generate any JavaScript at runtime, we're free to treat our scripts as an embedded resources.
IEnumerable<ScriptReference> IScriptControl.GetScriptReferences() { return GetScriptReferences(); } protected virtual IEnumerable<ScriptReference> GetScriptReferences() { ScriptReference reference = new ScriptReference(); reference.Path = ResolveClientUrl("SampleScriptControl.js"); return new ScriptReference[] { reference }; }
The Last Mile - How Does That JavaScript Look?
Since the ScriptDescriptors are doing the work of passing the property values to your JavaScript controls, the ScriptReference mechanism has a pretty simple job - load JavaScript files. Here's a stripped down sample - it's pretty repetitive, so don't get worried by the length:
Type.registerNamespace('SampleCode'); SampleCode.SampleScriptControl = function(element) { SampleCode.SampleScriptControl.initializeBase(this, [element]); this._showLoadingImage = null; this._loadingImage = null; this._displayTime = null; } SampleCode.SampleScriptControl.prototype = { initialize : function() { //Your initialize code here - wire up event delegates }, dispose : function() { $clearHandlers(this.get_element()); //Your dispose code here }, //Property accessors get_showLoadingImage : function() { return this._showLoadingImage; }, set_showLoadingImage : function(value) { if (this._showLoadingImage !== value) this._showLoadingImage = value; }, get_loadingImage : function() { return this._loadingImage; }, set_loadingImage : function(value) { if (this._loadingImage !== value) this._loadingImage = value; }, get__displayTime : function() { return this._displayTime; }, set_displayTime : function(value) { if (this._displayTime !== value) this._displayTime = value; }, // Your event and control code here... } SampleCode.SampleScriptControl.registerClass('SampleCode.SampleScriptControl', Sys.UI.Control); if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();
The important things to notice here is that we're declaring our class with properties which match our script descriptors. The ASP.NET AJAX $create() method will call the appropriate property setters for each matching script descriptor, so our $create() call sample above will result in the following property setter calls:
set_showLoadingImage(true); set_loadingImage("http://s3.amazonaws.com/sample/load.png"); set_displayTime(5);
Putting It All Together
We started with a problem - it's hard to get property values from server-side code to JavaScript objects. Then we looked at the ScriptControl / IScriptControl solution, which does the following things for us:
- Provides a way to keep our settings on a per-component level, so we don't run into the problems our global variable solutions hit.
- Provides a way to wire our server-side properties to client-side properties in a way that's clearly spelled out, and easy for others to extend or maintain.
- We're never treating JavaScript as strings. We reference our JavaScript as a file, declare how our server-side properties map to client-side properties, and the wiring up is done for us.
There's more to the subject:
- If you're building an extender control, thing get a bit more complex.
- We breezed over the actual client-side logic and events. There are a few good articles which go through this in more detail, such as the MSDN walkthrough and Fritz Onion's article.
- A great source of information is the source of the AJAX Control Toolkit, which is available on CodePlex.