Latest Entries »

Sunday, May 29, 2011

Hightly efficiently GridView with JSON and Caching(in part 2)



Introduction

Hello. In this article I will show you how to extremely improve GridView performance. For this purpose I prepared table with huge amout of data (about 4000 rows). We will start with trivial things and then we will do real code exhibition. Note that I don't use pagination now.

Little things matter. Really.

Take a look at this GridView:
<asp:GridView ID="GblContactsGv" runat="server" AutoGenerateColumns="False" CssClass="EntityGridView"
        OnRowCommand="GblContactsGv_RowCommand" OnRowDeleting="GblContactsGv_Deleting">
        <Columns>
            <asp:TemplateField HeaderText="ID">
                <ItemTemplate>
                    <asp:Label runat="server" ID="glbContactIdLbl" Text='<%# Eval("gblContactId") %>'
                        CssClass="label" />
                </ItemTemplate>
            </asp:TemplateField>
            <asp:TemplateField HeaderText="Email">
                <ItemTemplate>
                    <asp:Label runat="server" ID="glbContactIdLbl" Text='<%# Eval("Email") %>' CssClass="label" />
                </ItemTemplate>
            </asp:TemplateField>
            <asp:TemplateField HeaderText="Phone">
                <ItemTemplate>
                    <asp:Label runat="server" ID="glbContactIdLbl" Text='<%# Eval("Phone") %>' CssClass="label" />
                </ItemTemplate>
            </asp:TemplateField>
        </Columns>
    </asp:GridView>
This simple GridView will render your HTML such like this:
<tr>
   <td>
                    <span class="label" id="ctl00_CenterColumn_GblContactsGv_ctl02_glbContactIdLbl">78</span>
                </td><td>
                    <span class="label" id="ctl00_CenterColumn_GblContactsGv_ctl02_glbContactIdLbl">test@test.test 78</span>
                </td><td>
                    <span class="label" id="ctl00_CenterColumn_GblContactsGv_ctl02_glbContactIdLbl">123456 78</span>
                </td>
  </tr>
On the other hand the same result could be done with this GridView:
<asp:GridView ID="GblContactsGv" runat="server" AutoGenerateColumns="False" CssClass="EntityGridView"
        OnRowCommand="GblContactsGv_RowCommand" OnRowDeleting="GblContactsGv_Deleting">
        <Columns>
            <asp:BoundField DataField="gblContactId" HeaderText="ID" />
            <asp:BoundField DataField="Email" HeaderText="Email" />
            <asp:BoundField DataField="Phone" HeaderText="Phone" />
        </Columns>
</asp:GridView>
So again let see what is going to send to browser:
<tr>
    <td>
        78
    </td>
    <td>
        test@test.test 78
    </td>
    <td>
        123456 78
    </td>
</tr>
In first code listing each row(for 3 columns) contains 438 characters and in second GridView using BoundFields each row contains just 112 characters. In this case I saved 326 characters per row. Assuming you use UTF-8 as page encoding it's 652 bytes by row!

Depending on amount of rows and columns I can save about 30%-40% of data.  Wherever you can you should avoid any control with runat="server" because each row is using NamingContainer to add unique ClientID.

Avoid PostBacks and UpdatePanels.

Now I will show you how to make dramatically performance improvement. In this scenario I will use GridView shown above but I added all columns definied in C# class holding database table. I also fill table with ~ 4000 rows. I will create 3 pages: PostBack.aspx, UpdatePanel.aspx and jQuery.aspx. First will won't use any AJAX, second will use build in asp.net UpdatePanel to create AJAX Request and last example will be using jQuery.ajax() method with JSON and [WebMethod()].
<asp:GridView ID="GblContactsGv" runat="server" AutoGenerateColumns="False" CssClass="EntityGridView"
        OnRowCommand="GblContactsGv_RowCommand" OnRowDeleting="GblContactsGv_Deleting">
        <Columns>
            <asp:BoundField DataField="gblContactId" HeaderText="ID" />
            <asp:BoundField DataField="Email" HeaderText="Email" />
            <asp:BoundField DataField="Phone" HeaderText="Phone" />
            <asp:BoundField DataField="URL" HeaderText="URL" />
            <asp:BoundField DataField="Description" HeaderText="Description" HtmlEncode="false" />
            <asp:BoundField DataField="Address" HeaderText="Address" />
            <asp:TemplateField HeaderText="Delete">
                <ItemTemplate>
                    <asp:LinkButton runat="server" ID="deleteBtn" CommandArgument='<%# Eval("gblContactId") %>' CommandName="delete" 
                    Text="Delete" />
                </ItemTemplate>
            </asp:TemplateField>
        </Columns>
