Practical .NET

Build an ASP.NET JavaScript Generator

ASP.NET provides a wealth of options for dynamically integrating JavaScript into your client-side pages. And by adding T4 into the mix, you can generate, at runtime, exactly the client-side code that your page needs.

Most of the JavaScript you put in your ASP.NET page is static: You write it at design time, either in a library or in the page itself. But there are at least three scenarios when dynamically generating JavaScript at runtime on the server can make your ASP.NET application simpler and faster.

For instance, if you have a UserControl that incorporates client-side code, you need to ensure that any page it's hosted on includes the JavaScript libraries your UserControl depends on. At the same time, you don't want your page to download any more JavaScript than necessary. The solution is to have each UserControl dynamically add the JavaScript libraries it needs at runtime, while not adding any library already added by the page.

And while AJAX is great, reaching back to the server from the client so you can retrieve information is necessarily slow. As long as the amount of data isn't huge, it's faster to just embed essential data in the page and skip the AJAX call (a practice that also reduces demands on your server).

Finally, if you have a page that handles several different scenarios, you could write some general-purpose JavaScript code that handles all the scenarios. Unfortunately, that code will have to contain If or Select Case statements, making it harder to test and debug than code dedicated to handling a specific scenario. Dynamically generating the JavaScript code you need on the server solves this problem by letting you create just the code needed for this request. If you use the runtime code-generation tools in the Microsoft Text Template Transformation Toolkit (T4), your JavaScript code-generation process can be extremely simple.

Supporting JavaScript in UserControls
The control point for inserting JavaScript code into your ASP.NET page on the server is the ClientScriptManager, available from the ClientScript property of the Page class. A good first step, then, is to retrieve that object in the Page_Load event and store it in a field so you can use it elsewhere in the page. That's what this code does:

Dim csm As ClientScriptManager
Protected Sub Page_Load(ByVal sender As Object, 
  ByVal e As System.EventArgs) Handles Me.Load
  csm = Me.ClientScript;    
End Sub

Retrieving the ClientScriptManager in a UserControl is only slightly more complicated -- you have to go to the UserControl Page property to get to the ClientScript property:

csm = Me.Page.ClientScript

Now that you have the ClientScriptManager in a UserControl, you can use its RegisterClientScriptInclude to add script tags for JavaScript libraries your control needs. You must pass the RegisterClientScriptInclude method two parameters: a key to mark the request and the URL of the script to be downloaded. The key allows you to use the ClientScriptManager's IsClientScript­IncludeRegistered method to check to see if a tag with that key has already been loaded. Using IsClientScriptIncludeRegistered isn't required -- the ClientScriptManager is smart enough not to add the same library twice. However, using IsClientScriptInclude­Registered is supposed to be faster than relying on the Client­ScriptManager's internal checking (I've never bothered to test this).

This code in a UserControl checks to see if the key "OverStock.js" has already been registered. If it hasn't, the UserControl adds a script tag to download the OverStock.js script to the page it's a part of:

If csm.IsClientScriptIncludeRegistered("OverStock.js") = False Then
  csm.RegisterClientScriptInclude("OverStock", "Scripts/Overstock.js")
End If

The resulting HTML will look something like this:

 <body >
   <form method="post" action="InsertCode.aspx " id="OverStockForm" >
   <div class="aspNetHidden" >
     <input type="hidden" name="__VIEWSTATE" ...
   </div >
   <script src="Scripts/Overstock.js" type="text/javascript" > </script >

By using RegisterClientScriptInclude in your UserControl, the developer building a page with your UserControl doesn't have to be aware that your UserControl needs the library: Your UserControl takes care of itself. And it doesn't matter if the developer building the page adds two copies of your UserControl to the page or if some other UserControl needs the OverStock library: The library will be added to the page exactly once. You do need to ensure that the same key is used for every request for a library -- using the library's file name is a good way to ensure this.

