Building Web Sites with ASP.NET - Part 2
 
Published: 14 Nov 2008
Abstract
In this second part of the series, Brian demonstrates how to add AJAX features for an application that already has been built using the server-based approach of posting back and processing data on the server. This article shifts the application to doing more work on the client. After providing a detailed overview of the topic, he examines the steps required to implement AJAX features with the help of a sample application which renders news content using the ListView control with the help of relevant source code.
by Brian Mains
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 22130/ 24

Introduction

ASP.NET AJAX has many helpful features that make developing ASP.NET applications asynchronous. Sometimes though, with the out-of-the-box controls in the ASP.NET framework, it is hard to tap into the existing control architecture to add custom JavaScript capabilities.  Oftentimes, creating your own custom control just to perform those actions on the client-side is what is needed. To understand how to add AJAX to an existing ASP.NET control requires a thorough understanding that the various paths server controls can take through the process. What I mean by that is some server controls render different HTML markups based upon the input settings. One instance is the Textbox control: in single line mode, it renders an <input type="text"> element, in multi line mode, it renders a <textarea> element, and in password mode it renders an <input type="password"> element.

In this article, I am focusing more on building additional features of an existing ASP.NET web site to incorporate some new AJAX features. The idea for these new features is to prevent as many postbacks as possible, and to create a richer user experience.

So why create a richer experience, and prevent posting back? Preventing post backs reduces the amount of binding that often takes place. A .NET application may often bind data to a GridView or ListView just to refresh the user interface, for the purposes of changing display modes, sorting, paging, or the like. All of the data gets rebound only to change modes or because of the possibility that some other user or process modified or added data to the system. This approach provides more of an instantaneous response than posting back to the server can do. In addition, it reduces the payload of pushing HTML to the client, which increases the payload being transferred across the wire.

The key to performing work on the client is to do two kinds of work: sending the data to the server, and manipulate the client. In a server-only approach without AJAX, the data that the user manipulates gets posted back along with the action that occurred, letting the underlying control know that an insert or update occurred and re-render its interface by binding again.

On the client, all of the work to create or refresh the user interface must occur on the client; simply binding again does not refresh the user interface. There are many other nuances to be aware of when shifting logic from the server to the client. So while the server approach is easy, and it is easy to use an UpdatePanel, it may often be better to use a client-side scripting approach to generating the user interface.

When shifting logic from the server to the client, be aware of some initial problems. First, any control marked as Visible="false" does not render in markup. The underlying HTML element rendered is not in the document object model, and thus cannot be manipulated and may cause client-side scripting errors. The better alternative would be to add null checks in JavaScript code, or set the style="display:none" property on the server control (which maps over to the client correctly for most controls).

Another complexity can come from understanding how the server control renders. I mentioned this before, but I will mention another example that can happen with the CheckBoxList control.  The list can render in vertical or horizontal modes, and can repeat depending on the settings of the RepeatLayout, RepeatDirection, and RepeatColumns properties. Depending on the RepeatLayout property, the underlying structure can render as a table or in flow mode. In flow mode, the actual text is not inside the checkbox tag, but rendered in a label element (not to be confused with the label server control).

Another issue to be aware of is dynamically generated ID's. Always provide an ID, because a null ID does not generate the id attribute on the client side, and this does not jive with Firefox.  Firefox needs the id to use document.getElementById, while Internet Explorer will use the name attribute as well.

With that said, let us begin to look at how we are going to improve upon the process.

Using the ListView Control

The module I am going to begin to develop is a news section on the site, where it lists the latest news for that site. The ListView control is a great control to use for this module for one simple reason: it gives you complete control over the user interface layout. Having control over the layout creates great flexibility in how the application works, and makes it really easy to append JavaScript code. With a GridView, while it is nice to have this control encapsulate the underlying UI because ASP.NET AJAX scripts need to know what to work with under the scenes, a ListView is much easier to work with.

Take a look at the structure of a ListView below. This list view renders both the read-only and the edit controls at the same time, but hides the textboxes.

Listing 1: News ListView Definition

