Introduction
The new state management and postback features of ASP.NET are indeed very exciting. They provide developers with a whole new range of mechanisms for producing dynamic web pages. The ability to write your own custom controls takes that ability to a whole new level allowing you to write a control with custom functionality that can easily be reused in multiple pages by simply defining custom tags, similar to any other HTML element. The person implementing the layout for a page no longer needs to know all the details of how to write client-side code to get the dynamic behavior that has become so popular. However, there are some pitfalls that developers need to be aware of. ASP.NET promotes server-heavy designs. Network traffic can be dramatically increased as each client-side event can potentially cause a round trip to the server. Many of the effects that result from these frequent trips to the server can easily be accomplished with a few simple JavaScript functions. Calls to the server should be kept to a minimum, with as much being done on the client as possible. Using custom controls to generate client-side script we can take advantage of Dynamic HTML on the client while still providing a measure of separation between the layout and the logic.
Client-Side script generation
One of the goals of generating script from a custom control is to allow a developer to create the control and specify it's behavior then publish it for others to use without having to know how the code works. We want to encapsulate the implementation of the control and tightly couple the HTML rendering to the script that works with it to reduce the possible points of failure associated with more traditional methods of web component reuse (i.e. cut & paste, and include files). The most straight forward approach to script generation is to write the script along with the control in the Render
method of your control (see code below:).
namespace Spotu
{
public class HelloWorld : Control
{
protected override void Render (
HtmlTextWriter writer
)
{
writer.Write(@"
<script>
function HelloWorld()
{
document.all('_msg').innerText = 'Hello World';
}
</script>");
writer.Write("<button onclick='javascript:HelloWorld()'>"
+ "Click Me</button>");
writer.Write("<div id=_msg></div>");
}
}
}
The code block below shows an example of a page using the HelloWorld
class with client script generation.
<%@ Page language="c#" %>
<%@ Register Namespace='Spotu'
TagPrefix='spotu'
Assembly ='helloworld' %>
<html>
<body>
<form runat="'server'">
<spotu:HelloWorld runat="'server'/">
</form>
</body>
</html>
This approach works, and does solve the initial problem of allowing a developer to write a custom control that someone else can use in their page to provide dynamic capabilities without having to post back to the server. However, it is not very elegant, and it does have some shortcomings, most notably we cannot include this control in a page multiple times, doing so would cause multiple divisions to be created with the same id. Even if we do uniquely name the elements in this example it is still inefficient because the JavaScript gets written out with every reference to this control. This can produce a lot of overhead, transmitting the same script down to the client for each instance of the control.
We need some way to have a control generate script, but generate it only once, even if multiple instances of the control are used on the same page. Fortunately for us the developers at Microsoft thought of this and provided a way to register a script block to ensure we only write out a section of script once using the Page.RegisterClientScriptBlock
method. This method takes two parameters, an id that identifies the script block so the Page
class will know to ignore any other requests to register the same block of code, and a string containing the script to be registered. The best place to register the script block is in the Init
event handler for the control. To take advantage of this event, override the OnInit
method of the Control
class. With this in mind the HelloWorld example could be rewritten as shown below:
using System;
using System.Web;
using System.Web.UI;
namespace Spotu
{
public class HelloWorld : Control
{
protected override void OnInit(EventArgs e)
{
string strCode = @"
<script>
function HelloWorld(id)
{
document.all(id).innerText = 'Hello World';
}
</script>";
Page.RegisterClientScriptBlock("Spotu_HelloWorld",
strCode);
}
protected override void Render(HtmlTextWriter writer)
{
writer.Write("<button onclick='javascript:HelloWorld(\""
+ this.UniqueID + "\")'>"
+ "Click Me</button>");
writer.Write("<div id='" + this.UniqueID
+ "'></div>");
}
}
}
This approach is much better but there is still a problem. If the script that is being register is lengthy, or if there are a lot of calculations, data accesses, etc. in generating our script we will still take a performance hit when the page loads as the control creates this huge block of script that ends up being tossed out because it is already registered. Once a block of script is registered we can test for it using the Page.IsClientScriptBlockRegistered
method. To improve the performance of the HelloWorld
control we would include the call in our OnInit
method as shown below:
protected override void OnInit(EventArgs e)
{
if (!Page.IsClientScriptBlockRegistered("Spotu_HelloWorld"))
{
string strCode = @"
<script>
function HelloWorld(id)
{
document.all(id).innerText = 'Hello World';
}
</script>";
Page.RegisterClientScriptBlock("Spotu_HelloWorld",
strCode);
}
}
Using client script generation from custom controls provides a clean encapsulated method of enabling dynamic behavior in web pages while still shielding the page designer from having to know the details of how to produce the desired effect. Developers are now free to concentrate on how to get a control to do what you want it to do without being bogged down with were to put it on the page, or being pestered by the marketing guy to move a control around, add a new one, or take one away. By combining this approach with designer integration controls with dynamic behaviors can easily be customized and reused across multiple pages with little or no developer interaction and with out the pitfalls of server side includes or cut & paste code reuse.
Caching
Some of you might be inclined to ask: How do I cache this script so it doesn't get downloaded every time? After all, client-side script tends to be fairly static, not needing to be downloaded every time a web page is loaded.
There are a couple of options for caching the output of your control. The ASP.NET approach would be to take advantage of output caching. There are a myriad of output caching options, but most of them place the responsibility of setting up that caching on the person doing the presentation by using directives and flags in the .aspx page. Also, caching the entire page may not be the desired effect. Some pages are extremely dynamic. In such cases the ideal would be to just cache the control, or some portion of the control. ASP.NET does have some support for this, but that support is reserved primarily for user controls (.ascx files), which doesn't provide the reuse we are looking for.
For custom controls providing generated script we may want to consider using an external script file. As we have already noted, most script does not change often, if at all and can readily be cached on the client. Instead of writing out the script directly from out custom control we can instead place the script in an external script file and simply write out a <script...>
tag with a src="..."
attribute that references our script file. This allows the control, and the page to fluctuate as often as necessary without incurring the network traffic of always downloading the script to the client. The primary drawback to the approach is the deployment. There are now two files that need to be deployed in order to use the control in a page and the .js file must be reachable from the page that is using it. Relative paths don't always work because each page that uses the control may be at a different level. One deployment solution is to create a directory at the top level of your application (example: includes) and reference it in your control using Request.ApplicationPath + "/includes/<your script file here>"
. Another approach might be to provide custom properties on you control so the location of the external source file can be specified in the .aspx page. Code shown below is an example of a calculator implemented using this approach.
using System;
using System.Web;
using System.Web.UI;
using System.Collections.Specialized;
namespace Spotu
{
public class Calculator : Control, IPostBackDataHandler
{
const string sc_strStyleClass = "calcButton";
private string _strNumButton;
private string _strOpButton;
private string _strScriptSrc;
private string _strStyleHref;
private string _strSavedValue;
private int _intCalcValue = 0;
public string ScriptSrc
{
get { return _strScriptSrc; }
set { _strScriptSrc = value; }
}
public string StyleSrc
{
get { return _strScriptSrc; }
set { _strScriptSrc = value; }
}
#region IPostBackDataHandler
public virtual bool LoadPostData (
string postDataKey,
NameValueCollection values
)
{
_strSavedValue = "Saved Value: "
+ values[UniqueID + "_display"];
return false;
}
public virtual void RaisePostDataChangedEvent()
{
}
#endregion
protected override void LoadViewState (
object savedState
)
{
_strSavedValue = savedState as string;
}
protected override object SaveViewState()
{
return _strSavedValue;
}
protected override void OnInit (
EventArgs e
)
{
_strNumButton = string.Format("<button "
+ "onclick='javascript:g_{0}.EnterNumber(this.innerText);'"
+ " class='{1}'>", this.UniqueID, sc_strStyleClass);
_strOpButton = string.Format("<button "
+ "onclick='javascript:g_{0}.OnOperator(this.innerText);' "
+ "class='{1}'>", this.UniqueID, sc_strStyleClass);
if (_strScriptSrc == null)
{
_strScriptSrc = Context.Request.ApplicationPath
+ "/includes/calc.js";
}
if (_strStyleHref == null)
{
_strStyleHref = Context.Request.ApplicationPath
+ "/includes/calcStyle.css";
}
string strScriptBlock = "<script src='"
+ _strScriptSrc
+ "'></script>";
Page.RegisterClientScriptBlock("Spotu_Calculator",
strScriptBlock);
string strStyle = "<link rel='stylesheet' "
+ "type='text/css' href='"
+ _strStyleHref
+ "'></link>";
Page.RegisterClientScriptBlock("Spotu_Calculator_Style",
strStyle);
}
protected override void OnLoad (
EventArgs e
)
{
if (Page.IsPostBack)
{
_intCalcValue =
Int32.Parse(Context.Request.Form[UniqueID
+ "_display"]);
}
}
protected override void Render (
HtmlTextWriter writer
)
{
string strHtml = string.Format(@"
<script> var g_{0} = new Calc('{0}_display'); </script>
<table>
<tr colspan='*'>
<input type='text'
name='{0}_display'
readonly=true
value={4}>
</input>
</tr>
<tr><td>{1}7</button></td>
<td>{1}8</button></td>
<td>{1}9</button></td>
<td>{2}/</button></td>
<td>
<button
class='{3}'
onclick='javascript:g_{0}.OnClear();'>
C
</button>
</td>
</tr>
<tr><td>{1}4</button></td>
<td>{1}5</button></td>
<td>{1}6</button></td>
<td>{2}*</button></td>
</tr>
<tr><td>{1}1</button></td>
<td>{1}2</button></td>
<td>{1}3</button></td>
<td>{2}-</button></td>
</tr>
<tr><td>{1}0</button></td>
<td></td>
<td>{1}.</button></td>
<td>{2}+</button></td>
<td>
<button
class='{3}'
onclick='javascript:g_{0}.OnEqual();'>
=
</button>
</td>
</tr>
</table>", UniqueID,
_strNumButton,
_strOpButton,
sc_strStyleClass,
_intCalcValue);
writer.Write(strHtml);
writer.Write("<INPUT type='submit' name='"
+ this.UniqueID + "' value='Save'></INPUT>");
writer.Write("<H3 id='" + UniqueID + "_savedVal'>"
+ _strSavedValue + "</H3>");
}
}
}
calculator.aspx
<%@ Page %>
<%@ Register Namespace='Spotu'
TagPrefix='spotu'
Assembly ='calc' %>
<html>
<body>
<form runat="'server'">
<spotu:Calculator runat="'server'/">
<hr>
<spotu:Calculator runat="'server'/">
</form>
</body>
</html>
JavaScript source file for calculator
function Calc(dispId)
{
this.intCurrentVal = 0;
this.intLastNum = 0;
this._op = "";
this.bEqual = false;
this.displayId = dispId;
this.EnterNumber = function(num)
{
if (this.bEqual)
this.OnClear()
if (this.intLastNum != 0)
this.intLastNum += num;
else
this.intLastNum = num;
document.all(this.displayId).value = this.intLastNum;
}
this.ComputeValue = function()
{
switch (this._op)
{
case '+':
this.intCurrentVal = Number(this.intCurrentVal)
+ Number(this.intLastNum);
break;
case '-':
this.intCurrentVal -= this.intLastNum;
break;
case '*':
this.intCurrentVal *= this.intLastNum;
break;
case '/':
this.intCurrentVal /= this.intLastNum;
break;
default:
this.intCurrentVal = this.intLastNum;
}
document.all(this.displayId).value = this.intCurrentVal;
}
this.OnOperator = function(op)
{
if (!this.bEqual)
this.ComputeValue();
this.bEqual = false;
this.intLastNum = 0;
this._op = op;
}
this.OnEqual = function()
{
this.ComputeValue();
this.bEqual = true;
}
this.OnClear = function()
{
this._op = "";
this.intCurrentVal = 0;
this.intLastNum = 0;
this.bEqual = false;
document.all(this.displayId).value = this.intCurrentVal;
}
}
Style sheet for calculator buttons
.calcButton
{
width=25;
}
Examining the code
One item to note is that the reference to the style-sheet that defines the style for the calculator buttons is located in the OnInit
method along with the script block registration. Registering blocks of client side code is not limited to "script" alone. The style sheet here is external allowing the designer the ability to modify the look and feel of the buttons by modifying the .css file. Another approach to allowing the page designer to change the look and feel of the calculator would be to implement custom properties, or better yet, custom properties with sub-properties to group them together (example: Font-Style, Font-Size, etc.). This approach seems somewhat limiting in that the designer can then only change the properties you have exposed. With style sheets the designer has all the options available to him/her that would be there if a standard HTML element was being used, options that would otherwise be unavailable since he/she does not have direct access to the HTML elements your custom control produces and would not be able to apply a class or style to them.
There is one block of script that is written out when the control is rendered instead of being included in the .js file. This allows multiple instances of the calculator control to be used in the same page. The UniqueID
property inherited from the Control
class is used to differentiate the controls from each other. The UniqueID
property is a unique identifier that identifies an instance of a control within a page.
The locations of the style sheet and the external script file default to an /includes directory located at the virtual application root. However, there are two custom properties provided that allow the designer to override where those file are located.
By using the UniqueID
for the control as the name for the submit button we make sure that the LoadPostData
method for our control only gets called when the 'Save' button for that control is clicked. If we had named the text box with the UniqueID
for the control then we would end up saving the calculated number for all the controls on the page, regardless of how the submit to the server was done. This example is a little contrived and if you are really serious about reducing server load you could alter the 'Save' button so that instead of posting the form back to the server it does a Web Services call.
Conclusion
Using custom controls to generate client-side script can have tremendous benefits. The custom control will look and behave similar to any other control written with ASP.NET making it easy to reuse and shielding the page designer from needing to know the details of how the code works. By using client-side scripting to create the dynamic behaviors you can greatly increase the responsiveness of the individual pages and the overall performance of your web site by significantly decreasing the number of calls that are made to the server. Using external files for your script has both positives and negatives. The pros include taking advantage of browser caching and easy access for customizability. The cons include a more complex deployment both in the production environment as well as the design time environment.
Downloads
Download and unzip the demo project into the root of a virtual application. The calculator.aspx file should be in the root directory of the virtual app, the calc.js and caclStyle.css files in a /includes directory under the virtual app., and the calc.dll in a /bin directory under the virtual application.