Skip navigation

AJAX Abstraction

<br />(May 2009 Issue)

CoverStory

LANGUAGES: C# | VB

ASP.NET VERSIONS: 3.5

AJAX Abstraction

Abstraction of Concrete Concepts Improves Functionality

By Brian Mains

Since the ASP.NET AJAX framework came out, it really hasn t done anything exceptionally new that isn t already capable within the JavaScript language itself or in one of the many JavaScript libraries on the market (like extJS, Prototype, jQuery, Rico, etc.). Concepts like class development and design patterns are already available in JavaScript (you can find several books on the subject), and CSS class toggling, attaching event handlers, and rounding corners of tables (without the use of an extender) are already available in other JavaScript libraries.

What the ASP.NET AJAX framework does provide is a more managed way to develop JavaScript components and expose structures that look more like the ASP.NET Framework API. What every JavaScript library attempts to offer is to provide more features using less code and this includes the ASP.NET AJAX framework.

When developing any application, on the client or on the server, it is good practice to develop using an approach that creates reusable code, rather than embedding code into an application in a situation where it can t be reused. If the developer can t make use of that code elsewhere, what good is the code (even if it is well designed)? This same concept works with JavaScript; it is better to create a reusable library than embed the JavaScript code in the page markup.

Reusability is good, but abstraction of concrete concepts provides even greater functionality. What I mean by this statement is that creating components that work with a concrete implementation of HTML/JavaScript, but work with these elements in an abstract way, provides a greater level of reuse. This is the core concept of this article.

Simplifying Table Modifications

Tables are simplistic HTML structures; it s so easy I m going to assume you know what a table looks like. While it s easy to define a table structure in HTML, it s a little more tedious to create one in JavaScript. An on-demand world requires on-the-fly, AJAX-enabled, client-side table generation, so that s what this component is all about.

But what do we need to do with tables beyond the standard HTML definition? Let s look at some possible scenarios:

         A custom control or extender may dynamically generate a table, or refresh a table based on external data.

         A page may use a table, and want to be able to use this component, to tap in to an existing table structure and modify it in some way.

         A page may extract information from a table by reading its rows, columns, or cells.

In all these scenarios, I don t want the developer to worry about some of these details; I want to spare them from having to write the type of code you see in Figure 1.

var table = document.createElement("TABLE");

var thead = document.createElement("THEAD");

table.appendChild(thead);

var tbody = document.createElement("TBODY");

table.appendChild(tbody);

var tfoot = document.createElement("TFOOT");

table.appendChild(tfoot);

var headerRow = thead.insertRow();

var headerCell = headerRow.insertCell();

headerCell.innerHTML = "Name";

headerCell = headerRow.insertCell();

headerCell.innerHTML = "Address";

var contentRow = tbody.insertRow();

var contentCell = contentRow.insertCell();

contentCell.innerHTML = "Brian";

contentCell = contentRow.insertCell();

contentCell.innerHTML = "Some Address";

Figure 1: Defining a table programmatically

This is the DOM approach to creating a table in JavaScript; creating the table as a string and assigning it via the innerHTML property of another HTML element is another viable option. However, this article uses the DOM approach throughout. You probably noticed the code in Figure 1 wasn t that difficult to write. What I m attempting to do with this simple example is create an abstract approach (not letting the user worry about the table specifics) to provide a better set of features with JavaScript using less code.

The following JavaScript component example is named TableContentManager. This component can generate a new table structure on the fly, or it can create an instance of itself using an existing table, reading in the table s information (expecting that that table fits the standard definition the TableContentManager generates).

The TableContentManager is outlined in Figure 2. The approach for creating a new table versus reading the existing table is done by using static methods, a factory method design pattern approach.

Nucleo.Web.TableContentManager = function(table,

columns) {

Function._validateParams(arguments, [

{ name: "table", type: Object, mayBeNull: false,

optional: false },

{ name: "columns", type: Array, mayBeNull: false,

optional: false }

]);

this._table = table;

this._columns = columns;

this._events = new Sys.EventHandlerList();

}

Nucleo.Web.TableContentManager.registerClass(

"Nucleo.Web.TableContentManager", Sys.Component);

Nucleo.Web.TableContentManager.createNew =

function(targetParent, columns) {

}

Nucleo.Web.TableContentManager.read =

function(targetTable) {

}

Figure 2: TableContentManager s core definition