<asp:ListView ID="lvwNews" runat="server" ItemPlaceholderID="plcNewsList"
      OnItemDataBound="lvwNews_ItemDataBound" InsertItemPosition="LastItem">
      <LayoutTemplate>
            <div id="NewsList">
                  <asp:PlaceHolder ID="plcNewsList" runat="server" />
            </div>
      </LayoutTemplate>
      <ItemTemplate>
            <asp:Label ID="lblNewsHeadline" runat="server" 
                  Text='<%# Eval("Headline") %>' CssClass="NewsTitle" />
            <asp:TextBox ID="txtNewsHeadline" runat="server" 
                  Text='<%# Eval("Headline") %>' style="display:none" />
            <br />
            
            <asp:Label ID="lblNewsDescription" runat="server" 
                  Text='<%# Eval("Description") %>' />
            <asp:TextBox ID="txtNewsDescription" runat="server" 
                  Text='<%# Eval("Description") %>' TextMode="MultiLine"
                  Rows="5" Columns="30" style="display:none" />
            <br />
            
            Posted at <asp:Label ID="lblNewsPostedDate" runat="server" 
                  Text='<%# Eval("CreatedDate") %>' />
            &nbsp;&nbsp;
            <asp:LinkButton ID="lnkEdit" runat="server"
                  OnClientClick="NewsEdit_Click();returnfalse;">Edit</asp:LinkButton>
      </ItemTemplate>
      <ItemSeparatorTemplate>
            <br />
            <hr />
            <br />
      </ItemSeparatorTemplate>
      <InsertItemTemplate>
            <span class="NewsTitle">Headline:</span>
            <asp:TextBox ID="txtNewsHeadline" runat="server" 
                  Text='<%# Eval("Headline") %>' />
            <br />
            
            <span class="NewsTitle">Description:</span>
            <asp:TextBox ID="txtNewsDescription" runat="server" 
                  Text='<%# Eval("Description") %>' TextMode="MultiLine"
                  Rows="5" Columns="30" />
            <br />
            
            <asp:LinkButton ID="lnkSave" runat="server" Text="Save"
                  OnClientClick="NewsInsert_Click();return false;" />
      </InsertItemTemplate>
</asp:ListView>

This approach means that the initial setup of the table renders on the server, and will be manipulated on the client using JavaScript (newer items and edits sent via web services, while the client updates the UI). Notice how the controls are laid out: the TextBox edit controls have their display set to none, rather than setting Visible="false." Visible set to false means the HTML element is not rendered in the browser, which is the wrong response. Also, notice how buttons do not post back; this is prevented by using the "return false;" statement in the OnClientClick. By default, the __doPostback method is called after the OnClientClick code. The return false statement prevents that code from being called.  I would also recommend setting the UseSubmitBehavior for any Button controls to false, so the button does not render as a submit button (which ignores your code and posts back directly).

There should be careful planning in whatever approach is taken. If the control is bound on the server, it means more content is passed over the wire to the client (possibly in the megabytes).  However, if the client renders the UI, then only the data is passed over the wire, and the markup is generated on the client. But this can be more complicated to setup.

However, whatever changes are made to the client are not persisted in ViewState. If using the ListView approach, the ListView has to be rebound on every page load if something changes; otherwise, it would not know about the client-side code additions because it was not in ViewState.  Using the client only approach, the select web service call is called every postback, unless some caching mechanism can be implemented.

Using the ListView route, the insert template and the edit link will only be visible if the user has permissions. Because the news module is in a user control, the user control exposes properties to turn this on or off, and thus the server controls these capabilities.

To insert a new record, use the following code:

Listing 2: Inserting a Row of Data

function NewsInsert_Click() {
      var body = $get("NewsInsertRow");
      var headline = description = null;
 
      for (var index = 0; index < body.childNodes.length; index++) {
            var control = body.childNodes[index];
 
            if (control.id != undefined && control.id != null 
                  && control.id.length > 0) {
                  if (control.id.endsWith("txtNewsHeadline"))
                        headline = control.value;
                  else if (control.id.endsWith("txtNewsDescription"))
                        description = control.value;
            }
      }
 
      var newsItem = {
            Headline: headline,
            Description: description,
            CreatedDate: new Date()
      };
 
      WebSiteStarterKit.Web.Services.NewsService.CreateNewsItem(newsItem,
            function(results, context, method) { createNewsRow(results); },
            function(results, context, method) { },
            body);
}

