AJAX Abstraction
(May 2009 Issue)
October 30, 2009
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, itreally hasn t done anything exceptionally new that isn t already capable withinthe JavaScript language itself or in one of the many JavaScript libraries onthe market (like extJS, Prototype, jQuery, Rico, etc.). Concepts like classdevelopment and design patterns are already available in JavaScript (you canfind several books on the subject), and CSS class toggling, attaching eventhandlers, and rounding corners of tables (without the use of an extender) arealready available in other JavaScript libraries.
What the ASP.NET AJAX framework does provide is amore managed way to develop JavaScript components and expose structures thatlook more like the ASP.NET Framework API. What every JavaScript libraryattempts to offer is to provide more features using less code and this includesthe ASP.NET AJAX framework.
When developing any application, on the client oron the server, it is good practice to develop using an approach that createsreusable code, rather than embedding code into an application in a situationwhere it can t be reused. If the developer can t make use of that codeelsewhere, what good is the code (even if it is well designed)? This sameconcept works with JavaScript; it is better to create a reusable library thanembed the JavaScript code in the page markup.
Reusability is good, but abstraction of concrete conceptsprovides even greater functionality. What I mean by this statement is thatcreating components that work with a concrete implementation ofHTML/JavaScript, but work with these elements in an abstract way, provides agreater level of reuse. This is the core concept of this article.
Simplifying Table Modifications
Tables are simplistic HTML structures; it s so easyI m going to assume you know what a table looks like. While it s easy to definea table structure in HTML, it s a little more tedious to create one inJavaScript. An on-demand world requires on-the-fly, AJAX-enabled, client-sidetable generation, so that s what this component is all about.
But what do we need to do with tables beyond the standardHTML definition? Let s look at some possible scenarios:
A custom control or extender may dynamicallygenerate a table, or refresh a table based on external data.
A page may use a table, and want to be able touse this component, to tap in to an existing table structure and modify it insome way.
A page may extract information from a table byreading its rows, columns, or cells.
In all these scenarios, I don t want the developerto worry about some of these details; I want to spare them from having to writethe 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 inJavaScript; creating the table as a string and assigning it via the innerHTMLproperty of another HTML element is another viable option. However, thisarticle uses the DOM approach throughout. You probably noticed the code inFigure 1 wasn t that difficult to write. What I m attempting to do with thissimple example is create an abstract approach (not letting the user worry aboutthe table specifics) to provide a better set of features with JavaScript usingless code.
The following JavaScript component example is namedTableContentManager. This component can generate a new table structure on thefly, or it can create an instance of itself using an existing table, reading inthe table s information (expecting that that table fits the standard definitionthe TableContentManager generates).
TheTableContentManager is outlined in Figure 2. The approach for creating a newtable versus reading the existing table is done by using static methods, afactory 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
Forcreating a new table structure, I m using the DOM approach that is similar tocreating an XML document in .NET: create content using document.createElement,then append child content to parent content using the appendChild method. Thetable has a header, body, and footer appended to it, followed by all the rowsand cells that make up each row.
Inaddition, the table must be appended to the parent so it comes in view. Thesimplest way was to pass along the parent to the createNew static method. Yousaw 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 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 tableThis methoddoesn t create any actual data; rather, it creates the header structure andestablishes the collection of columns. This collection of columns is used toenable the consumer to reference the column by name instead of by index. Therows of data that are created later are validated against the columns, ensuringthat only the correct number of columns of data is created. For instance, iffive column names are passed in, a five-column structure, one column for eachof the column names, is generated. If one more or less is passed in, anexception is thrown.The body ofthe table is generated by two methods in the prototype, which is called afterthe TableContentManager class is generated. These methods, createNewRow andupdateRow, bind a single object to the table for a specified row, creating orupdating the row as necessary.Thesemethods work nicely because an object in JavaScript is noted by curly braces,{}, and each property/value of that property is noted in the name:valuenotation. Luckily, a similar approach also works with array scenarios, and thismethod has dual functionality. Take a look at the example of reading in asingle row of data shown in Figure 4.updateRow:function(index, values) { if (values == null || values == undefined) throwError.argumentNull("values"); var isArray = (Object.getType(values) ==typeof (Array)); if (isArray && (values.length !=this.get_columnCount())) throw Error.argument("The arraydoesn't have the correctnumber of values"); var body = this._getBodyElement(); var contentRow = null; //If -1 (new row indicator) or the bodydoesn'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 rowcollection else contentRow = body.rows[index]; for (var cellIndex = 0; cellIndex < this.get_columnCount(); cellIndex++) { //If the data source is an array, referenceby 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 tableIf thevalues passed in are an array, the object s type is an array, and an arraywould have to be referenced by an index value (0 - X, where X is the last indexof the column, in the notation values[0]). If an object consists of name:valuepairs, the object must reference properties using the list of columns passed inusing createNew. So an object would have to be referenced asobject["ColumnName"].In Figure4, updateRow does all the work because it s easier to create a method thatcreates/updates rows and cells in a table, rather than split the functionality.You may have seen this method reference other methods or properties; those areself-explanatory except for _attachToRowEvents. This method attaches to theclient-side events, using the delegate process ASP.NET AJAX created to listenfor the click, mouseover, or mouseout events of the table rows.The secondmajor 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 staticread method, which assumes the content is generated in the same format that theTableContentManager generates it (a header row that contains column names),with each body row representing a row of data.Whenreading the table, the body of the content isn t read into variables. Thereasoning for this approach comes from the idea that any data within the tableis dynamically referenced (instead of storing a static reference to all therow s values). Because the JavaScript component works with the table throughthe _table variable, it has all the information it needs to extract thisinformation later. Columns, however, are a different story, because the columnsare used to read/validate information in the table and shouldn t change. Checkout 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 = newNucleo.Web.TableContentManager(targetTable, columns); tableManager._attachToRowEvents(headerRow); return tableManager;}Figure 5: Reading an existing tableThis component provides the possibility for allsorts of helper methods. For instance, it s handy to have methods that extractinformation from the table. One idea is to allow users to extract informationby the name of the column, or by the column or row index. Additionally, it shandy to have methods that can read anentire row or entire column of data, rather than a single cell.To make useof the columns, ASP.NET AJAX added an indexOf method that finds a value out ofthe array. Because the columns are stored in a variable and exposed through thecolumns property (via get_columns), the column s index in this collection isused, 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(); returnbody.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 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 columnIndex++) { Array.add(rowValues, row.cells[columnIndex].innerHTML); }},Figure 6: Getting cell valuesSee how theindex of the column in the local collection matches the index of the column inthe table, and can be passed along to a different method to perform the actualwork? These methods make it handy to get information out of the table.To thispoint, what this article is trying to achieve is to make JavaScript codingeasier by abstracting the work (working with the TableContentManager instead ofa table HTML element) and by reducing the total number of lines of code adeveloper must write. In the future, new features easily can be added bycreating methods in the TableContentManager class.Let s seehow this abstraction benefits us by looking at a working sample. We re going touse the web service shown in Figure 7 to stream data to the client. By usingthe ScriptService attribute, this enables the web service to be used in an AJAXapplication.publicclass TestService : WebService{ [WebMethod] public object GetResultSet() { return new[] { new { ID = 1, Name = "Sports", Description = "Covers every kindof sport" }, new { ID = 2, Name = "Entertainment" Description = "DVD's, CD's, TV onDVD, Blue Ray, etc." } }; }}Figure 7: A web service that returns dataThe webservice in Figure 7 returns only a sampling of data; the actual data sourcereturns a little more sample data. As a side note on using the anonymousfeatures of .NET, the new anonymous types and anonymous collections featuresmake it easy to set up examples or return subsets of data by using thesefeatures to return a customized result. Because the web service can return thereference as an object, any type of anonymous object can be accommodated (aslong as the object can be serialized properly). A web page can use this webservice and display the results in an approach like that shown in Figure 8.
UseSubmitBehavior="false" OnClientClick="update();returnfalse;" Text="Refresh Table" /> 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 _tableManager.updateRow(index,results[index]); _tableManager.add_rowClick( TableContentManager_RowClick); updateStyles(); }Figure 8: Creating or updating a tableThe web servicereturns an array of objects in JSON format that can be used to generate atable. 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, passingthis reference along. Each row of the table gets refreshed with new data thatcomes from the web service; the old data gets overwritten with the new data.This process is triggered by a button click.One item tonote: when the page posts back to the server, dynamic content is not retainedusing 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 thatis beyond the scope of the article.Styling the TableIt seems every website in the world must rely onCSS, which is the best way to style website content to make it more appealingto the user. Tables fit within this category. Most developers use a set ofstyles for styling the table s header, footer, and content rows. TheTableContentManager publicizes a header style, item style, and footer style forthe time being. A common approach to styling content is to supply the name of aCSS class to apply to each row. You see CSS classes heavily used in the AJAXcontrol toolkit, while other toolkits (my unfinished Nucleo.NET toolkit and theAJAX Data Controls projects, available on CodePlex) use styles by convertingstyle content to text.The common approaches to exposing styles are viaproperties, with a getter and setter, or by passing them in to the staticfactory methods. When setting style-based content, the way I ve found worksbest across the recent versions of the major browsers is to set the cssTextproperty of the style with the CSS markup string. Setting the styles, whencreating the table dynamically, would look something like Figure 9.vartr = tbody.insertRow();tr.style.cssText= this.get_itemStyle();Figure 9: Assigning styles to a rowYou may have noticed the updateStyles method in thesample code in Figure 8. This method establishes the row-based styles andapplies them to the table. In the TableContentManager, styles are exposed viagetters and setters, which causes a problem; the table content is generatedbefore the getters and setters can be applied to the table. To remedy thisrequires the updateTableStyles method, which is called in updateStyles, asillustrated in Figure 10.//Definedin the test pagefunctionupdateStyles() { if (_tableManager != null) { _tableManager.set_headerStyle( "color:navy;background-color:gray; font-weight:bold;"); _tableManager.set_itemStyle("color:navy; background-color:lightyellow;"); _tableManager.updateTableStyles(); }}//Definedin TableContentManager classupdateTableStyles: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 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 methodAs a side note, setting certain styles at the rowlevel works; setting other styles may not work at the row level. I ve hadissues with certain CSS attributes at the row level, so if you try somethingyourself and it doesn t work, it may not be supported by the table row.Other Helpful MethodsDHTML provides hook-ins for JavaScriptdevelopers, in the sense that every DOM element exposes many events thatJavaScript developers can use. ASP.NET AJAX provides a delegate-based eventhandling system that is a two-step process for registering event handlers toclient events.Events work in a multi-step process. First, eventhandlers are registered using the Function.createDelegate method, the commonapproach to creating event handlers in ASP.NET AJAX. In addition, AJAXcomponents can expose their own events by adding three methods for the event: amethod 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 (andcan be called elsewhere or named differently, like with an _on prefix). Ihave 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, newNucleo.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 eventsThe events property (get_events) is a specialobject of type Sys.EventHandlerList that handles all events. All event handlersare stored in this object, and are called by calling the handler returned fromthe getHandler method. The signature these events expose is determined by theevent-raising method themselves, which is illustrated in the _processEventprivate method. Any event handler registered using the add method of that eventwill receive this event notification. So, the final process for events is thatthe table row fires the callback method, and the callback method fires theclass event, which then any event handlers get fired in the ASPX page or usercontrol.Normallythe callback wouldn t call a private method; rather, the callback from clickingthe row would call the RowClicked event by firing the event handlers registeredthrough the event property. In this case, though, there is some complexitywhich necessitates a common method. This complexity comes in the way of anevent argument.Theseevents have the signature of two parameters: the object raising the event(TableContentManager) and the event argument, similar to events in the .NETFramework. An event is raised by getting the handlers for that event andcalling them as a delegate. The delegate has some important information, suchas the row currently clicked, that row s index, whether the row is the headerrow or the footer row, and so on. All of this is stored in the custom eventargument, available with the sample source code (available for download; seeend of article for details).But first,it s important to understand that the DOM events fire for the row. These eventscall the _processEvent method. This method determines whether the currentobject is a row or a cell. If a cell, it s easy to get the row by accessing theparentNode property. The event handler gets a call with a customTableRowEventArgs object, which contains the current row (in reference to thebody; index zero is the header cell, which I want the index to be used for onlybody 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 examplein Figure 8 also includes one more method of note: an event handler referencefor the click event, named TableContentManager_RowClick. Check out the eventhandler in Figure 12.functionTableContentManager_RowClick(sender, e) { if (e.get_isHeader()) { var columnValues = _tableManager.getCellValues("Name"); var message = ""; for (var index = 0; index 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 clicksWhen the rowis clicked, the header property is used to enter into a special case. If thecurrent row click is being processed for the header row, the getCellValues iscalled, getting all the values for the current row (note I m not tracking thecell 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 thecurrent row is returned, using the body row s index and grabbing the namevalue.ConclusionYou don t necessarily need ASP.NET AJAX to use thiscomponent; this component can be defined for regular JavaScript or leveragedagainst other JavaScript frameworks. However, this code uses the ASP.NET AJAXframework concepts to create abstract solutions to common problems, and simplifiesthe 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 thiscomponent. Another key principle is encapsulation, hiding from the developer thework done against the table.It would be easy to add other important features tothis component. For instance, the component could create a style for mousingover and out of a row. It could allow for creating a column that has a rowselector, to allow the user to select rows. It also could add or remove columnsdynamically, or even rearrange the columns on the fly.This type of component offers many possibilities. Thisobject may not be the most useful to your work, but the concepts discussedherein should help you see how it applies to the applications you work on andthe specific client-side challenges you may face.Source code accompanying this article is availablefor download.Brian Mains ([email protected])is a Microsoft MVP and consultant with Computer Aid Inc., where he works withnon-profit and state government organizations.
About the Author
You May Also Like