Creating JavaScript Components and ASP.NET Controls
Every now and again I'll actually meet someone who realizes that you don't need a JavaScript framework to make full-scale AJAX applications happen... but rarely in the Microsoft community. Most people think you need Prototype, jQuery, or ASP.NET AJAX framework in order to do anything from networking calls, DOM building, or component creation. Obviously this isn't true. In fact, when I designed the Brainbench AJAX exam, I specific designed it to test how effectively you can create your own full-scale JavaScript framework (now how well the AJAX developer did on following my design, I have no idea).
So, today I would like to show you how you can create your own strongly-typed ASP.NET-based JavaScript component without requiring a full framework. Why would you not have Prototype or jQuery on your web site? Well, you wouldn't. Even Microsoft-oriented AJAX experts recognizes that jQuery provides an absolutely incredible boost to their applications. However, when it comes to my primary landing page, I need that to be extremely tiny. Thus, I rarely include jQuery or Prototype on that page (remember, Google makes EVERY page a landing page, but I mean the PRIMARY landing page.)
JavaScript Component
First, let's create the JavaScript component. When dealing with JavaScript, if you can't do it without ASP.NET, don't try it in ASP.NET. You only use ASP.NET to help package the component and make it strongly-typed. If the implementation doesn't work, then you have more important things to focus on.
Generally speaking, here's the template I follow for any JavaScript component:
window.MyNamespace = window.MyNamespace || {}; //+ //- MyComponent -// MyNamespace.MyComponent = (function( ) { //- ctor -// function ctor(init) { if (init) { //+ validate and save DOM host if (init.host) { this._host = init.host; //+ this.DOMElement = $(this._host); if(!this.DOMElement) { throw 'Element with id of ' + this._host + ' is required.'; } } else { throw 'host is required.'; } //+ validate and save parameters if (init.myParameter) { this._myParameter = init.myParameter; } else { throw 'myParameter is required.'; } } } ctor.prototype = { //- myfunction -// myfunction: function(t) { } }; //+ return ctor; })( );
You may then create the component like the following anywhere in your page:
new MyNamespace.MyComponent({ host: 'hostName', myParameter: 'stuff here' });
Now on to see a sample component, but, first, take note of the following shortcuts, which allow us to save a lot of typing:
var DOM = document; var $ = function(id) { return document.getElementById(id); };
Here's a sample Label component:
window.Controls = window.Controls || {}; //+ //- Controls -// Controls.Label = (function( ) { //- ctor -// function ctor(init) { if (init) { //+ validate and save DOM host if (init.host) { this._host = init.host; //+ this.DOMElement = $(this._host); if(!this.DOMElement) { throw 'Element with id of ' + this._host + ' is required.'; } } else { throw 'host is required.'; } //+ validate and save parameters if (init.initialText) { this._initialText = init.initialText; } else { throw 'initialText is required.'; } } //+ this.setText(this._initialText); } ctor.prototype = { //- myfunction -// setText: function(text) { if(this.DOMElement.firstChild) { this.DOMElement.removeChild(this.DOMElement.firstChild); } this.DOMElement.appendChild(DOM.createTextNode(text)); } }; //+ return ctor; })( );
With the above JavaScript code and "<div id="host"></div>" somewhere in the HTML, we can use the following to create an instance of a label:
window.lblText = new Controls.Label({ host: 'host', initialText: 'Hello World' });
Now, if we had a button on the screen, we could handle it's click event, and use that to set the text of the button, as follows:
<div> <div id="host"></div> <input id="btnChangeText" type="button" value="Change Value" /> </div> <script type="text/javascript" src="Component.js"></script> <script type="text/javascript"> //+ in reality you would use the dom ready event, but this is quicker for now window.onload = function( ){ window.lblText = new Controls.Label({ host: 'host', initialText: 'Hello World' }); window.btnChangeText = $('btnChangeText'); //+ in reality you would use a muli-cast event btnChangeText.onclick = function( ) { lblText.setText('This is the new text'); }; }; </script>
Thus, components are simple to work with. You can do this with anything from a simple label to a windowing system to a marquee to any full-scale custom solution.
ASP.NET Control
Once the component works, you may then package the HTML and strongly-type it for ASP.NET. The steps to doing this are very simple and once you do it, you can just repeat the simple steps (some times with a simple copy/paste) to make more components.
First, we need to create a .NET class library and add the System.Web assembly. Next, add the JavaScript component to the .NET class library.
Next, in order to make the JavaScript file usable my your class library, you need to make sure it's set as an Embedded Resource. In Visual Studio 2008, you do this by going to the properties window of the JavaScript file and changing the Build Action to Embedded Resource.
Then, you need to bridge the gap between the ASP.NET and JavaScript world by registering the JavaScript file as a web resource. To do this you register an assembly-level WebResource attribute with the location and content type of your resource. This is typically done in AssemblyInfo.cs. The attribute pattern looks like this:
[assembly: System.Web.UI.WebResource("AssemblyName.FolderPath.FileName", "ContentType")]
Thus, if I were registering a JavaScript file named Label.js in the JavaScript.Controls assembly, under the _Resource/Controls folder, I would register my file like this:
[assembly: System.Web.UI.WebResource("JavaScript.Controls._Resource.Label.js", "text/javascript")]
Now, it's time to create a strongly-typed ASP.NET control. This is done by creating a class which inherits from the System.Web.UI.Control class. Every control in ASP.NET, from the TextBlock to the GridView, inherits from this base class.
When creating this control, we want to remember that our JavaScript control contains two required parameters: host and initialText. Thus, we need to add these to our control as properties and validate these on the ASP.NET side of things.
Regardless of your control though, you need to tell ASP.NET what files you would like to send to the client. This is done with the Page.ClientScript.RegisterClientScriptResource method, which accepts a type and the name of the resource. Most of the time, the type parameter will just be the type of your control. The name of the resource must match the web resource name you registered in AssemblyInfo. This registration is typically done in the OnPreRender method of the control.
The last thing you need to do with the control is the most obvious: do something. In our case, we need to write the client-side initialization code to the client.
Here's our complete control:
using System; //+ namespace JavaScript.Controls { public class Label : System.Web.UI.Control { internal static Type _Type = typeof(Label); //+ //- @HostName -// public String HostName { get; set; } //- @InitialText -// public String InitialText { get; set; } //+ //- @OnPreRender -// protected override void OnPreRender(EventArgs e) { Page.ClientScript.RegisterClientScriptResource(_Type, "JavaScript.Controls._Resource.Label.js"); //+ base.OnPreRender(e); } //- @Render -// protected override void Render(System.Web.UI.HtmlTextWriter writer) { if (String.IsNullOrEmpty(HostName)) { throw new InvalidOperationException("HostName must be set"); } if (String.IsNullOrEmpty(InitialText)) { throw new InvalidOperationException("InitialText must be set"); } writer.Write(@" <script type=""text/javascript""> (function( ) { var onLoad = function( ) { window." + ID + @" = new Controls.Label({ host: '" + HostName + @"', initialText: '" + InitialText + @"' }); }; if (window.addEventListener) { window.addEventListener('load', onLoad, false); } else if (window.attachEvent) { window.attachEvent('onload', onLoad); } })( ); </script> "); //+ base.Render(writer); } } }
The code written to the client may looks kind of crazy, but that's because it's written very carefully. First, notice it's wrapped in a script tag. This is required. Next, notice all the code is wrapped in a (function( ) { }) ( ) block. This is a JavaScript containment technique. It basically means that anything defined in it exists only for the time of execution. In this case it means that the onLoad variable exists inside the function and only inside the function, thus will never conflict outside of it. Next, notice I'm attaching the onLoad logic to the window.load event. This isn't technically the correct way to do it, but it's the way that requires the least code and is only there for the sake of the example. Ideally, we would write (or use a prewritten one) some sort of event handler which would allow us to bind handlers to events without having to check if we are using the lameness known as Internet Explorer (it uses window.attachEvent while real web browsers use addEventListener).
Now, having this control, we can then compile our assembly, add a reference to our web site, and register the control with our page or our web site. Since this is a "Controls" namespace, it has the feel that it will contains multiple controls, thus it's best to register it in web.config for the entire web site to use. Here's how this is done:
<configuration> <system.web> <pages> <controls> <add tagPrefix="c" assembly="JavaScript.Controls" namespace="JavaScript.Controls" /> </controls> </pages> </system.web> </configuration>
Now we are able to use the control in any page on our web site:
<c:Label id="lblText" runat="server" HostName="host" InitialText="Hello World" />
As mentioned previously, this same technique for creating, packaging and strongly-typing JavaScript components can be used for anything. Having said that, this example that I have just provided borders the raw definition of useless. No one cares about a stupid host-controlled label.
If you don't want a host-model, but prefer the in-place model, you need to change a few things. After the changes, you'll have a template for creating any in-place control.
First, remove anything referencing a "host". This includes client-side validation as well as server-side validation and the Control's HostName property.
Next, put an ID on the script tag. This ID will be the ClientID suffixed with "ScriptHost" (or whatever you want). Then, you need to inform the JavaScript control of the ClientID.
Your ASP.NET control should basically look something like this:
using System; //+ namespace JavaScript.Controls { public class Label : System.Web.UI.Control { internal static Type _Type = typeof(Label); //+ //- @InitialText -// public String InitialText { get; set; } //+ //- @OnPreRender -// protected override void OnPreRender(EventArgs e) { Page.ClientScript.RegisterClientScriptResource(_Type, "JavaScript.Controls._Resource.Label.js"); //+ base.OnPreRender(e); } //- @Render -// protected override void Render(System.Web.UI.HtmlTextWriter writer) { if (String.IsNullOrEmpty(InitialText)) { throw new InvalidOperationException("InitialText must be set"); } writer.Write(@" <script type=""text/javascript"" id=""" + this.ClientID + @"ScriptHost""> (function( ) { var onLoad = function( ) { window." + ID + @" = new Controls.Label({ id: '" + this.ClientID + @"', initialText: '" + InitialText + @"' }); }; if (window.addEventListener) { window.addEventListener('load', onLoad, false); } else if (window.attachEvent) { window.attachEvent('onload', onLoad); } })( ); </script> "); //+ base.Render(writer); } } }
Now you just need to make sure the JavaScript control knows that it needs to place itself where it has been declared. To do this, you just create a new element and insert it into the browser DOM immediately before the current script block. Since we gave the script block and ID, this is simple. Here's basically what your JavaScript should look like:
window.Controls = window.Controls || {}; //+ //- Controls -// Controls.Label = (function( ) { //- ctor -// function ctor(init) { if (init) { if (init.id) { this._id = init.id; //+ this.DOMElement = DOM.createElement('span'); this.DOMElement.setAttribute('id', this._id); } else { throw 'id is required.'; } //+ validate and save parameters if (init.initialText) { this._initialText = init.initialText; } else { throw 'initialText is required.'; } } //+ var scriptHost = $(this._id + 'ScriptHost'); scriptHost.parentNode.insertBefore(this.DOMElement, scriptHost); this.setText(init.initialText); } ctor.prototype = { //- setText -// setText: function(text) { if(this.DOMElement.firstChild) { this.DOMElement.removeChild(this.DOMElement.firstChild); } this.DOMElement.appendChild(DOM.createTextNode(text)); } }; //+ return ctor; })( );
Notice that the JavaScript control constructor creates a span with the specified ID, grabs a reference to the script host, inserts the element immediately before the script host, then sets the text.
Of course, now that we have made these changes, you can just throw something like the following into your page and to use your in-place JavaScript control without ASP.NET. It would look something like this:
<script type="text/javascript" id="lblTextScriptHost"> window.lblText = new Controls.Label({ id: 'lblText', initialText: 'Hello World' }); </script>
So, you can create your own JavaScript components without requiring jQuery or Prototype dependencies, but, if you are using jQuery or Prototype (and you should be!; even if you are using ASP.NET AJAX-- that's not a full JavaScript framework), then you can use this same ASP.NET control technique to package all your controls.