There is some work in extracting the row information, a challenge that I have been thinking about is how to make more efficient. The problem comes with server-side templates. A server-side template field is not available via a direct reference. What I mean is that any server control on a page can reference it in JavaScript through the following code:

Listing 3: Referencing a server control on the client

var label = $get("<%= lblLabel.ClientID %>");

At runtime, the client ID of the label is used. Client ID is important because when using a master page, the ID of the label could be: ctl100$ContentPlaceHolder$lblLabel, and thus, it is not good to hard-code this value.

The challenge comes with the ListView. The ListView control uses templates, meaning the reference to the control does not work because that control may be repeated numerous times. To reference the txtNewsHeadline control directly will not work because it resides in a template that may or may not be bound, and referencing this way does not work.

I have thought a little about how to get around this. There are a couple ways, which I will discuss later. One of those ways is the approach I used above. The JavaScript code loops through the insert form, looking for a control with the ID of the headline/description fields and extracting their values. In JavaScript, controls (or their underlying elements) can be accessed via the childNodes collection using the client-side HTML reference. But the childNodes collection also contains literals, and thus referencing an element via childNodes[0] may not contain the right result.

Furthermore, I do not like to change code, so my approach is flexible in the sense that I do not have to rewrite code if I change the layout of the page. For instance, if I insert a new header to make the insert section look better, I would not have to change code when this changes childNode collection indexes.

In the web service call at the end, the web service proxy looks something like this:

<method>(<parameters separated by commas>, <success callback>, <failed callback>,
<context>);