Embedding Data in the Page
When you need the absolutely most-recent data from the server in a page, use an AJAX call to retrieve it from the server. However, if it's good enough that the data is up-to-date when the page is created, using AJAX is overkill. If the data is part of the page UI (for instance, in a dropdown list), you can store the data in a control. However, if you have data that isn't in any control, you can still put it in the page by wrapping it up in a JavaScript array with the ClientScriptManager RegisterArrayDeclaration method.

When using the RegisterArrayDeclaration, pass the name of your array as it will be used in your JavaScript code, and a string containing a set of comma-delimited values. That string can be ugly, though. This example appears to create an array of company divisions to be used in code:

Dim divisions As String = "Central, South, Overseas"
csm.RegisterArrayDeclaration("Divisions", divisions)

However, the resulting JavaScript code looks like this:

// <![CDATA[
var Divisions =  new Array(Central, South, Overseas);    
//]] >

JavaScript is going to treat the array members as function names, rather than as strings. To get the array members treated as strings, you'll need a string like this:

Dim divisions As String = """Central"", ""South"", ""Overseas"""

The resulting JavaScript will now look like what you want:

// <![CDATA[
var Divisions =  new Array("Central", "South", "Overseas");
//]] >

As with registering a script tag, ASP.NET won't add two arrays with the same name. However, when ASP.NET sees a second Register­ArrayDeclaration with the same array name, ASP.NET adds the new item to the existing array. You're probably going to be loading your array from a database, so you can take advantage of this to simplify your code. This example builds an array from the Northwind database Categories table, using the Entity Framework to retrieve the data:

