Freitag, Dezember 08, 2006

Building JavaScript Behaviors

During the last months I got some good feedback from readers and friends that point me to some missing topics in my AJAX book. I will continue to publish new content here before bringing a new version of the book.

This is a step by step instruction for building a new ASP.NET Control with a rich client side functionality by using JavaScript and a Behavior mechanism.

The sample functionality I use to show this is a simple dice (German: Würfel). It's a sample that I also used in my session at the "AJAX in Action" Conference in Germany some weeks ago.

You can download all files from http://www.mathertel.de/Downloads/Start_JSBTutorial.aspx.

1. Coding it all in one place

The best place for writing a new control is inside a single HTML file that contains all the fragments that you will separate later into different locations:

  • a HTML object structure that will be used for rendering the new control. This should be a single outer element that may contain complex inner HTML elements. Give it a unique id that can be used to identify the first prototype.
  • a CSS section inside the <head> element that will hold all the style rules that we will later move out into the common css file. You can code all the css rules into the html elements first if you like. Later you should not include any CSS code inside the rendered html elements mo make some personalization and style adoption easier.
  • a <script type="text/javascript"> element that will contain the JavaScript behavior definition using a object notation in the JSON coding style and the statement for binding the JavaScript behavior object to the HTML element.
  • include the common behavior loading mechanism <script type="text/javascript" src="../controls/jcl.js"></script> in the <head> element.

The advantage of using this intermediate development state is that you can hit F5 in the browser and can be sure that all your code will reload as expected. You also will not have any timing problems that may happen when JavaScript or CSS files are cached locally. You need no server side functionality so a *.htm file is fine for now.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN">
<html xmlns="http://www.w3.org/1999/xhtml">

<head>
  <title>Ein Wuerfel</title>
  <script type="text/javascript" src="jcl.js"></script>

  <style type="text/css">
    .Wuerfel {
      border: solid 2px green; width:40px; height:40px; overflow:hidden;
      cursor: pointer; background-color:#EEFFEE;
      font-size: 30px; padding:20px; text-align: center;
    }
  </style>

  <script type="text/javascript">
    var WuerfelBehaviour = {
      onclick: function(evt) {
        Wuerfel1.innerText = Math.floor(Math.random()*6)+1;
      }
    } // WuerfelBehaviour
  </script>
</head>

<body>
  <div id="Wuerfel1" class="Wuerfel" unselectable="on">click</div>
 
  <script defer="defer" type="text/javascript">
    jcl.LoadBehaviour("Wuerfel1", WuerfelBehaviour);
  </script>
</body>
</html>

The file wuerfel_01.htm contains an implementation in this state.

2. replacing all hard-coded references

If you want to make it possible to use the same control multiple times on the same page then you must avoid using hard coded ids or names. The only place where you should find the id of the outer HTML element is inside the first parameter of the jcl.LoadBehaviour function call.

All the other references should be replaced by using the "this" reference.

The other thing you should take care too are the parameters / attributes that you want to use together with the new control. You should define the as attributes in the outer HTML element and as properties of the JavaScript behavior definition. There should not be any constants inside the JSON object.

If everything is well done you can make a second copy of the outer HTML element with a new id and can bind the same behavior definition to it. Both elements should now work as expected independently. Check also if the parameters work as expected.

The file wuerfel_02.htm contains an implementation in this state.

3. separating the behavior code

The next step is to extract the core of the behavior into a new *.js file and reference this file by using a new <script type="text/javascript" src="wuerfel.js"></script> in the <head> element.

The advantage of a separate file for the behavior definition is that the implementation can be cached by the browser independently from the individual use and If the control is reused in different pages you can see dramatic performance improvements.

The file wuerfel_03.htm and wuerfel.js file contain an implementation in this state and wuerfel.js ??? has also got some more functionality.

4. separating the CSS style definitions

The style of the new control should not be coded inline into the html code but should be separated into some css statements. So I use a classname for the top element of the control by using the name of the behavior. If you have special inner elements they can be prefixed by the same name or you might use css selectors by specifying the outer and inner class names. Sample:

div.TreeView .do { ... }
div.TreeView .dc { ... }

Because the css statements are usually much smaller then the JavaScript code for a control I do not extract the css statements into separate files but include them all in a single css file for all the controls I've done. The *.css files are cached by the browser so loading them from the server doesn't occur too often.

5. converting to a ASP.NET User Control (*.ascx)

Now it's time to switch from a *.htm file to a *.aspx file because you will need some server side functionality now.

Rename the file and add a <%@ Page Language="C#" %> statement at the top of the page. In Visual Studio you will have to close the file and reopen it to get the full editor support for the right server side languages.

The html code and the javascript statement that binds the JavaScript Behavior of the new control is copied into the new User Control file wuerfel.ascx.

The id attribute that is rendered for the client should not be hardcoded to a static value. The UniqueID can be used and will produce the given id if one is specified in the *.aspx page.

<div id="<%=this.ClientID
    %>" class="Wuerfel" unselectable="on">click</div>
<script defer="defer" type="text/javascript">
  jcl.LoadBehaviour("<%=this.ClientID %>", WuerfelBehaviour);
</script>

Now it is easy to include the new control into the page by dragging the wuerfel.ascx file into a blank page while using the Design mode. The code will look like this:

<uc1:Wuerfel ID="Wuerfel1" runat="server" />

and a reference to the used control will also be generated:

<%@ Register Src="Wuerfel.ascx" TagName="Wuerfel" TagPrefix="uc1" %>

Open this file by using the browser and have a look to the source code that is delivered to the client - it will look very similar to what you had before. Again you can check whether everything is fine by pasting the <uc1:Wuerfel...> element several times. Visual Studio will automatically generate different ids so all elements work independent.

6. using the script including mechanism

You still have to take care of including the right JavaScript include in the <head> of your page. Now we also get this work done automatically. The advantage is that you do not have to take care of using the right include files and you will never forget to remove them when a control is removed from the page.

In the *.aspx page the <head> element must be marked with runat="server"

In the *.ascx file some server side programming is needed inside a <script runat="server"> tag:

protected override void OnPreRender(EventArgs e) {
  base.OnPreRender(e);

  if (Page.Header == null)
    throw new Exception("The <head> element of this page is not marked with
      runat='server'.");

  // register the JavaScripts includes without need for
    a Form.
  if (!Page.ClientScript.IsClientScriptBlockRegistered(Page.GetType(), "CommonBehaviour")) {
    Page.ClientScript.RegisterClientScriptBlock(Page.GetType(), "CommonBehaviour", String.Empty);
    ((HtmlHead)Page.Header).Controls.Add(new LiteralControl("<script type='text/javascript' src='"
      + Page.ResolveUrl("jcl.js")
      + "'><" + "/script>\n"));
  } // if

  if (!Page.ClientScript.IsClientScriptBlockRegistered(this.GetType(), "MyBehaviour")) {
    Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "MyBehaviour", String.Empty);
    ((HtmlHead)Page.Header).Controls.Add(new LiteralControl("<script type='text/javascript' src='"
      + Page.ResolveUrl("Wuerfel.js")
      + "'><" + "/script>\n"));
  } // if
} // OnPreRender

Have a look at the files wuerfel_04.aspx and wuerfel.ascx.

7. using a global registration for the control

When dragging a User Control onto a page the UserControl is registered for this page by using a server site Register tag.

<%@ Register Src="Wuerfel.ascx" TagName="Wuerfel" TagPrefix="uc1" %>

There is no real problem with that automatic stuff but if you copy HTML code around from one page to another you always have to take care of copying these Register tags as well.

Fortunately there is another solution that registers User Contols globally in the web.config file and needs no Register tags.

Open the web.config file you can find in the root of your web application and locate the <configuration><system.web><pages><controls> region. Here you can add a add element:

<add src="~/controls/LightBox.ascx" tagName="LightBox" tagPrefix="ve"/>

You can find many samples in the web.config file of the AJAXEngine demo web site project and there is a good post on this topic in Scott Guthrie's blog too at: http://weblogs.asp.net/scottgu/archive/2006/11/26/tip-trick-how-to-register-user-controls-and-custom-controls-in-web-config.aspx

8. converting to a ASP.NET Web Control (*.cs) implementation

When writing simple controls without nested other controls there is no need to convert a UserControl into a WebControl. You need this step only when the control will be used as a wrapper to more HTML code that is declared on the web page and not within the control itself. If you download the complete source code of the AJAX Engine project you can find some advanced implementations using ASP.NET Web Controls in the APP_Code folder. Writing WebControls and designers for Web Controls is not covered here.