</asp:GridView>

Now we will need FireBug and Network Tab to show some magic.

Here I got first page. Whenever I load page or perform <asp:linkbutton /> action I receive alost 3MB of data.


Now I thought a UpdatePanel will be better solution doing AJAX request. Let's try:
<asp:UpdatePanel runat="server" ID="upPanel1">
        <ContentTemplate>
   <!-- The same GridView like before -->
  </ContentTemplate>
</asp:UpdatePanel>

And again take a look what happen when I click LinkButton:


Image above show that Request["__VIEWSTATE"] (1,13 MB) and Request["__EVENTVALIDATION"] (31,7 KB) is extremly big! Whenever you use ajax request each time those two hidden inputs are send to server. Even when you update just one single Label.

Now I will show how to do simple action via jQuery.ajax() and C# [WebMethod()] for performance improve. One thing what will change in aspx file will be script tag:
$(document).ready(function () {
    $('.delete').click(function () {
        var commentId = $(this).closest('tr').find(':hidden').val();
        var row = $(this).closest('tr');

        $.ajax({
            type: "POST",
            url: "jQuery.aspx/DeleteComment",
            data: "{'c':'" + commentId + "'}",
            contentType: "application/json; charset=utf-8",
            dataType: "json",
            beforeSend: function () {
                $('#Result').html('Deleting...');
            },
            success: function (msg, textStatus, jqXHR) {
                if (msg.d == '0') {
                    row.closest('tr').hide();
                    $('#Result').html('Deleted !');
                } else {

                }
                return false;
            },
            error: function (jqXHR, textStatus, errorThrown) {
                $('#Result').html('Error !' + errorThrown);
            }
        });
        return false;
    });
And in GridView let modify last column:
<asp:TemplateField HeaderText="Delete">
 <ItemTemplate>
  <input type="hidden" value='<%# Eval("gblContactId") %>' />
  <asp:LinkButton runat="server" ID="deleteBtn" CommandArgument='<%# Eval("gblContactId") %>'
   CommandName="delete" CssClass="delete" Text="Usuń" />&nbsp;&nbsp;
  <a href="#" class="delete">Delete</a>
 </ItemTemplate>
</asp:TemplateField>
Last thing to show off is aspx.cs method:
[WebMethod()]
    public static string DeleteComment(string c)
    {
        string response = "";

        if (DataRepository.GblContactProvider.Delete(int.Parse(c)))
        {
            response = "0";
        }
        return response;
    }
Now let's look on Firebug and see some magic. Here we got Request data:

And here we got answer from server:
Here is real performance shock. I don't want even count performance improvment!

Loading data with JSON

If we have need to load huge amout of data we should consider load pure JSON data and process this data inside JavaScript code. Before we perform this we need have class which has [Serializable()] attribute and then use any JSON serializer available in .NET world. Here I used build in System.Runtime.Serialization.Json.DataContractJsonSerializer class. Here is example:
[Serializable()]
class Contact
{
    public int GblContactId;
    public string Email;
    //Other properties

    public Contact(int gblcontactId, string email, string url, string desc, string phone, string address)
    {
        this.GblContactId = gblcontactId;
        this.Email = email;
        //Other stuff in constructor
    }
    //GetAll() for load data
    //....
    //....
}
// Inside aspx.cs
[WebMethod()]
    public static string LoadCommentsAjax()
    {
        List contacts = Contact.GetAll();
        return Serialize(contacts);
    }

    public static string Serialize<T>(T obj)
    {
        DataContractJsonSerializer serializer = new DataContractJsonSerializer(obj.GetType());
        using (MemoryStream ms = new MemoryStream())
        {
            serializer.WriteObject(ms, obj);
            return Encoding.Default.GetString(ms.ToArray());
        }
    }
For big JSON data we need to modify web.config:
<system.web.extensions>
<scripting>
  <scriptResourceHandler enableCaching="true" enableCompression="true"/>
  <webServices>
 <jsonSerialization maxJsonLength="2147483644" />
  </webServices>
</scripting>
</system.web.extensions>

Now let's test this with script:
$('.ajaxloadjQuery').click(function () {
    $.ajax({
        type: "POST",
        url: "jQuery.aspx/LoadCommentsAjax",
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        beforeSend: function () {
            $('#Result').html(' Loading comments ...');
        },
        success: function (msg, textStatus, jqXHR) {
            var items = $.parseJSON(msg.d);

            $(items).each(function () {
                console.log('ID: ' + this.GblContactId + ' Address: ' + this.Address + ' Description ' + this.Description + ' Email: ' + this.Email + ' Phone: ' + this.Phone + ' URL: ' + this.Url);
            });
            var jq = jqXHR;
            return false;
        },
        error: function (jqXHR, textStatus, errorThrown) {}
    });
    return false;
});
When we click we will have ajax request with just 888.2 KB rather than about 2.8MB. It's more than 30% less request size.

One important notice is that in $(items).each(function(...)) I've used something this.GblContactId or this.Address. This is because DataContractJsonSerializer automatically maps serialized class into JSON object with the same property names. Okey we used Console.Log() to print output into FireBug but I will show you a complete script to create html table from client side based on JSON data:
$('.ajaxloadjQuery').click(function () {
    $.ajax({
        type: "POST",
        url: "jQuery.aspx/LoadCommentsAjax",
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        beforeSend: function () {
            $('#Result').html(' ładuje komentarze ...');
        },
        success: function (msg, textStatus, jqXHR) {
            var ts = textStatus;
            var items = $.parseJSON(msg.d);
            var rows = '';
            //Adding header info:
            rows += '<tr>'
            rows += '<th>ID</th>';
            rows += '<th>Address</th>';
            rows += '<th>Description</th>';
            rows += '<th>Email</th>';
            rows += '<th>Phone</th>';
            rows += '<th>Url</th>';
            rows += '<th>Delete</th>';
            rows += '</tr>';
            var counter = 0;
            //('#myTable').show();
            $(items).each(function () {
                rows += '<tr>';
                rows += '<td>' + this.GblContactId + '</td>';
                rows += '<td>' + this.Address + '</td>';
                rows += '<td>' + this.Description + '</td>';
                rows += '<td>' + this.Email + '</td>';
                rows += '<td>' + this.Phone + '</td>';
                rows += '<td>' + this.Url + '</td>';
                rows += '<td> <input type="hidden" value="' + this.GblContactId + '" /> <a href="#" class="delete">Delete</a> </td';
                rows += '</tr>';
                counter++;
                //For showing JSON data in portion with 100 rows
                if (counter % 100 == 0) {
                    $('#myTable').append(rows);
                    rows = '';
                }
            });
            $('#myTable').append(rows);
            var jq = jqXHR;
            return false;
        },
        error: function (jqXHR, textStatus, errorThrown) {
            var ts = textStatus;
            var err = errorThrown;
            var jq = jqXHR;
            $('#Result').html('Pojawił się błąd !' + err);
            console.log(ts + ' ' + err + ' ' + jq);
        }
    });
    return false;
});

Conclusions

Here doing jQuery.ajax with big data set we saved approx 30% on our Request size passing just only JSON insted of rendered html table(GridView). Next very suprising thing that really matter was usage of Bound Fields in contradiction to using standard server controls. We saved again 30% of Request size. It's really good result.

Yes we could use pagination for making our Request much more smaller and use more sophisticated architecture but sometimes we are forced to send big data package.

Monday, May 23, 2011

Most useful features in Firebug 1.7.0

Comming soon :-)

Saturday, May 21, 2011

Saving scroll position beetween pages in ASP.NET after redirecting

Comming soon.

jQuery find function $.find('selector') versus traditional selecting $('selector')

Comming soon.