Dim ne As New northwndEntities
For Each cat In ne.Categories
  csm.RegisterArrayDeclaration("Divisions", """" & cat.CategoryName & """")
Next

The resulting JavaScript array declaration looks like this:

// <![CDATA[
var Categories =  new Array("Beverages", "Condiments", "Confections", 
"Dairy Products", "Grains/Cereals", "Meat/Poultry", "Produce", "Seafood");
//]] >

Unfortunately, unlike registering script tags, the RegisterArray­Declaration doesn't stop you from adding duplicate items to the array.

Attaching Events Dynamically
Another use for server-side JavaScript generation is to pass server-side data as a parameter to the functions that need it by generating your function calls at runtime. This function, for instance, expects to be passed a tax rate that, like my array data, can't be found elsewhere on the page:

 <script type="text/javascript" >
  function CalculateTaxes(taxRate) {
    // Use taxRate
  }
 </script >

The following server-side code adds a call to the client-side CalculateTaxes function for the client-side click event of a button, using the button's OnClientClick property. This server-side code inserts the tax rate as the parameter to the function call:

Me.CalcTaxesButton.OnClientClick = "CalculateTaxes(" & taxRate & ")"

The resulting HTML will look something like this (assuming that the taxRate variable on the server is set to 13 percent):

 <input type="button" name="CalcTaxesButton" value="Calculate Taxes" 
  onclick="CalculateTaxes(0.13); ...

While it's easy to add code to the onclick event through the OnClientClick property, using other events is only slightly more complicated. A control's Attributes property lets you access any attribute on the associated HTML element by name, so you can use Attributes to access other events. This code, for instance, calls the CalculateTaxes method from a control's onblur event:

Me.TotalSalesTextBox.Attributes("onblur") = 
  "CalculateTaxes(" & taxRate & ")"

Accessing controls in a GridView to set their client-side events requires more finesse. A GridView that lists the order details for an order might have five columns: product name, price, discount, quantity and (in the fifth column) a button for calculating the taxes on the current row. As each row is built and the data is loaded into the row, the GridView fires the RowDataBound event. The e parameter in that event has a Row property that provides access to the row currently being created. This code would access the button in the fifth column, and set its OnClientClick event to call the CalculateTaxes function:

Protected Sub GridView1_RowDataBound(sender As Object, 
  e As System.Web.UI.WebControls.GridViewRowEventArgs) _
    Handles GridView1.RowDataBound
Dim but As Button

  If e.Row.RowIndex  >= 0 Then
    but = CType(e.Row.Cells(4).Controls(0), Button)
    but.OnClientClick = "CalculateTaxes(" & taxRate &  ")"
  End If
End Sub

But why stop there? The code in the CalculateTaxes function would now have to find the matching row and retrieve the price, discount and quantity textboxes from controls in the row. Why not just pass all values from the row to the CalculateTaxes function so that the function doesn't have to retrieve data from the GridView at all, as the code in Listing 1 does?

Generating Code
My last example generated code for each row on the GridView to meet the needs for that row. You can take the same approach to the page as a whole: On the server, why not generate exactly the client-side code that the page needs? Your best tool for doing that is Microsoft T4 technology.

Assume that goods the company has too many of ("Overstock") are given an automatic 10 percent discount. The code in the Calculate­Taxes function would have to look like this to handle that scenario:

function CalculateTaxes(taxRate, ProductID, quantity, price, discount) 
{
  if (type="Overstock")
  {
    discount = .10;
  }
  var extendedPrice = quantity * price * (1-discount);

But in a page devoted to overstock items, the discount will always be 10 percent. In that case, for the Overstock page, the code could be much simpler:

function CalculateTaxes(taxRate, ProductID, quantity, price) 
{
  var extendedPrice = quantity * price * .9;

Rather than write (and test) the more-complicated code, why not generate just the JavaScript code that's required? A T4 template to generate the two different versions of the code would look like Listing 2.

A template consists of control blocks (enclosed in <# # > delimiters) containing server-side code that controls the code-generation process. Between the control blocks is the text that will form the generated code. This example checks a StockType variable pulled from a T4 Session object (not the ASP.NET Session object) to decide what text to include in the generated code.

To create this T4 template, first select Project | Add New Item, and (in the Add New Item dialog) select the General category. In that category, select the Preprocessed Text Template and give it a name before clicking the Add button (I called mine CalcTaxes.tt). You'll get a couple of warnings -- just select the "Don't show this again option" and click the OK button. The Preprocessed Text Template added to your project consists of a .tt file that you put the template code in, and a code file (which you can safely ignore).

To generate the JavaScript code from the template at runtime, first instantiate your template's class (CalcTaxes, in my case), which will be in the My.Templates namespace in Visual Basic:

Dim jsT4 As New My.Templates.CalcTaxes

To pass the StockType value as a parameter to the TransformText method, use the Preprocessed template Session property. You first load the Session property with a Dictionary that uses a string to store object values. You can then add any parameters you need to the Session object, as this code does:

jsT4.Session = New Dictionary(Of String, Object)
jsT4.Session.Add("StockType", "Overstock")

Finally, you call the TransformText method of the class, which returns a string containing the generated code. The code to get the code from CalcTaxes looks like this:

Dim jsText As String = jsT4.TransformText()

Now you need to insert the generated JavaScript code into your page. The easiest way to do that is with the ClientScriptManager RegisterClientScriptBlock method. As with the other Register methods, this method accepts a key that allows you to check to see if the script block has already been added. Unlike the other Register methods, it's essential that you use the IsClientScriptBlockRegistered method to make sure that you don't add the same script block twice. That's what this code does:

If csm.IsClientScriptBlockRegistered("CalcTaxes") = False Then
  csm.RegisterClientScriptBlock(Page.GetType, 
    "CalcTaxes", jsText, True)
End If

The first parameter to the RegisterClientScriptBlock method specifies the kind of object that will be accessing the script (typically, the Page object); the second parameter is the key that identifies the block; the third parameter is the generated JavaScript code; and the final parameter causes the code to be wrapped in a script block. My page now has more focused code that does just what the page needs.

There's nothing wrong with static JavaScript. But when using user controls that depend on JavaScript libraries, or when trying to improve performance by avoiding server-side calls -- or if you're just trying to simplify your client-side code through server-side code generation -- ASP.NET gives you the tools to dynamically add the code you need to your page.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube