Click here to Skip to main content
15,867,453 members
Articles / Web Development / ASP.NET
Article

ASP.NET Custom Controls - Client-Side Script Generation

Rate me:
Please Sign up or sign in to vote.
4.67/5 (48 votes)
7 Apr 20039 min read 232.8K   3.9K   121   18
Produce controls that encapsulate client-side script that are easily reused and provide dynamic behavior without incurring the overhead of frequent calls to the server.

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:).

C#
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.

ASP.NET
<%@ 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:

C#
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:

C#
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.

C#
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;


    // Custom property for explicitly setting the location
    // of the script file
    public string ScriptSrc
    {
      get { return _strScriptSrc;  }
      set { _strScriptSrc = value; }
    } // End ScriptSrc

    // Custom property for explicitly setting the location
    // of the stylesheet file
    public string StyleSrc
    {
      get { return _strScriptSrc;  }
      set { _strScriptSrc = value; }
    } // End StyleSrc

    #region IPostBackDataHandler

    // LoadPostData gets call when the 'save' button
    // rendered by this control is clicked
    public virtual bool LoadPostData (
      string postDataKey,
      NameValueCollection values
      )
    {
      _strSavedValue = "Saved Value: " 
               + values[UniqueID + "_display"];
      return false;
    } // end LoadPostData

    // Needed to implement IPostBackDataHandler
    public virtual void RaisePostDataChangedEvent()
    {
    } // End RaisePostDataChangedEvent

    #endregion

    // Loads the state of the control from the 
    // viewstate managed by .NET
    protected override void LoadViewState (
      object savedState
      )
    {
      _strSavedValue = savedState as string;
    } // End LoadViewState

    // Saves the state of the control
    protected override object SaveViewState()
    {
      return _strSavedValue;
    } // End SaveViewState

    // Init event handler, called to initialize any state
    // in the object before the viewstate is restored.
    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);
    } // End OnInit

    // Load Event Handler.  Retrieve the value posted in the
    // display field of the calculator so we can keep the
    // state of the display regardless of how the form is
    // submitted
    protected override void OnLoad (
      EventArgs e
      )
    {
      if (Page.IsPostBack)
      {
        _intCalcValue = 
          Int32.Parse(Context.Request.Form[UniqueID
                                           + "_display"]);
      }
    } // End OnLoad

    // Render out the control
    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>");
    } // End Render
  }
}

calculator.aspx

ASP.NET
<%@ 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

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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionData Caching to Cilent side Pin
Jilani Shaikh25-Jun-08 23:40
Jilani Shaikh25-Jun-08 23:40 
GeneralBrowser output Pin
kiran300425-May-07 1:30
kiran300425-May-07 1:30 
QuestionHow can I change to apply with calculator.cs Pin
HighSpeedWire16-Jul-06 14:34
HighSpeedWire16-Jul-06 14:34 
Generalcalculate total on client side Pin
rp69017-Aug-05 11:26
rp69017-Aug-05 11:26 
Questionhow to send mail using smtp server Pin
lalitsharma28-May-05 1:21
lalitsharma28-May-05 1:21 
GeneralWell well... Pin
Mattias Olgerfelt5-Sep-04 11:31
Mattias Olgerfelt5-Sep-04 11:31 
GeneralGood! Pin
Mubi | www.mrmubi.com25-Apr-04 21:39
professionalMubi | www.mrmubi.com25-Apr-04 21:39 
GeneralGulped down your code fine Pin
Arun Reginald Zaheeruddin11-Feb-04 21:57
Arun Reginald Zaheeruddin11-Feb-04 21:57 
GeneralFinally, documentation Pin
Keith Farmer10-Nov-03 9:44
Keith Farmer10-Nov-03 9:44 
GeneralThanks Pin
ThePhoenix8-Jul-03 22:11
ThePhoenix8-Jul-03 22:11 
GeneralProblem with RegisterClientScriptBlock Pin
Roger Willcocks12-Jun-03 13:30
Roger Willcocks12-Jun-03 13:30 
GeneralRe: Problem with RegisterClientScriptBlock Pin
Jim Spears27-Jun-03 12:23
Jim Spears27-Jun-03 12:23 
GeneralRe: Problem with RegisterClientScriptBlock Pin
Roger Willcocks27-Jun-03 21:47
Roger Willcocks27-Jun-03 21:47 
Found the cause.

The script does not render if there is no tag on the page.
GeneralVery informative Pin
ramky23-May-03 10:01
ramky23-May-03 10:01 
GeneralVery very nice Pin
nsimeonov15-Apr-03 18:38
nsimeonov15-Apr-03 18:38 
GeneralRe: Very very nice Pin
Steven Betts18-Apr-03 9:54
Steven Betts18-Apr-03 9:54 
GeneralHow can you apply with calculator.cs [modified] Pin
HighSpeedWire16-Jul-06 10:38
HighSpeedWire16-Jul-06 10:38 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.