Proper Web User Control client-side event hooking through JavaScript
By George Mihaescu
Summary: Shows how to properly implement ASP.Net Web
User Control client-side event hooking through JavaScript so that the control
can co-exist in pages that user other controls with a similar requirement.
Tested using ASP.Net 2.0 on IE 6.0 and 7.0 and Firefox 1.5
and 2.0.
The problem
You are implementing a Web User Control which you intend to
use in many pages of many sites (and even possibly sell to ASP.Net developers).
Your control however needs to have some initialization done when the document
is loaded. No problem, you think, you simply add a JavaScript line in your
control’s aspx:
document.onload = control_initialization_js_function;
Of course, this will work… only if the page using the
control needs only one such control. If the page happens to use multiple
instances of this control or different controls that have the need to
initialize on the document onload, handler, then the last one to hook the event
wins, and all the others will stay un-initialized. And this is valid for any UI
event-hooking the control needs to do.
The bottom line is that a Web User Control that needs to
hook in any client-side event handling must do so without disrupting whatever such
even handling the using page (or other controls with similar needs) might
already have in place.
The solution
Of course, there is always leaving this issue to the caller.
Your control has an InitControl JavaScript function and you simply document
that it is the control user’s responsibility to make sure this function gets
called in the document’s onload handler. This low-tech solution is not
particularly nice to the user of your control (to whom you’re actually passing
a buck that belongs to you) and may not even work in more complex scenarios
(where you need to temporarily hook an event, then later un-hook it).
A much better solution would be one in which the control is
self-contained; the control user just drops it on his page and sets its
properties just like he does with any other control, without having to do any
special JavaScript trick the control may need. The control takes care of
hooking the events itself. We achieve this through JavaScript event chaining
(for lack of a better term), a simple technique through which we dynamically
create a handler for the event in question, and this handler’s body always
calls the previous handler that was hooked for that event (if there was one).
If each control used in a page uses this technique, their
handlers will “chain”, resulting in all the handlers being called. For example,
assume that the page has its own onload handler called page_handler and we have
three controls, each with its own handler: control1_handler, control2_handler
and control3_handler, respectively. Then the technique I described is the
equivalent of having the following handler:
<script language="javascript" type="text/javascript">
function conceptual_handler ()
{
page_handler ();
control1_handler ();
control2_handler ();
control3_handler ();
}
</script>
<body onload="conceptual_handler ();">
...
</body>
So how does this actually happen? Below is an example
consisting of a master page and two Web user controls, which the master page
uses. The master page and each control has its own onload handler, and each
control correctly chains its handler on the window.onload.
UserControl1.aspx:
<%@ Control
Language="C#"
AutoEventWireup="true"
CodeFile="UserControl1.ascx.cs"
Inherits="UserControl1"
%>
<script language="javascript" type="text/javascript">
function control1OnLoadHandler()
{
alert ("JS:
initializing user control 1");
}
function addLoadEventHandler(func)
{
var previous_handler
= window.onload;
if(typeof window.onload != "function")
window.onload = func;
else window.onload
= function()
{
previous_handler ();
func();
}
}
//add this control's onLoad handler to
the document's onload, without
//replacing whatever handler may be
there already
addLoadEventHandler (control1OnLoadHandler);
</script>
<asp:Label ID="Label1" runat="server" Text="This is a label in UserControl1"></asp:Label>
UserControl2.aspx:
<%@ Control
Language="C#"
AutoEventWireup="true"
CodeFile="UserControl2.ascx.cs"
Inherits="UserControl2"
%>
<script language="javascript" type="text/javascript">
function control2OnLoadHandler()
{
alert ("JS:
initializing user control 2");
}
function addLoadEventHandler(func)
{
var previous_handler
= window.onload;
if(typeof window.onload != "function")
window.onload = func;
else window.onload
= function()
{
previous_handler ();
func();
}
}
//add this control's onLoad handler to
the document's onload, without
//replacing whatever handler may be
there already
addLoadEventHandler (control2OnLoadHandler);
</script>
<asp:Label ID="Label1" runat="server" Text="This is a label in UserControl2"></asp:Label>
MasterPage.master:
<%@ Master
Language="C#"
AutoEventWireup="true"
CodeFile="MasterPage.master.cs"
Inherits="MasterPage"
%>
<%@ Register
Src="UserControl1.ascx"
TagName="UserControl1"
TagPrefix="uc1"
%>
<%@ Register
Src="UserControl2.ascx"
TagName="UserControl2"
TagPrefix="uc2"
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD
XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Untitled
Page</title>
<script type="text/javascript" language="javascript">
function
masterPageOnLoadHandler ()
{
alert ("JS:
init the master page called");
}
</script>
</head>
<body onload="javascript:masterPageOnLoadHandler ();">
<form id="form1" runat="server">
This is the master page content.<br /><br />
<uc1:UserControl1 ID="UserControl1_1" runat="server" /><br /><br />
<uc2:UserControl2 ID="UserControl2_1" runat="server" /><br /><br />
<div>
<asp:contentplaceholder id="ContentPlaceHolder1" runat="server">
</asp:contentplaceholder>
</div>
</form>
</body>
</html>
The trick is actually in the addLoadEventHandler function which checks to
see whether there is already a handler registered on the event we care for
(here, onload):
·
if not, registers as handler the function that it received as
parameter
·
if yes, dynamically creates a new handler that it registers for
the event, and the new handler first calls the previously registered handler,
than the function that was passed as parameter.
Then this addLoadEventHandler
function is called having as parameter the function the control needs to register
as event handler. This basically creates the logical “function chain” having
the same effect as the conceptual_handler
shown before.
For the sake of completeness, to have each of the controls
even better behaved, its JavaScript code should be placed in its own namespace.
This ensures that there are no clashes among the JavaScript functions used by
various controls within a page – this becomes critical if you are implementing
a suite of such controls for distribution to other ASP.Net developers:
<%@ Control
Language="C#"
AutoEventWireup="true"
CodeFile="UserControl2.ascx.cs"
Inherits="UserControl2"
%>
<script language="javascript" type="text/javascript">
var JSControl2 = {}; //JS
namespace for this control
JSControl2.control2OnLoadHandler = function ()
{
alert ("JS:
initializing user control 2");
}
JSControl2.addLoadEventHandler = function (func)
{
var
previous_handler = window.onload;
if(typeof window.onload != "function")
window.onload = func;
else window.onload
= function()
{
previous_handler();
func();
}
}
//add this control's onLoad handler to
the document's onload, without
//replacing whatever handler may be
there already
JSControl2.addLoadEventHandler
(JSControl2.control2OnLoadHandler);
</script>
<asp:Label ID="Label1" runat="server" Text="This is a label in UserControl2"></asp:Label>
Shortcomings: only one that I can see so far, and a
minor one for that matter: you have no control over the order in which the
handlers of the controls are called. The mechanism described above basically
calls the handlers in the order in which the controls appear in the page.
But if the controls need to have their handlers called in a
specific order (e.g. must first call the handler for control 4, then 2, then 3,
and only at the end for control 1) then there is something wrong with the design
of the controls and / or page that contains them. Controls should not be aware
or affected (in their behaviour) by the presence of other controls on the page
– after all, this is exactly why we’ve gone through all the trouble above: to
create encapsulated, self-contained controls that take care of hooking in their
environment automatically if they need to.