For creating a new table structure, I m using the DOM approach that is similar to creating an XML document in .NET: create content using document.createElement, then append child content to parent content using the appendChild method. The table has a header, body, and footer appended to it, followed by all the rows and cells that make up each row.

In addition, the table must be appended to the parent so it comes in view. The simplest way was to pass along the parent to the createNew static method. You saw the same code in Figure 1, which is now reusable (see Figure 3).

Nucleo.Web.TableContentManager.createNew =

function(targetParent, columns) {

var table = document.createElement("TABLE");

var thead = document.createElement("THEAD");

table.appendChild(thead);

table.appendChild(document.createElement("TBODY"));

table.appendChild(document.createElement("TFOOT"));

var headerRow = thead.insertRow();

for (var columnIndex = 0; columnIndex < columns.length;

columnIndex++) {

var headerCell = headerRow.insertCell();

headerCell.innerHTML = columns[columnIndex];

}

targetParent.appendChild(table);

var tableManager =

new Nucleo.Web.TableContentManager(table, columns);

tableManager._attachToRowEvents(headerRow);

return tableManager;

}

Figure 3: Creating a new table

This method doesn t create any actual data; rather, it creates the header structure and establishes the collection of columns. This collection of columns is used to enable the consumer to reference the column by name instead of by index. The rows of data that are created later are validated against the columns, ensuring that only the correct number of columns of data is created. For instance, if five column names are passed in, a five-column structure, one column for each of the column names, is generated. If one more or less is passed in, an exception is thrown.

The body of the table is generated by two methods in the prototype, which is called after the TableContentManager class is generated. These methods, createNewRow and updateRow, bind a single object to the table for a specified row, creating or updating the row as necessary.

These methods work nicely because an object in JavaScript is noted by curly braces, {}, and each property/value of that property is noted in the name:value notation. Luckily, a similar approach also works with array scenarios, and this method has dual functionality. Take a look at the example of reading in a single row of data shown in Figure 4.

