Homepage AJAX engine framework

AJAX Samples Part 5: Implementing JavaScript Behaviors

mathertel -> AJAXEngine -> JavaScript Behaviours Tutorial


Building the client side script for a component using a JavaScript behavior

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.

The JavaScript Behavior mechnism is implemented in the jcl.js file. You can view the source code here.

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:

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.

Integration into the ASP.NET framework

Now it's time to reduce the complexity of USING the new control for developers by using the rich features of a server framework.

1. 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.

2. 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 tag.
  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

3. 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

4. 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.

Properties, Attributes and Parameters

The way parameters are passed to the behavior implementation is some kind of tricky:

  1. you write down a parameter into the *.aspx source file.
  2. when the page is called from the server the parameter is passed to the server side control.
  3. the parameter is then written out into the response (html) stream and send to the client.
  4. when the behavior is attached to the html element it is made available to javascript.
  5. the behavior implementation is using the parameter by using this.parameter.

There are some traps and tricks on the way.

Take care of uppercase characters in the parameter name. Parameters with uppercase characters work fine on the server but using them on the client breaks the xhtml format spec. You can use lowercase parameters on the server and the client and you don't get confused when writing code for the server platform and the client platform the same time.

If you want to make a parameter available to the server control you have to add a public property or a public field to the class.

Using public fields is working fine but Visual Studio will not help with intellisense then so I prefer using a encapsulated private field using a public property:

private string _datatype = String.Empty;

public string datatype {
  get { return _datatype; }
  set { _datatype = value; }
} // datatype

public string working_but_no_intellisense = String.Empty;

Passing null through a parameter just doesn't work because you cannot specify an attribute for a xml or html tag with a null value. I am using empty strings instead.

If no attribute value is specified in the source code you need to define a default value. The easiest is to assign the default value to the private server field declaration and always render the attribute into the response stream.

The firefox browser makes a big difference between attributes of a html element and a property of an object so when attaching a behavior to a html element all the attributes are copied into object properties.

Don't use any reserved words you know from C#, VB.NET, JAVA, JavaScript, HTML or the DOM specification as a name and don't start a name with "on" because this naming convention is used for identifying event handlers.

1. Adding a parameter to the dice sample server control

The sample up to now is only showing random numbers from 1 to 6. A new parameter named "maxnumber" should make it possible to get random numbers between 1 and any positive number greater than 2.:

private int _maxnumber = 6;

public int maxnumber {
  get { return _maxnumber; }
  set { _maxnumber = value; }
} // maxnumber

This parameter is not needed on the server side and we just pass it to the client through a html attribute:

<div id="<%=this.ClientID %>" class="Wuerfel" maxnumber="<%=this.maxnumber %>"
  unselectable="on">click</div>

You can find this implementation in wuerfel2.ascx.

2. Adding a parameter to the behavior

On the client side we need to declare tha parameter as well. The given assignment will always be overwritten be the attribute the server adds to the html element.

And then we must use.

// parameter to set the maximum number
maxnumber : 6, 

// find a random number
var n = Math.floor(Math.random()*(this.maxnumber-1))+1;

You can find this implementation in wuerfel2.js.

3. Using the new feature

Now the new parameter can be used on any wuerfel2 tag:

<uc1:Wuerfel2 ID="Wuerfel2" maxnumber="42" runat="server" />

You can find it in Wuerfel_05.aspx.

Event handling

Methods for event handling

The methods that are used to handle events from the mouse, keyboard or system are identified by their name prefix "on".  When the behavior is bound to the HTML element these methods are not just copied over from the JavaScript behavior declaration to the html element but are wrapped by a special function that looks like

function() {
  return method.apply(htmlElement, arguments);
}

This wrapper is generated automatically for all methods of the behavior that start with the 2 characters "on" to ensure that the JavaScript "this" pointer is pointing to the htmlElement the method belongs to. This really simplifies writing event code. Keep in mind that this special attachment of methods for events is based on this naming convention so don't name other methods of the control this way.

Simple Events

The first sample already used an event (onclick) and registered a method to calculate a new random number for the dice.

// classical event handler implementation
onclick: function(evt) {
  evt = evt || window.event;
  var src = evt.srcElement;
  
  src.rolling = 50;
  src.count = 0;
  src.rollNext();
}

Because the this pointer is adjusted to the htmlElement we can use it instead of finding the right element through the event property srcElement:

// simpler event handler implementation
onclick: function(evt) {
  this.rolling = 50;
  this.count = 0;
  this.rollNext();
}

Global Events

Sometimes it is not possible to implement an event code by using this simple on__ naming scheme because the event that the behavior needs is not thrown to the htmlElement of the behavior.

If you are interested in global events you need to attach a method by using the AttachEvent method that is available through the jcl object. Don't use the on___ naming scheme for this method:

jcl.AttachEvent(document, "onmousedown", this._onmousemove);

If you are not interested in these events all the time the handler can be detached by calling:

jcl.DetachEvent(document, "onmousemove", this._onmousemove);

Mouse Events

Mouse events are a little bit special when implementing a drag&drop scenario. The onmousedown will always be raised on the element that will be dragged around but the other 2 events mousemove (while dragging) and onmouseup (when dragging ends and the drop occours) may be raised on any other elent on the page or even on the document object itself. Because the event "bubbles up" we can get all the events by attaichg these 2 methods to the document object.

The sample at VBoxDemo.aspx is using the VBox.js behavior that allows changing the width of the vertical separation by dragging the line between the left and right content.


This page is part of the http://www.mathertel.de/AJAXEngine/ project. For updates and discussions see The AJAX Engine blog.

tags: , ,