JavaScript Behaviors



The JavaScript behavior mechanism
As mentioned before, the basic idea behind the JavaScript Behavior
mechanism is using HTML elements for defining an object model of the
component and using JavaScript to implement the functionality. Here is a
small picture of the situation:
You can see that the outer HTML element that can have inner HTML
elements will be bound to the Behavior that is specified in JavaScript.
There are properties, events, public and private methods that make up the
Behavior object that is specified by using the JSON syntax.
A step by step instruction
The easiest way to understand building a new client side component by using
the JavaScript behavior mechanism is to do follow a step by step sample. It is
first implemented without using any server side help by only implementing
JavaScript in a plain html file. Then the JavaScript Behavior is modeled and at
last it is migrated into an ASP.NET user control.
There are 2 advantages when working this way. First you can focus on the
JavaScript implementation from the beginning and can avoid the typical cache
issues when using included JavaScript files and second you can see some of
the reasons where Behaviors take advantage over plain JavaScript
implementation.
The sample functionality I use to show this is a simple dice (German: Würfel).
It basically consists of defining new properties, specific methods and event
handlers by building a JavaScript prototype object for each behavior. Also a
common binding functionality is needed that attaches these definitions to a
HTML object after the page is loaded. Script include files can be used to bring
the prototype objects into the pages.
You can download all files from
http://www.mathertel.de/Downloads/Start_JSBTutorial.aspx.
Building the JavaScript Behavior Basics
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 JavaScript tag that includes the common behavior loading mechanism
<script type="text/javascript" src="../controls/jcl.js"></script>
in the <head> element.
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.
The <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.
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.
And a small <script type="text/javascript"> element that will cause to
bind the defined Behavior to the HTML 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.
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.
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:
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.
Building JavaScript Behaviors - 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 when using an
instance of the control on a page.
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 that 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.
Implementing event handlers was
simplified with the first 2007 version.
There is no more the need for the
special coding to get the compatible
event object.
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 occurs) may be raised on any
other element on the page or even on the document object itself. Because the
event "bubbles up" we can get all the events by attaching 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.

Post a Comment

You're welcome to share your ideas with us in comments.

Previous Post Next Post