updateRow: function(index, values) {

if (values == null || values == undefined)

throw Error.argumentNull("values");

var isArray = (Object.getType(values) == typeof (Array));

if (isArray && (values.length != this.get_columnCount()))

throw Error.argument("The array doesn't have the

correct number of values");

var body = this._getBodyElement();

var contentRow = null;

//If -1 (new row indicator) or the body doesn't have

//the total number of rows for the index, create

//a new row and attach to it

if (index == -1 || body.rows.length <= index) {

contentRow = body.insertRow();

this._attachToRowEvents(contentRow);

}

//ensure index isn't out of bounds of row collection

else

contentRow = body.rows[index];

for (var cellIndex = 0; cellIndex <

this.get_columnCount(); cellIndex++) {

//If the data source is an array, reference by index

//(ie values[0])

//If an object, reference by column name

//(ie values["Name"])

var value = isArray ? values[cellIndex] : values[

this.get_columns()[cellIndex]];

var contentCell = null;

//Create or get the existing cell

if (contentRow.cells.length <= cellIndex)

contentCell = contentRow.insertCell();

else

contentCell = contentRow.cells[cellIndex];

if (value != null)

contentCell.innerHTML = value.toString();

else

contentCell.innerHTML = "";

}

},

Figure 4: Loading a single row of data into the table

If the values passed in are an array, the object s type is an array, and an array would have to be referenced by an index value (0 - X, where X is the last index of the column, in the notation values[0]). If an object consists of name:value pairs, the object must reference properties using the list of columns passed in using createNew. So an object would have to be referenced as object["ColumnName"].

In Figure 4, updateRow does all the work because it s easier to create a method that creates/updates rows and cells in a table, rather than split the functionality. You may have seen this method reference other methods or properties; those are self-explanatory except for _attachToRowEvents. This method attaches to the client-side events, using the delegate process ASP.NET AJAX created to listen for the click, mouseover, or mouseout events of the table rows.

The second major function of this component, outside of dynamically creating a new table, is the process of reading a table. Reading the table is done through the static read method, which assumes the content is generated in the same format that the TableContentManager generates it (a header row that contains column names), with each body row representing a row of data.

When reading the table, the body of the content isn t read into variables. The reasoning for this approach comes from the idea that any data within the table is dynamically referenced (instead of storing a static reference to all the row s values). Because the JavaScript component works with the table through the _table variable, it has all the information it needs to extract this information later. Columns, however, are a different story, because the columns are used to read/validate information in the table and shouldn t change. Check out the process for reading from a table, as shown in Figure 5.

Nucleo.Web.TableContentManager.read =

function(targetTable) {

var columns = [];

var headerRow =

targetTable.getElementsByTagName("THEAD")

[0].rows[0];

for (var headerCellIndex = 0; headerCellIndex <

headerRow.cells.length; headerCellIndex++)

Array.add(columns,

headerRow.cells[headerCellIndex].innerHTML);

var tableManager =

new Nucleo.Web.TableContentManager(targetTable,

columns);

tableManager._attachToRowEvents(headerRow);

return tableManager;

}

Figure 5: Reading an existing table

This component provides the possibility for all sorts of helper methods. For instance, it s handy to have methods that extract information from the table. One idea is to allow users to extract information by the name of the column, or by the column or row index. Additionally, it s handy to have methods that can read an entire row or entire column of data, rather than a single cell.

To make use of the columns, ASP.NET AJAX added an indexOf method that finds a value out of the array. Because the columns are stored in a variable and exposed through the columns property (via get_columns), the column s index in this collection is used, as shown in getCellValue and getCellValues in Figure 6.

getCellValue: function(rowIndex, columnName) {

var columnIndex = Array.indexOf(this.get_columns(),

columnName);

return this.getCellValueAt(rowIndex, columnIndex);

},

getCellValueAt: function(rowIndex, columnIndex) {

var body = this._getBodyElement();

return body.rows[rowIndex].cells[columnIndex].innerHTML;

},

getCellValues: function(columnName) {

var columnIndex = Array.indexOf(this.get_columns(),

columnName);

return this.getCellValuesAt(columnIndex);

},

getCellValuesAt: function(columnIndex) {

var body = this._getBodyElement();

var columnValues = [];

for (var rowIndex = 0; rowIndex < body.rows.length;

rowIndex++)

Array.add(columnValues,

body.rows[rowIndex].cells[columnIndex].innerHTML);

return columnValues;

},

getRowValues: function(rowIndex) {

var body = this._getBodyElement();

var row = body.rows[rowIndex];

var rowValues = [];

for (var columnIndex = 0; columnIndex < row.cells.length;

columnIndex++) {

Array.add(rowValues,

row.cells[columnIndex].innerHTML);

}

},

Figure 6: Getting cell values

See how the index of the column in the local collection matches the index of the column in the table, and can be passed along to a different method to perform the actual work? These methods make it handy to get information out of the table.

To this point, what this article is trying to achieve is to make JavaScript coding easier by abstracting the work (working with the TableContentManager instead of a table HTML element) and by reducing the total number of lines of code a developer must write. In the future, new features easily can be added by creating methods in the TableContentManager class.

Let s see how this abstraction benefits us by looking at a working sample. We re going to use the web service shown in Figure 7 to stream data to the client. By using the ScriptService attribute, this enables the web service to be used in an AJAX application.

public class TestService : WebService

{

[WebMethod]

public object GetResultSet()

{

return new[]

{

new

{

ID = 1,

Name = "Sports",

Description = "Covers every kind of sport"

},

new

{

ID = 2,

Name = "Entertainment"

Description = "DVD's, CD's, TV on DVD,

Blue Ray, etc."

}

};

}

}

Figure 7: A web service that returns data

The web service in Figure 7 returns only a sampling of data; the actual data source returns a little more sample data. As a side note on using the anonymous features of .NET, the new anonymous types and anonymous collections features make it easy to set up examples or return subsets of data by using these features to return a customized result. Because the web service can return the reference as an object, any type of anonymous object can be accommodated (as long as the object can be serialized properly). A web page can use this web service and display the results in an approach like that shown in Figure 8.

<div id="tableOutputDisplay"></div>

<asp:Button ID="btnRefresh" runat="server"

UseSubmitBehavior="false"

OnClientClick="update();return false;"

Text="Refresh Table" />

<script language="javascript" type="text/javascript">

var _tableManager = null;

function pageLoad() {

update();

}

function update() {

TestService.GetResultSet(TestServiceSucceeded,

TestServiceFailed, $get("tableOutputDisplay"));

}

function TestServiceFailed(results, context, method) {

alert("FAILED");

}

function TestServiceSucceeded(results,

context, method) {

if (context.childNodes.length == 0)

_tableManager =

Nucleo.Web.TableContentManager.createNew(

context, ["ID", "Name", "Description"]);

else

_tableManager =

Nucleo.Web.TableContentManager.read(

context.childNodes[0]);

for (var index = 0; index < results.length; index++)

_tableManager.updateRow(index, results[index]);

_tableManager.add_rowClick(

TableContentManager_RowClick);

updateStyles();

}

</script>

Figure 8: Creating or updating a table

The web service returns an array of objects in JSON format that can be used to generate a table. If a table has not yet been created, the createNew method is called. Otherwise, the read method reads the table attached to the DIV parent, passing this reference along. Each row of the table gets refreshed with new data that comes from the web service; the old data gets overwritten with the new data. This process is triggered by a button click.

One item to note: when the page posts back to the server, dynamic content is not retained using viewstate. The page must rerender client-side content on every page load, which can be taxing on the server. There are ways to circumvent this, but that is beyond the scope of the article.

Styling the Table

It seems every website in the world must rely on CSS, which is the best way to style website content to make it more appealing to the user. Tables fit within this category. Most developers use a set of styles for styling the table s header, footer, and content rows. The TableContentManager publicizes a header style, item style, and footer style for the time being. A common approach to styling content is to supply the name of a CSS class to apply to each row. You see CSS classes heavily used in the AJAX control toolkit, while other toolkits (my unfinished Nucleo.NET toolkit and the AJAX Data Controls projects, available on CodePlex) use styles by converting style content to text.

The common approaches to exposing styles are via properties, with a getter and setter, or by passing them in to the static factory methods. When setting style-based content, the way I ve found works best across the recent versions of the major browsers is to set the cssText property of the style with the CSS markup string. Setting the styles, when creating the table dynamically, would look something like Figure 9.

var tr = tbody.insertRow();

tr.style.cssText = this.get_itemStyle();

Figure 9: Assigning styles to a row

You may have noticed the updateStyles method in the sample code in Figure 8. This method establishes the row-based styles and applies them to the table. In the TableContentManager, styles are exposed via getters and setters, which causes a problem; the table content is generated before the getters and setters can be applied to the table. To remedy this requires the updateTableStyles method, which is called in updateStyles, as illustrated in Figure 10.

//Defined in the test page

function updateStyles() {

if (_tableManager != null) {

_tableManager.set_headerStyle(

"color:navy;background-color:gray;

font-weight:bold;");

_tableManager.set_itemStyle("color:navy;

background-color:lightyellow;");

_tableManager.updateTableStyles();

}

}

//Defined in TableContentManager class

updateTableStyles: function() {

if (this.get_headerStyle() != null &&

this.get_headerStyle().length > 0) {

var header = this.get_table().getElementsByTagName(

"THEAD")[0];

if (header != null && header.rows.length > 0)

header.rows[0].style.cssText = this.get_headerStyle();

}

if (this.get_itemStyle() != null &&

this.get_itemStyle().length > 0) {

var body = this._getBodyElement();

for (var rowIndex = 0; rowIndex < body.rows.length;

rowIndex++)

body.rows[rowIndex].style.cssText =

this.get_itemStyle();

}

if (this.get_footerStyle() != null &&

this.get_footerStyle().length > 0) {

var footer = this.get_table().getElementsByTagName(

"TFOOT")[0];

if (footer != null && footer.rows.length > 0)

footer.rows[0].style.cssText = this.get_footerStyle();

}

}

Figure 10: The updateTableStyles method

As a side note, setting certain styles at the row level works; setting other styles may not work at the row level. I ve had issues with certain CSS attributes at the row level, so if you try something yourself and it doesn t work, it may not be supported by the table row.

Other Helpful Methods

DHTML provides hook-ins for JavaScript developers, in the sense that every DOM element exposes many events that JavaScript developers can use. ASP.NET AJAX provides a delegate-based event handling system that is a two-step process for registering event handlers to client events.

Events work in a multi-step process. First, event handlers are registered using the Function.createDelegate method, the common approach to creating event handlers in ASP.NET AJAX. In addition, AJAX components can expose their own events by adding three methods for the event: a method each to add, remove, and raise the event handler. The add_ and remove_ prefixes are necessary, but the method to raise the event isn t required (and can be called elsewhere or named differently, like with an _on prefix). I have opted to omit the raise_ method, but have used it in the past. To sum up, take a look at Figure 11.

add_rowClick: function(handler) {

this.get_events().addHandler("click", handler); },

remove_rowClick: function(handler) {

this.get_events().removeHandler("click", handler); },

add_rowMouseOver: function(handler) {

this.get_events().addHandler("mouseover", handler); },

remove_rowMouseOver: function(handler) {

this.get_events().removeHandler("mouseover", handler); },

add_rowMouseOut: function(handler) {

this.get_events().addHandler("mouseout", handler); },

remove_rowMouseOut: function(handler) {

this.get_events().removeHandler("mouseout", handler); },

_processEvent: function(domEvent, eventName) {

var handler = this.get_events().getHandler(eventName);

if (handler) {

var row = domEvent.target;

if (domEvent.target.tagName == "TD" |

| domEvent.target.tagName == "TH")

row = domEvent.target.parentNode;

handler(this, new Nucleo.Web.TableRowEventArgs(

row, row.rowIndex - 1,

(row.parentNode.tagName == "THEAD"),

(row.parentNode.tagName == "TFOOT")

));

}

},

_rowClickCallback: function(domEvent) {

this._processEvent(domEvent, "click");

},

_rowMouseOverCallback: function(domEvent) {

this._processEvent(domEvent, "mouseover");

},

_rowMouseOutCallback: function(domEvent) {

this._processEvent(domEvent, "mouseout");

},

Figure 11: Client-side events

The events property (get_events) is a special object of type Sys.EventHandlerList that handles all events. All event handlers are stored in this object, and are called by calling the handler returned from the getHandler method. The signature these events expose is determined by the event-raising method themselves, which is illustrated in the _processEvent private method. Any event handler registered using the add method of that event will receive this event notification. So, the final process for events is that the table row fires the callback method, and the callback method fires the class event, which then any event handlers get fired in the ASPX page or user control.

Normally the callback wouldn t call a private method; rather, the callback from clicking the row would call the RowClicked event by firing the event handlers registered through the event property. In this case, though, there is some complexity which necessitates a common method. This complexity comes in the way of an event argument.

These events have the signature of two parameters: the object raising the event (TableContentManager) and the event argument, similar to events in the .NET Framework. An event is raised by getting the handlers for that event and calling them as a delegate. The delegate has some important information, such as the row currently clicked, that row s index, whether the row is the header row or the footer row, and so on. All of this is stored in the custom event argument, available with the sample source code (available for download; see end of article for details).

But first, it s important to understand that the DOM events fire for the row. These events call the _processEvent method. This method determines whether the current object is a row or a cell. If a cell, it s easy to get the row by accessing the parentNode property. The event handler gets a call with a custom TableRowEventArgs object, which contains the current row (in reference to the body; index zero is the header cell, which I want the index to be used for only body rows), and whether the current row is a child of a header or footer tag (by checking the parentNode property and looking at the tag name).

The example in Figure 8 also includes one more method of note: an event handler reference for the click event, named TableContentManager_RowClick. Check out the event handler in Figure 12.

function TableContentManager_RowClick(sender, e) {

if (e.get_isHeader()) {

var columnValues =

_tableManager.getCellValues("Name");

var message = "";

for (var index = 0; index < columnValues.length;

index++) {

if (index > 0)

message += ", ";

message += columnValues[index];

}

alert(message);

}

else if (e.get_isFooter()) {

}

else {

var cellValue =

_tableManager.getCellValue(e.get_index(), "Name");

alert(cellValue);

}

}

Figure 12: Handling row clicks

When the row is clicked, the header property is used to enter into a special case. If the current row click is being processed for the header row, the getCellValues is called, getting all the values for the current row (note I m not tracking the cell that was clicked, so currently I m grabbing all the values for the Name). However, if a body row s element is clicked, only the Name column value for the current row is returned, using the body row s index and grabbing the name value.

Conclusion

You don t necessarily need ASP.NET AJAX to use this component; this component can be defined for regular JavaScript or leveraged against other JavaScript frameworks. However, this code uses the ASP.NET AJAX framework concepts to create abstract solutions to common problems, and simplifies the work that must be done to perform these common tasks.

One of the keys to abstraction is simplification, which is what this article illustrated by wrapping common functions into this component. Another key principle is encapsulation, hiding from the developer the work done against the table.

It would be easy to add other important features to this component. For instance, the component could create a style for mousing over and out of a row. It could allow for creating a column that has a row selector, to allow the user to select rows. It also could add or remove columns dynamically, or even rearrange the columns on the fly.

This type of component offers many possibilities. This object may not be the most useful to your work, but the concepts discussed herein should help you see how it applies to the applications you work on and the specific client-side challenges you may face.

Source code accompanying this article is available for download.

Brian Mains ([email protected]) is a Microsoft MVP and consultant with Computer Aid Inc., where he works with non-profit and state government organizations.

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish