.NET Blog

Tony Cavaliere

 
My Favourite Albums
  And the Grappa wins.
E-mail me Send mail
Add to Technorati Favorites AddThis Feed Button

Subscribe to Cynot Why Not


Recent posts

Disclaimer

Hey unlike other bloggers I stand by what I say but just in case. The opinions expressed herein are my own except on Tuesday when the second card is not turned up otherwise it ain't worth squat.

© Copyright 2010

Postback, UpdatePanel or JSON which one?

Microsoft introduced AJAX as an add on to .NET 2.0. Today, AJAX is now included as part of the core release of .NET 3.5. With this technology it is significantly easier for developers to implement Web 2.0 type applications. Rather than posting the entire form and receiving the entire page, the browser can send a partial request and receive a partial response. This is known as partial rendering. In some cases, all that is required is adding a ScriptManager and an UpdatePanel control and you are done. If you wish to reduce the payload even further you can use JavaScript Object Notation (JSON) and reduce the request and response footprint even further. In this post, I will investigate each of these technics, that is, the postback, UpdatePanel and JSON and hopefully give some insight on when each is appropriate.

Prerequisites

  1. Visual Studio 2008. Visual Web Developer Express 2008 should be fine. This post will be using VS2008.
  2. SQL Server with the Northwind database. The express ver1sion should be fine.

Initial Setup

Start Visual Studio 2008 and create a new web site. Delete the default.aspx web form that was added. When done your solution should be similar to the following:

Figure 1: Initial Solution

F1 Initial Solution 

Next add a LINQ To SQL class to the web site. But before you do this make sure you have a data connection to the Northwind database. Right click on the web site and select Add New Item. Select LINQ To SQL Class and name it Northwind.dbml. The Add New Item dialog is shown in Figure 2.

Figure 2: Add LINQ To SQL Class

F2 Add LINQ To SQL Class

A dialog will appear asking whether you would like to place the LINQ To SQL class into the App_Code folder, answer yes. For this scenario, we will be constructing a query that returns all the products for a customer using postal code to identify the customer. This requires that we add the Customers, Orders, Order Details and Products table to the designer surface. In the Server Explorer, navigate to the Northwind connection (if it doesn't already exist then add the connection) and expand the tables node. Select each table and drag them to the designer surface. Once completed the designer window should appear similar to the following:

Figure 3: LINQ To SQL Object Model

F3 LINQ To SQL Object Model

Behind the scenes LINQ To SQL is generating object classes that mirrors the tables that were added to the designer surface. To view the source for these classes use the Class View window. There you should see the Customer, Order, Order_Detail and Product classes. You may need to save the dbml file before these classes become visible in the Class View. One last point the dbml file is an XML file that describes schema for tables. This XML file is used to generate the object classes.

The next step is to add the code that will be responsible for retrieving products by postal code. Here we will use a LINQ query and the query will return a ProductsByPostalCode object that is serializable. This serialization will be important when we implement code that uses JSON. Right click the APP_Code folder and select the Add New Item menu item. Next select the class icon and choose Products.vb as the name for the file. Finally, add the code in Listing 1 to the Products class.

Listing 1: Code to Retrieve Products by Postal Code.

Imports System

Imports System.ServiceModel

Imports System.Runtime.Serialization

 

Public Class Products

 

    Public Function GetByPostalCode(ByVal pc As String) As List(Of ProductsByPostalCode)

 

        Dim db As New NorthwindDataContext()

        Dim products = From c In db.Customers _

                       Join o In db.Orders On o.CustomerID Equals c.CustomerID _

                       Join d In db.Order_Details On d.OrderID Equals o.OrderID _

                       Join p In db.Products On d.ProductID Equals p.ProductID _

                       Where c.PostalCode.Equals(pc) _

                       Select New ProductsByPostalCode With { _

                            .CompanyName = c.CompanyName, _

                            .Quantity = d.Quantity, _

                            .ProductName = p.ProductName, _

                            .PostalCode = c.PostalCode, _

                            .Phone = c.Phone _

                        }

        Return products.ToList

 

    End Function

 

End Class

 

<DataContract()> _

Public Class ProductsByPostalCode

 

    Private _CompanyName As String

    <DataMember()> _

    Public Property CompanyName() As String

        Get

            Return _CompanyName

        End Get

        Set(ByVal value As String)

            _CompanyName = value

        End Set

    End Property

 

    Private _ProductName As String

    <DataMember()> _

    Public Property ProductName() As String

        Get

            Return _ProductName

        End Get

        Set(ByVal value As String)

            _ProductName = value

        End Set

    End Property

 

    Private _Quantity As Short

    <DataMember()> _

    Public Property Quantity() As Short

        Get

            Return _Quantity

        End Get

        Set(ByVal value As Short)

            _Quantity = value

        End Set

    End Property

 

    Private _PostalCode As String

    <DataMember()> _

    Public Property PostalCode() As String

        Get

            Return _PostalCode

        End Get

        Set(ByVal value As String)

            _PostalCode = value

        End Set

    End Property

 

    Private _Phone As String

    <DataMember()> _

    Public Property Phone() As String

        Get

            Return _Phone

        End Get

        Set(ByVal value As String)

            _Phone = value

        End Set

    End Property

 

End Class

The method GetProductsByPostalCode creates a LINQ query that joins the Customers, Orders, Order_Details and Products objects and selects the CompanyName, ProductName, Quantity, PostalCode and Phone for the specified postal code. This data is returned in a custom object, ProductsByPostalCode. Using an anonymous type would not have been approp1riate here as serializing anonymous types is not easily done (not even sure if it is at all possible). The PoductsByPostalCode class is attributed with <DataContact> and the only method GetByPostalCode is attributed by <DataMember>. These attributes specify that the type defines or implements a data contract and is serializable. The <Serializable> attribute could have been used but <Data Contract> is the preferred WCF way.

That's it, the data access layer is complete.

 

Postback

Implementing the postback model is fairly straight forward. Add a web form to the web site and call the web form, Postback.aspx. Next add a TextBox, Button and GridView. Afterwards the markup should look like Listing 2.

List 2: Postback.aspx markup code. 

<html xmlns="http://www.w3.org/1999/xhtml">

<head runat="server">

    <title>Untitled Page</title>

</head>

<body>

    <form id="form1" runat="server">

    <div>

      <asp:TextBox ID="txtPostalCode" runat="server"></asp:TextBox>

      <asp:Button ID="btnGetProducts" runat="server" Text="Get Products"></asp:Button>

      <br/>

      <asp:GridView ID="gvProducts" runat="server"></asp:GridView>

    </div>

    </form>

</body>

</html>

Next in the code behind add the following code that gets executed when the button is clicked. This code calls the GetByPostalCode method that was created earlier.

List 3: Postback.aspx.vb code behind.

    Protected Sub btnGetProducts_Click(ByVal sender As Object, ByVal e As System.EventArgs) _

    Handles btnGetProducts.Click

        Dim prod As New Products

        gvProducts.DataSource = prod.GetByPostalCode(txtPostalCode.Text)

        gvProducts.DataBind()

    End Sub

Now lets take a look at the payload of this web application. To do this we will use a Fiddler a great tool for monitoring HTTP traffic. First start Fiddler and then start the Postback.aspx. Enter the postal code 12209 and click the Get Products button. You should see a table with 12 products, as shown in Figure 4.

Figure 4: Products returned for postal code 12209.

F4 Postabackaspx Products for 12209

In Fiddler, select the postback.aspx URL session and then select the Performance Statistics tab. The bytes sent are 1522 and bytes received are 5,295. If you would like to see the raw data sent to and from the browser, double click the session in Fiddler and select the Raw tab. It is apparent that the entire form is sent to the server and the server sends the entire page back to the browser.

Figure 5: Raw request and Response from Fiddler

F5 Fiddler Request and Response 

 

Partial Rendering with the UpdatePanel

This time we will use the AJAX UpdatePanel to fetch the products. Adding an UpdatePanel is extremely simple, all you need to do is add a ScriptManager tag and then surround the portion of the page you wish to render partially with an UpdatePanel control. Add a new web form to the site and give it a name UpdatePanel. Next copy the markup from Postback.aspx to the UpdatePanel.aspx and similarly copy the code behind from Postback.aspx.vb to UpdatePanel.aspx.vb.

Drag and drop the AJAX ScriptManager from the toolbox to the UpdatePanel.aspx making sure that the <ScriptManager> tag appears after the <form> tag. The next step is to add the AJAX UpdatePanel control to the UpdatePanel.aspx markup. Again this is done by dragging and dropping the AJAX UpdatePanel control from the toolbox. Add the <ContentTemplate> tags within the the <UpdatePanel> tags. Lastly, cut the markup for the Button, TextBox and GridView controls and paste them within the <ContentTemplate> tags. If all is well you should have code resembling Listing 4.

Listing 4: Markup with the UpdatePanel Control

<html xmlns="http://www.w3.org/1999/xhtml">

<head id="Head1" runat="server">

    <title>Untitled Page</title>

</head>

<body>

    <form id="form1" runat="server">

      <asp:ScriptManager ID="ScriptManager1" runat="server">

      </asp:ScriptManager>

    <div>

      <asp:UpdatePanel ID="UpdatePanel1" runat="server">

        <ContentTemplate>

          <asp:TextBox ID="txtPostalCode" runat="server"></asp:TextBox>

          <asp:Button ID="btnGetProducts" runat="server" Text="Get Products"></asp:Button>

          <br/>

          <asp:GridView ID="gvProducts" runat="server"></asp:GridView>       

        </ContentTemplate>

      </asp:UpdatePanel>

    </div>

    </form>

</body>

</html>

I have not included the button event handler code as it is identical to that of the Postback.aspx.vb.

Now let's look at the payload of this page. Start Fiddler and then navigate to the UpdatePanel.aspx page. You may need to set the UpdatePanel.aspx as the Start page. Next enter the postal code 12209 and select the Get Products button. The output from the page will be the same as in Figure 4. In Fiddler select the session that corresponds to the button click and then select the Performance Statistics tab. Fiddler reports that the bytes sent are 933 and bytes received are 5124. Not a huge saving, but let's not forget that the page consists almost entirely of the grid. What if the page contained other content that did not require refreshing then the payload difference between the postback model and the UpdatePanel would be significant. Figure 6 shows the request and response for the UpdatePanel. Note that the response only contains the markup that appears within the UpdatePanel. This is what is meant by partial rendering; only the markup that is requested is sent back in the HTTP response.  

Figure 6: UpdatePanel: Request and Response

F6 UpdatePanel Request and Response

This is great. By adding only a few lines of markup you can significantly reduce the payload of your page.

 

WCF and JSON

The last method I will demonstrate reduces the payload even more, but at a cost. JavaScript Object Notation (JSON) is an extremely terse way of serializing objects. Basically, JSON uses name-value pairs to serialize the object. XML serialization could be used, instead, but it would be more verbose and more importantly there is no built-in JavaScript support for XML serialization. Windows Communication Foundation (WCF) makes it extremely simple to create a service that uses JSON as it's transport mechanism.

To add an AJAX-enabled WCF Service, right click the web site and select Add New Item. The Add New Item dialog appears. Select the AJAX-enabled WCF Service template and name it ProductService.svc. Figure 7 shows the Add New Item dialog.

Figure 7: Add AJAX-enabled WCF Service.

F7 Add AJAX-enable WCF Service 

Adding the AJAX-enabled WCF Service causes three things to happen:

  1. ProductService.svc file is added to the web site. This is analogous to the asmx file generated file in web services.
  2. ProductService.vb file is added in the App_Code folder. This the code behind for the service. Remove the AspNetCompatibility attribute.
  3. The <system.serviceModel> node containing WCF configuration is added to the web.config file. Remove the line <serviceHostingEnvironment aspNetCompatibilityEnabled="true" /> from the web.config.

I've had issues with keeping the ASP.NET Compatibility setting. For some reason the client browser, at runtime, does not recognize the namespace (in this case CynotWhyNot). This is strange as intellisense recognizes the namespace.  

By default, the WCF service added will use JSON for serialization. Changing it to use XML requires a change to the <behaviors> section in the web.config. In order to use this service, changes are required to ProductService.vb, namely, we need to add the code to call the data access code. Listing 5 contains the necessary changes to call the Products GetByPostalCode method.

Listing 5: ProductService.vb

Imports System.ServiceModel

Imports System.ServiceModel.Activation

Imports System.ServiceModel.Web

 

<ServiceContract(Namespace:="CynotWhyNot.WhichOne")> _

Public Class ProductService

 

    <OperationContract()> _

    Public Function GetProducts(ByVal pc As String) As List(Of ProductsByPostalCode)

        Dim nwp As New Products

        Dim products As New List(Of ProductsByPostalCode)

        products = nwp.GetByPostalCode(pc)

        Return products

    End Function

 

End Class

Some points of interest:

  1. The <ServiceContract> attribute is added at the class level. All interfaces that are to be exposed as a service must have this attribute. Interestingly, when adding an AJAX-enabled WCF service no interface is generated. I have been looking for an explanation for why it behaves this way as I was under the impression that all WCF services must implement an interface that is attributed by the <ServiceContract> attribute. I have come across a few web casts that mention this as a convenience. Nonetheless it works fine.  It is good practise to add a namespace to avoid collisions. 
  2. The <OperationContract> attribute is added to the GetProducts method. The <OperationContract> attribute exposes the method as a service operation.

The WCF framework will automatically generate the JavaScript code required to invoke this service. To view the generated JavaScript, navigate the browser to the service and then  append /js to the URL. I am using FireFox here as it allows the view of the code directly in the browser. IE, on the other hand, r1equires that the JavaScript be saved to a file.

Figure 8: Generated JavaScript to Invoke the Service. 

F8 Javascript generated code

Note the function CynotWhyNot.WhichOne.ProductService.GetProducts. This will be the JavaScript function that will be called to asynchronously invoke the server side service.

With the WCF service in place we can now concentrate on the aspx page. This page will need to call the service, retrieve the product data from the call to the service and finally render the data.  Yes, dare I say it we need to write JavaScript. This is what I meant by there is a cost and that cost is coding HTML DOM using JavaScript.

Add a new web form to the web site and call it JSON.aspx. Next add the following code to JSON.aspx.

Listing 6: JSON.aspx Markup and Code

<html xmlns="http://www.w3.org/1999/xhtml">

<head runat="server">

    <title>Untitled Page</title>

</head>

 

<script type="text/javascript">

 

  function getProducts() {

    var postalCode = document.getElementById('txtPostalCode');

    CynotWhyNot.WhichOne.ProductService.GetProducts(postalCode.value,onGetProductsComplete);

  }

 

  function onGetProductsComplete(products) {

    clearRows();

    //If products are returned then go ahead and add rows to the table.

    if (products.length > 0)

    {

        for(var i = 0; i < products.length; i++)

        {         

            var product = products[i];

            addRow(

                product.CompanyName,

                product.Phone,

                product.PostalCode,

                product.ProductName,

                product.Quantity

            );

        }

    }

  }

 

  var c = "#EFF3FB";

  function addRow(company,phone,postal,product,quantity) {

    if (c == "#EFF3FB") { c = "white"; } else { c = "#EFF3FB"; }

    var table = document.getElementById('tblProducts');

    var row = table.insertRow(table.rows.length);

    row.bgColor = c;

    row.insertCell(0).appendChild(document.createTextNode(company));

    row.insertCell(1).appendChild(document.createTextNode(postal));

    row.insertCell(2).appendChild(document.createTextNode(phone));

    row.insertCell(3).appendChild(document.createTextNode(product));

    row.insertCell(4).appendChild(document.createTextNode(quantity));

  }

 

  function clearRows() {

    var table = document.getElementById('tblProducts');

    for(var i = table.rows.length - 1; i > 0 ; i--)

    {

      table.deleteRow(i);

    }

  }

 

</script>

 

<body>

    <form id="form1" runat="server">

    <div>

 

      <asp:ScriptManager ID="ScriptManager1" runat="server">

        <Services>

          <asp:ServiceReference Path="~/ProductService.svc" />

        </Services>

      </asp:ScriptManager>

 

      <asp:TextBox ID="txtPostalCode" runat="server"></asp:TextBox>

      <asp:Button ID="btnGetProducts" runat="server" Text="Get Products" OnClientClick="getProducts(); return false;"></asp:Button>

 

      <br/>

 

      <asp:Table ID="tblProducts" runat="server" EnableViewState="false" >

          <asp:TableHeaderRow BackColor="#DDDDDD" Font-Size="Small">

              <asp:TableHeaderCell HorizontalAlign="Left" Width="400px"><h2>Company</h2></asp:TableHeaderCell>

              <asp:TableHeaderCell HorizontalAlign="Left" Width="150px"><h2>Postal</h2></asp:TableHeaderCell>

              <asp:TableHeaderCell HorizontalAlign="Left" Width="150px"><h2>Phone</h2></asp:TableHeaderCell>

              <asp:TableHeaderCell HorizontalAlign="Left" Width="350px"><h2>Product</h2></asp:TableHeaderCell>

              <asp:TableHeaderCell HorizontalAlign="Left" Width="100px"><h2>Quantity</h2></asp:TableHeaderCell>

          </asp:TableHeaderRow>

      </asp:Table>

 

    </div>

    </form>

</body>

</html>

Let's breakdown this code.

The <ScriptManager> tag is added so that we can add a reference to ProductService.srv service. This will automatically cause the JavaScript generated code (see Figure 8) to be uploaded to the client browser. An added feature is that design time intellisense support for this JavaScript is included. I was impressed when I first saw this. The addition of the <ScriptManager> tag also causes the broser to upload the Microsoft Ajax library.

The button uses the OnClientClick attribute to call the custom function getProducts and then returns false. Returning false is needed otherwise the click action will cause a postback and we want to prevent this from happening.

The GridView has been replaced with a table and only the table header is added. The table rows containing the product information will be added using JavaScript.

Finally, we have four JavaScript functions contained within the script block. The function getProducts calls the JavaScript service proxy CynotWhyNot.WhichOne.ProductService.GetProducts passing two parameters; postalCode.value and onGetProductsComplete. postalCode.value is the postal code entered in the text box and  onGetProductsComplete is a function callback that is called when the service has completed. There are two optional parameters that have been omitted for brevity; a callback to a function in case an error occurred in the service and userContext which can be any data the developer wishes to pass to the callback function when the service successfully completed (in our case onGetProductsComplete).

The function onGetProductsComplete is called asynchronously when the service completes provided there were no errors. Any return value from the service call is returned as the first parameter to the callback. The great thing is this parameter is automatically de-serialized for you. That is, the parameter, products, in the callback to onGetProductsComplete  is a JavaScript object that parallels the server side object List(Of ProductsByPostalCode). Since JavaScript has no understanding of managed generics, the JavaScript object is returned as an array of objects of type ProductsByPostalCode.

The remaining JavaScript code is responsible for adding rows to the table. I  won't bore you with the details of it.

Run this page, enter a postal code of 12209 and then select the Get Products button. The resulting page is shown in figure 9.

Figure 9: Running JSON.aspx.

F9 Running JSON.aspx

The question is what kind of payload results when we use JSON. Start Fiddler and then browse to JSON.aspx, enter 12209 as the postal code and then select the Get Products button. Select the session in Fiddler and then click the Performance Statistics tab. The bytes sent is now only 546 and bytes received is 2408. Down by a factor of 2 from the previous ways. Now let's take a look at the actual data sent across the wire. Double click on the session in Fiddler and select the raw tab. The results are shown in Figure 10.

Figure 10: Data transmitted using JSON

F10 Data JSON

The request is reduced to {"pc":"12209"} and the response is reduced to a collection of name-value pairs. Now that's compact!

 

Summary

We started off by asking the question, Which One?

The traditional post back method is straight forward to code but has the largest payload as it always sends the entire form to the server and the server responds by sending the complete markup. The UpdatePanel is great way of reducing the payload. Implementation can be as simple as adding a few lines to the aspx file. Even though the payload is reduced, the server processing remains unaltered as the complete page life cycle process is run. Nonetheless, it is great way to improve performance. Lastly, JSON with WCF is by far the best way of minimizing payload. But this comes at a cost, the developer is required to write JavaScript code.

What I typically suggest:

  • Use postback if you are saving data, e.g., an entry form.
  • Use UpdatePanel when you need to update a portion of the screen that was initiated by the user.
  • Use JSON when you want to periodically update a portion of the screen. An example of this might be a stock price updated every minute.

 

Guess the movie

Out of order, I show you out of order. You don't know what out of order is, Mr. Trask. I'd show you, but I'm too old, I'm too tired, I'm too fuckin' blind. If I were the man I was five years ago, I'd take a FLAMETHROWER to this place! Out of order? Who the hell do you think you're talkin' to? I've been around, you know? There was a time I could see. And I have seen. Boys like these, younger than these, their arms torn out, their legs ripped off. But there isn't nothin' like the sight of an amputated spirit. There is no prosthetic for that. You think you're merely sending this splendid foot soldier back home to Oregon with his tail between his legs, but I say you are... executin' his soul! And why? Because he's not a Bairdman. Bairdmen. You hurt this boy, you're gonna be Baird bums, the lot of ya. And Harry, Jimmy, Trent, wherever you are out there, FUCK YOU TOO!

Currently rated 5.0 by 2 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Categories: ASP.NET | JSON | WCF
Posted by CynotWhyNot on Wednesday, April 23, 2008 11:26 AM
Permalink | Comments (72) | Post RSSRSS comment feed