The callback methods defined in this instance are inline (functions do not have to be an explicit declaration, but can be passed inline as an anonymous delegate works in C#), but the name of the method could also be provided. This callback calls a method to create a new row in the user interface, using the createNewsRow method.

Listing 4: Creating a new Row

function createNewsRow(newsItem) {
      var body = $get("NewsList");
      var insertRow = $get("NewsInsertRow");
 
      var headlineLabel = document.createElement("SPAN");
      headlineLabel.innerHTML = newsItem.Headline;
      body.insertBefore(headlineLabel, insertRow);
 
      var headlineBox = document.createElement("INPUT");
      headlineBox.value = newsItem.Headline;
      headlineBox.style.display = "none";
      body.insertBefore(headlineBox, insertRow);
 
      body.insertBefore(document.createElement("BR"), insertRow);
 
      var descriptionLabel = document.createElement("SPAN");
      descriptionLabel.innerHTML = newsItem.Description;
      body.insertBefore(descriptionLabel, insertRow);
 
      var descriptionBox = document.createElement("TEXTAREA");
      descriptionBox.rows = 5;
      descriptionBox.cols = 30;
      descriptionBox.value = newsItem.Description;
      descriptionBox.style.display = "none";
      body.insertBefore(descriptionBox, insertRow);
 
      body.insertBefore(document.createElement("BR"), insertRow);
 
      var createdDateLabel = document.createElement("SPAN");
      createdDateLabel.innerHTML = "Posted at: " + 
            newsItem.CreatedDate.format("MM/dd/yyyy hh:mm:ss"+ "&nbsp;&nbsp;";
      body.insertBefore(createdDateLabel, insertRow);
 
      var saveButton = document.createElement("A");
      saveButton.innerHTML = "Edit";
      saveButton.href = "javascript:NewsEdit_Click();return false;";
      body.insertBefore(saveButton, insertRow);
 
      body.insertBefore(document.createElement("BR"), insertRow);
}

This method does the work of appending a new row to the user interface, working asynchronously to add the new entry to the database and to the user interface. This method uses the Document Object Model (DOM) approach to creating the user interface.  It is a top down approach that creates the ion, and then the user interface elements.

So what happens on the backend? A web service is what gets called out of all of this, and the web service writes the data to an XML file that resides in the web project, shown in Listing 5.

Listing 5: Creating a news item via web service

[
WebService(Namespace = "http://tempuri.org/"),
WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1),
System.ComponentModel.ToolboxItem(false),
ScriptService
]
public class NewsService : WebService
{
      #region " Methods "
 
      [WebMethod]
      public NewsItem CreateNewsItem(object newsItem)
      {
            Dictionary<stringobject> values = newsItem 
                  as Dictionary<stringobject>;
            if (values == null)
                  throw new Exception(
                        "Cannot convert newsItem parameter to a collection");
 
            NewsItem item = new NewsItem
                              {
                                    Headline = (string)values["Headline"],
                                    Description = (string)values["Description"],
                                    CreatedDate = DateTime.Now
                              };
 
            XmlDocument document = new XmlDocument();
            document.Load(Server.MapPath("~/App_Data/News.xml"));
 
            XmlElement element = document.CreateElement("Story");
            document.DocumentElement.AppendChild(element);
            element.AppendChild(document.CreateElement("Headline"));
            element.AppendChild(document.CreateElement("Description"));
            element.AppendChild(document.CreateElement("CreatedDate"));
 
            element["Headline"].InnerText = item.Headline;
            element["Description"].InnerText = item.Description;
            element["CreatedDate"].InnerText = item.CreatedDate.ToString();
            document.Save(Server.MapPath("~/App_Data/News.xml"));
 
            return item;
      }
 
      #endregion
}

What gets passed up to the web service is a dictionary of items (this is because what I passed up was in a name/value pair collection, a typical class setup in JavaScript). This means that the values { Headline : "Some Headline", "Description" : "Some Description" } gets converted to a dictionary with a string key and an object value. This is the typical approach; actually, if you use a JavaScriptConverter object, you will see this approach widely used.

The reason for converting objects to string/object is because all objects have to be serialized when passing them from web service to client, or vice versa. Object references cannot be passed in directly; rather, they are broken down into their primitive or serializable parts. This is the same concept that happens when writing data to a database or serializing an object to XML (an approach some Object Databases use). So the web service simply has to read and write data from XML in order to store the data in this solution, but the solution could easily use a database (I typically have used a database to do this).

Conclusion

The page renders the news content via a ListView. The ListView renders the entire UI structure, including a form to insert new data. Rather than posting back, the form calls a web service that snags the data from the form and passes it to a web service call. Upon succeeding, the web service callback creates a new row in the UI. All of this happens seamlessly to the user, but there are some pitfalls in this process to be aware of, which is what this article tries to illuminate.

This is a somewhat conceptual article, because if you have in your possession any third-party controls, these controls already perform some of these features. Furthermore, some advances in the ASP.NET AJAX framework will make this process easier, namely a new GridView like control that is client-based and client-side templating support.



User Comments

Title: פיתוח אתרים   
Name: moshiko bracha
Date: 2011-01-16 12:08:23 PM
Comment:
asp .net is the best way to build today... nice post tnx for the info
Title: Mr.   
Name: dengone
Date: 2009-12-19 8:35:10 PM
Comment:
I think I have got what you write.
Thanks a lot.
Title: Executive Director (Commercial & Industrial Business System Development (Head Office)   
Name: Thomas Woon Wei Leou
Date: 2008-11-25 3:52:12 PM
Comment:
I am presently undertaking my model (BSc (Honours in Computer Science & Management); Module of :- Intelligent Decision Making & Selection in tendon with an Advance Intelligent Making using Selective Decisional Process.

I think there would be a great & wider audience in this present field of study & interest. A simple application would be a Webbot (A Realistic Human Clone; with Full Emotions & Senstivity interecting with the Environment (Real World in Applcation Real Life Time Situation)

If there is some interest, I am very much happy to contribute Ideals for Projects of Future System, Science & Technology an Beyond the Real Matter World, i.e. Visions & Dreams & Our Final Destiny.) Regards.






Community Advice: ASP | SQL | XML | Regular Expressions | Windows


©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-04-19 5:37:29 AM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search