September 2007 - Posts

In today's web 2.0 world everyone is exposing their application by some kind of services. Some of them even goes further and returning the data in JSON format so that we can consume it directly in browser without using any kind of proxy. There are a lot of popular applications including Yahoo's Flickr, Google Ajax Feed API etc using this technique to deliver the data right into the browser. In this post, I will show you, how to create the same which exposes the Northwind database data. If you just heard this JSONP term, I recommend you to read this page before moving forward.

Let us take a quick example of the client side. Let us assume that we want to show the top 10 customers of the Northwind who submitted the most orders. Here is the code which shows the customers in a bullet list:

<div id="divCustomers"></div>
<script type="text/javascript">
    function onCustomerLoaded(result, methodName)
    {
        var html = '<ul>';

        for(var i = 0; i < result.length; i++)
        {
            html += '<li>' + result[i] + '</li>';
        }

        html += '</ul>';

        document.getElementById('divCustomers').innerHTML = html;
    }
</script>
<script type="text/javascript" 
src="Service.ashx?methodName=topCustomers&jsonp=onCustomerLoaded"></script>

Here the most important thing is the script tag at the end. What we are doing is requesting a generic handler, in this case the Service.ashx and passing the methodName as topCustomers and the callback function. The handler in turn returns the customer names in JSON format in the callback function along with the method name, so the callback function gets automatically executed once it is downloaded in the browser. For example, the following will be returned for the above request:

onCustomerLoaded(["Save-a-lot Markets","Ernst Handel","QUICK-Stop","Folk och fä HB","Hungry Owl All-Night
 Grocers","HILARION-Abastos","Berglunds snabbköp","Rattlesnake Canyon Grocery","Bon app\u0027","Frankenversand"
], 'topCustomers');

Let us now see how the server side is constructed. But before that let me share with you the designing ideas. Instead of having multiple generic handlers or a single handler with lots of switch/if statement we will use a single generic handler with Factory design pattern. The Factory will return a concrete class based upon the passed methodName which will be used to solely handle that request. The methodName and its class will be declared in the web.config file, the Factory will be responsible to read the config file and instantiate the concrete handler. On the other hand the generic handler will request the Factory for the concrete handler and perform some pre/post processing. The following shows the code of the ProcessRequest method of the Service.ashx file:

BaseHandler handler = null;

try
{
    handler = HandlerFactory.CreateHandler(context);

    if (handler != null)
    {
        handler.Execute();

        string output = string.Empty;
        string callbackMethodName = context.Request.Params["jsonp"];

        if (!string.IsNullOrEmpty(callbackMethodName))
        {
            string callback = string.Format(CultureInfo.CurrentCulture, "{0}({1}, '{2}');", callbackMethodName, handler.Output, handler.MethodName);
            output += callback;
        }
        else
        {
            output = string.Format(CultureInfo.CurrentCulture, "var {0} = {1};", handler.MethodName, handler.Output);
        }

        context.Response.ContentType = "application/x-javascript";
        context.Response.Write(output);
    }
    else
    {
        context.Response.StatusCode = 404;
    }
}
finally
{
    if (handler != null)
    {
        handler.Dispose();
    }
}

The generic handler requesting the Factory to return the concrete handler, if found it executes then handler. Then it formats the result of that handler depending upon whether the callback function is specified or not. We will shortly see how to use it without specifying the callback function. At last, it sets the response content type and returns the response. If the concrete handler is not found it throws a 404 error.

Now let us see the Factory code, it reads the configuration file and creates the handler with reflection. Here is the code of the CreateHandler method.

string methodName = context.Request.Params["methodName"];

if (!string.IsNullOrEmpty(methodName))
{
    HandlerMapSection settings = (HandlerMapSection)ConfigurationManager.GetSection(HandlerMapSection.SectionName);

    if (settings != null)
    {
        HandlerMap map = settings.Maps[methodName];

        if (map != null)
        {
            BaseHandler handler = (BaseHandler)_currentAssembly.CreateInstance(map.TypeName,
                                    false, BindingFlags.CreateInstance, null, new object[] { context },
                                    System.Globalization.CultureInfo.CurrentCulture, null);

            return handler;
        }
    }
}

return null;

And the web.config file contains the following lines:

<configSections>
  <section name="handlerMapping" type="HandlerMapSection"/>
</configSections>
<handlerMapping>
  <map methodName="getCustomerList" typeName="CustomerListHandler"/>
  <map methodName="topCustomers" typeName="TopCustomerHandler"/>
</handlerMapping>

If we want to add new methods, we will simply create a new class inherited from BaseHandler. By the way I am using the Asp.net Ajax JavaScriptSerializer for serializing the response. But you can use any other if you want.

I have stated in the above that it is also possible to use it in the client side without passing the callback function. The following shows the code:

<div id="divCustomers"></div>
<script type="text/javascript" src="Service.ashx?methodName=topCustomers"></script>
<script type="text/javascript">
    var html = '<ul>';

    for(var i = 0; i < topCustomers.length; i++)
    {
        html += '<li>' + topCustomers[i] + '</li>';
    }

    html += '</ul>';

    document.getElementById('divCustomers').innerHTML = html;
</script>

As we did not pass the callback function it returns the data as a variable of the method name. For example, for the above, the following is returned:

var topCustomers = ["Save-a-lot Markets","Ernst Handel","QUICK-Stop","Folk och fä HB","Hungry Owl All-Night
 Grocers","HILARION-Abastos","Berglunds snabbköp","Rattlesnake Canyon Grocery","Bon app\u0027","Frankenversand"
];

The most important thing working with the JSONP request is the placing of the JSONP script tag. For example, if we place the JSONP script tag after the codes which shows the bullet list we will get an error that the topCustomers is undefined. The same holds true for the first example, if we place the JSONP tag above the codes, we will get an error that onCustomerLoaded is undefined. The reason behind this is when a browser encounters an external script tag it holds its execution until it downloads it.

So far we have seen the examples where the script tag is already declared in the page. But in real life we might have to add the script tag dynamically based upon the user action. The following shows a simple customer list which adds the script dynamically and shows 10 customer at a time in a bullet list:

<div id="divCustomers"></div>
<input id="btnPrevious" type="button" value="<" disabled="disabled" onclick="movePrevious()" />
<input id="btnNext" type="button" value=">" disabled="disabled" onclick="moveNext()" />
<script type="text/javascript">
    var _rowPerPage = 10;
    var _pageNo = 1;
    var _startRowIndex = 0;

    function movePrevious()
    {
        _pageNo -= 1;
        loadCustomers();
    }

    function moveNext()
    {
        _pageNo += 1;
        loadCustomers();
    }

    function loadCustomers()
    {
        _startRowIndex = ((_pageNo - 1) * _rowPerPage);
        var url = 'Service.ashx?methodName=getCustomerList&start=' + _startRowIndex + '&max=' + _rowPerPage + '&jsonp=onLoaded';

        addScript(url);
    }

    function onLoaded(result, methodName)
    {
        var html = '<ul>';

        for(var i = 0; i < result.Rows.length; i++)
        {
            html += '<li>' + result.Rows[i].Company + '</li>';
        }

        html += '</ul>';
        document.getElementById('divCustomers').innerHTML = html;

        document.getElementById('btnPrevious').disabled = (_startRowIndex < _rowPerPage);
        document.getElementById('btnNext').disabled = ((_startRowIndex + _rowPerPage) >= result.Total);
    }

    function addScript(scriptUrl)
    {
        var script = document.createElement('script');

        script.setAttribute('type','text/javascript');
        script.setAttribute('src', scriptUrl);

        var head = document.getElementsByTagName('head')[0];
        head.appendChild(script);
    }

    loadCustomers();
</script>

You will find the complete code in the bottom of the post.

A note about the Asp.net Ajax: As far as I can remember the earlier version of Asp.net Ajax also has this feature but the final version does not allows this as it requires application/json in the request header which is not possible to add by the script tag. Certainly there is some security concerns and you should not expose your sensitive data with this. You will find more details on Scott Guthrie's this blog post. But the recent version Microsoft Astoria also has the JSONP feature.

Download: Full Source

kick it on DotNetKicks.com

Jeff Prosise showed us the power of Asynchronous Asp.net Page in developing truly scalable web application in his great article. But I found a lot people finding hard time implementing it in real life based upon those examples. For example, in the data bind example he uses the BeginExcecuteReader/EndExecuteReader method of the DataReader but in real life we do not use it, instead we return some custom entity collection or the DataReader/DataSet directly from the bussiness logic layer. Omar showed me why this approach will not work and even it will add extra overhead comparing the regular model.In this post, I will show you some real life usage of the asynchronous pages and how easily you can convert your regular pages with it. Let us start with a simple example, you want show all the customers of USA of the Northwind database in a GridView. You will come up something similar like the following:

protected void Page_Load(object sender,
    EventArgs e)
{
    if (!IsPostBack)
    {
        LoadCustomers();
    }
}

private void LoadCustomers()
{
    string SQL = string.Format("SELECT * FROM [Customers] WHERE [Country] = '{0}'", "USA");

    using (IDbConnection cnn = new SqlConnection(_connectionString))
    {
        cnn.Open();

        using (IDbCommand cmd = cnn.CreateCommand())
        {
            cmd.CommandText = SQL;
            cmd.CommandType = CommandType.Text;

            using (IDataReader rdr = cmd.ExecuteReader())
            {
                //Bind it with the GridView
                gvCustomers.DataSource = rdr;
                gvCustomers.DataBind();
            }
        }
    }
}

Now let us convert this regular page with the asynchronous model. First add Async="true" in the page directive. Next, create a delegate with the same signature of LoadCustomers method, we will use this delegate for the async operation. The following shows the complete code:

private delegate void LoadData();
private LoadData _loadCustomers;

protected void Page_Load(object sender,
    EventArgs e)
{
    if (!IsPostBack)
    {
        AddOnPreRenderCompleteAsync(new BeginEventHandler(BeginLoadCustomers),
            new EndEventHandler(EndLoadCustomers));
    }
}

private IAsyncResult BeginLoadCustomers(object sender,
    EventArgs e,
    AsyncCallback cb,
    object state)
{
    _loadCustomers = new LoadData(LoadCustomers);
    return _loadCustomers.BeginInvoke(cb, state);
}

private void EndLoadCustomers(IAsyncResult result)
{
    _loadCustomers.EndInvoke(result);
}

private void LoadCustomers()
{
    string SQL = string.Format("SELECT * FROM [Customers] WHERE [Country] = '{0}'", "USA");

    using (IDbConnection cnn = new SqlConnection(_connectionString))
    {
        cnn.Open();

        using (IDbCommand cmd = cnn.CreateCommand())
        {
            cmd.CommandText = SQL;
            cmd.CommandType = CommandType.Text;

            using (IDataReader rdr = cmd.ExecuteReader())
            {
                //Bind it with the GridView
                gvCustomers.DataSource = rdr;
                gvCustomers.DataBind();
            }
        }
    }
}

What we are doing is very simple, we are wrapping up the actual method with a Begin/End Pair and we are passing the pair to AddOnPreRenderCompleteAsync method. In the Begin we are creating an instance of the delegate and calling the invoke which actually fires a thread from the ThreadPool. We can also use the RegisterAsyncTask method instead of AddOnPreRenderCompleteAsync and there some extra advantage of using it instead of AddOnPreRenderCompleteAsync, among the advantages I think the thread impersonate(It is not possible to access the HttpContext.Current from the other threads) and the parallel execution of multiple async method are the most important. Let us consider that you also want to show the employees in another GridView of the Northwind database along with the customers which country is set to USA, since there is no dependency between these operations we can use two separate thread to for each operation. The following shows the complete code:

private delegate void LoadData();
private LoadData _loadCustomers;
private LoadData _loadEmployees;

protected void Page_Load(object sender,
    EventArgs e)
{
    if (!IsPostBack)
    {
        RegisterAsyncTask(
            new PageAsyncTask(
            new BeginEventHandler(BeginLoadCustomers),
            new EndEventHandler(EndLoadCustomers), null, null, true)
            );

        RegisterAsyncTask(
            new PageAsyncTask(
            new BeginEventHandler(BeginLoadEmployees),
            new EndEventHandler(EndLoadEmployees), null, null, true)
            );
    }
}

private IAsyncResult BeginLoadCustomers(object sender,
    EventArgs e,
    AsyncCallback cb,
    object state)
{
    _loadCustomers = new LoadData(LoadCustomers);
    return _loadCustomers.BeginInvoke(cb, state);
}

private void EndLoadCustomers(IAsyncResult result)
{
    _loadCustomers.EndInvoke(result);
}

private IAsyncResult BeginLoadEmployees(object sender,
    EventArgs e,
    AsyncCallback cb,
    object state)
{
    _loadEmployees = new LoadData(LoadEmployees);
    return _loadEmployees.BeginInvoke(cb, state);
}

private void EndLoadEmployees(IAsyncResult result)
{
    _loadEmployees.EndInvoke(result);
}

private void LoadCustomers()
{
    string SQL = string.Format("SELECT * FROM [Customers] WHERE [Country] = '{0}'", "USA");

    using (IDbConnection cnn = new SqlConnection(_connectionString))
    {
        cnn.Open();

        using (IDbCommand cmd = cnn.CreateCommand())
        {
            cmd.CommandText = SQL;
            cmd.CommandType = CommandType.Text;

            using (IDataReader rdr = cmd.ExecuteReader())
            {
                //Bind it with the GridView
                gvCustomers.DataSource = rdr;
                gvCustomers.DataBind();
            }
        }
    }
}

private void LoadEmployees()
{
    string SQL = string.Format("SELECT * FROM [Employees] WHERE [Country] = '{0}'", "USA");

    using (IDbConnection cnn = new SqlConnection(_connectionString))
    {
        cnn.Open();

        using (IDbCommand cmd = cnn.CreateCommand())
        {
            cmd.CommandText = SQL;
            cmd.CommandType = CommandType.Text;

            using (IDataReader rdr = cmd.ExecuteReader())
            {
                //Bind it with the GridView
                gvEmployees.DataSource = rdr;
                gvEmployees.DataBind();
            }
        }
    }
}

The code is almost same as the previous one except we have added another set of methods for the employees and we are using the RegisterAsyncTask method instead of AddOnPreRenderCompleteAsync in page load event. Here the most important thing in the RegisterAsyncTask method is the last parameter where we are passing true, this ensures that both the method will be executed simultaneously. If we used AddOnPreRenderCompleteAsync method or do not pass true in the last parameter of the RegisterAsyncTask method then it will first execute the customer part, once complete it start the employee part.

In the last and final section, we will see an Master/Detail example of Async Page. We will have a DropDownList which will be populated with all the categories of Northwind database and based upon the selected category we will load the products in a GridView. Since the product loading depends upon the selected category& thus it is not possible to do the parallel execution of both method, instead the product loading needs to wait until the category loads first. The following shows the compete code:

private delegate void LoadData();
private LoadData _loadCategories;
private LoadData _loadProducts;

protected void Page_Load(object sender,
    EventArgs e)
{
    if (!IsPostBack)
    {
        RegisterAsyncTask(
            new PageAsyncTask(
            new BeginEventHandler(BeginLoadCategories),
            new EndEventHandler(EndLoadCategories), null, null)
            );

        RegisterAsyncTask(
            new PageAsyncTask(
            new BeginEventHandler(BeginLoadProducts),
            new EndEventHandler(EndLoadProducts), null, null)
            );
    }
}

protected void Category_Changed(object sender,
    EventArgs e)
{
    RegisterAsyncTask(new PageAsyncTask(
        new BeginEventHandler(BeginLoadProducts),
        new EndEventHandler(EndLoadProducts), null, null)
        );
}

private IAsyncResult BeginLoadCategories(object sender,
    EventArgs e,
    AsyncCallback cb,
    object state)
{
    _loadCategories = new LoadData(LoadCategories);
    return _loadCategories.BeginInvoke(cb, state);
}

private void EndLoadCategories(IAsyncResult result)
{
    _loadCategories.EndInvoke(result);
}

private IAsyncResult BeginLoadProducts(object sender,
    EventArgs e,
    AsyncCallback cb,
    object state)
{
    _loadProducts = new LoadData(LoadProducts);
    return _loadProducts.BeginInvoke(cb, state);
}

private void EndLoadProducts(IAsyncResult result)
{
    _loadProducts.EndInvoke(result);
}

private void LoadCategories()
{
    const string SQL = "SELECT [CategoryID], [CategoryName] FROM [Categories]";

    using (IDbConnection cnn = new SqlConnection(_connectionString))
    {
        cnn.Open();

        using (IDbCommand cmd = cnn.CreateCommand())
        {
            cmd.CommandText = SQL;
            cmd.CommandType = CommandType.Text;

            using (IDataReader rdr = cmd.ExecuteReader())
            {
                ddlCategory.DataSource = rdr;
                ddlCategory.DataBind();
            }
        }
    }
}

private void LoadProducts()
{
    string SQL = string.Format("SELECT * FROM [Products] WHERE [CategoryID] = {0}",
        ddlCategory.SelectedItem.Value);

    using (IDbConnection cnn = new SqlConnection(_connectionString))
    {
        cnn.Open();

        using (IDbCommand cmd = cnn.CreateCommand())
        {
            cmd.CommandText = SQL;
            cmd.CommandType = CommandType.Text;

            using (IDataReader rdr = cmd.ExecuteReader())
            {
                gvProducts.DataSource = rdr;
                gvProducts.DataBind();
            }
        }
    }
}

As you can see that we are not passing true in the last argument of RegisterAsyncTask method, it ensures that the category will load first and then the products. We are also registering another async task in the selected index change event of the DropDownList.

I hope this will clear your confusion with async pages and you will be able to implement easily it in your projects.

Download: Full Source

kick it on DotNetKicks.com

Today, I found another interesting post in Asp.net Ajax Web Service Forum. How do you ensure that your web service is called from your aspx page. The goal is to protect your web service from unauthorized usage. Certainly, you can clear the protocols section of the web.config but it will not make any impact calling the ajax enabled web service. You will still be able to call the web method with the following code:

HttpWebRequest request = (HttpWebRequest)WebRequest.Create("YOUR URL");

request.ContentType = "application/json; charset=utf-8";
request.Method = "POST";

//Assuming the HTTP Post does not require any form fields
using (HttpWebResponse response = (HttpWebResponse) request.GetResponse())
{
    using (StreamReader sr = new StreamReader(response.GetResponseStream()))
    {
        Console.WriteLine(sr.ReadToEnd());
    }
}

Checking the http referer in the web method will not also help as it can be easily set too. So what is the way to ensure that it is not called from any other place except the aspx pages. Truly speaking, there is no full proof way you can guarantee but you can add some complexity, so it gets a bit difficult comparing the above. Lets say in the aspx page which is used to call the web service we add the following code:

private void GenerateSecurityTicket()
{
    string cacheKey = User.Identity.Name + ":securityTicket";
    string securityTicket = Guid.NewGuid().ToString();

    Cache[cacheKey] = securityTicket;

    string script = string.Format("SECURITY_TICKET = '{0}';", securityTicket);

    ScriptManager.RegisterClientScriptBlock(this, this.GetType(), "securityKey", script, true);
}

protected void Page_Load(object sender, EventArgs e)
{
    GenerateSecurityTicket();
}

What it does is, every time the page is rendered it creates a new Guid, puts it in the cache and embedded it as a JavaScript global variable. Next, in the web service we add the following code:

[WebMethod]
public string SecureMethod()
{
    EnsureTicket();

    return "This is a valid call.";
}

private void EnsureTicket()
{
    HttpContext context = HttpContext.Current;

    string headerTicket = context.Request.Headers["securityTicket"];

    if (string.IsNullOrEmpty(headerTicket))
    {
        throw new SecurityException("Security ticket must be present.");
    }

    string cacheKey = context.User.Identity.Name + ":securityTicket";
    string cacheTicket = (string)context.Cache[cacheKey];

    if (string.Compare(headerTicket, cacheTicket, false) != 0)
    {
        throw new SecurityException("Security ticket mismatched.");
    }
}

So what we are doing is checking if the http request has any key and if it matches with the key stored in the cache. If the key is not present or does not match with the stored key we are simply raising a security exception.

Next, when we are invoking the web method we have to make sure that the required header is added. But unfortunately there is no way to add the header directly in the WebServiceProxy class. Instead, we have to hook the WebRequestManager invokingRequest event to add the header. This is a special event which is fired for all kinds of ajax operation including the Update Panel partial update. The following shows the code:

function pageLoad(sender, args)
{
    Sys.Net.WebRequestManager.add_invokingRequest(onInvoke);
}

function pageUnload(sender, args)
{
    Sys.Net.WebRequestManager.remove_invokingRequest(onInvoke);
}

function onInvoke(sender, args)
{
    args.get_webRequest().get_headers()['securityTicket'] = SECURITY_TICKET;
}

function invokeSecureMethod()
{
    SecureService.SecureMethod(
                                    function(result)
                                    {
                                        alert(result);
                                    },
                                    function(e)
                                    {
                                        alert(e.get_message());
                                    }
                              );
}

Now anybody wants to call the web method needs to get the key from page, we can even add more complexity by encapsulating the key generation with a JavaScript function rather than stroing it plain variable. Again it is not proper way to ensure unauthorized usage but does any body has a better idea?

Download: Full Source

kick it on DotNetKicks.com

I am extremely sorry to my blog visitors that the source codes as well as the screenshots are chopped off with this half screen layout.  Let us pray that the WebLog support team will add some full page theme asap.

In August Ajax column of DotNetSlackers, I have shown an effective exception logging system. In that column, I concluded saying that a lot of enhancement can be done with it which I finally did and created a project in CodePlex - Asp.net Ajax Exception Logging. Here is the list of enhancements:

  • A new Control is introduced - ExceptionManager which is used to register the listeners. No need to manually register those.
  • Automatic Logging of Unhandled and Update Panel Exceptions not need to hook those events manually. Just turn on the HandleUnhandledException and HandleUpdatePanelException which is by default turned on.
  • Encapsulating listeners in its own server side class.
  • An Enhanced version of web service listener which includes dynamic location of web service path/method name and batch submitting of exceptions.
  • A built-in web service to log exceptions - ExceptionLogService.
  • Introducing provider model in server side, so you can roll your provider to log these exceptions.
  • Built-in provider which logs in Asp.net HealthMonitoring/WebEvents.

Let me give a quick tour.

Registering Listeners

Earlier you have to register the listeners like the following:

function applicationLoad(sender, args)
{
    var mgr = Ajax.Logging.ExceptionManager.getInstance();

    mgr.addListener(new Ajax.Logging.WebServiceListener());
    mgr.addListener(new Ajax.Logging.PanelListener($get('pnlExceptions')));
    mgr.addListener(new Ajax.Logging.SysDebugListener());
    mgr.addListener(new Ajax.Logging.AlertListener());
}

Now you will do it in the server side:

<AjaxLogging:ExceptionManager ID="TheExceptionManager" runat="server">
    <Listeners>
        <AjaxLogging:WebServiceListener ServicePath="~/ExceptionLogService.asmx" ServiceMethod="Log" />
        <AjaxLogging:PanelListener Panel="pnlException" />
        <AjaxLogging:SysDebugListener />
        <AjaxLogging:AlertListener />
    </Listeners>
</AjaxLogging:ExceptionManager>

It will automatically inject the earlier JavaScript codes in the client side.

Handling Unhandled and Update Panel Exception

Previously you have to manually hook those events to log exceptions, now it will automatically log those exceptions, just ensure you did not turned off the HandleUnhandledException and HandleUpdatePanelException. You can also set the exception code from the server side .The Control will automatically inject the following code:

window.onerror = function(message, url, lineNumber)
{
    var e = Error.create(message, {description:message, name:'UnhandledError', lineNumber:lineNumber, url:url});
    Ajax.Logging.ExceptionManager.getInstance().publishException(105000, e);
    return true;
}

Sys.WebForms.PageRequestManager.getInstance().add_endRequest(AjaxLoggingHandleUpdatePanelError)
function AjaxLoggingHandleUpdatePanelError(sender, args)
{
    var e = args.get_error();
    if (e != null)
    {
        Ajax.Logging.ExceptionManager.getInstance().publishException(105001, e);
        args.set_errorHandled(true);
    }
}
Encapsulating Listener in Server Side Class

Now each of the listener is encapsulated in its own server side class. You can also roll your own listener from Inheriting the BaseListener class. For example, the following shows the code of AlertListener:

public class AlertListener : BaseListener
{
    [Browsable(false)]
    public override string CreateScript
    {
        [DebuggerStepThrough()]
        get
        {
            string script = string.Format(System.Globalization.CultureInfo.CurrentCulture, "Ajax.Logging.ExceptionManager.getInstance().addListener(new {0}());", this.GetType().FullName);

            return script;
        }
    }

    [Browsable(false)]
    public override IEnumerable<ScriptReference> ScriptReferences
    {
        [DebuggerStepThrough()]
        get
        {
            return new ScriptReference[] { new ScriptReference(Owner.Page.ClientScript.GetWebResourceUrl(this.GetType(), "Ajax.Logging.Scripts.AlertListener.js")) };
        }
    }
}

You have to override the CreateScript and ScriptReferences property which will contain the listener specific code. The client side rule to create listener remains the same.

Enhanced Web Service Listener

In previous version the exceptions were forwarded to a static web service and the web service was called each time the publishException is called, now it supports dynamic web service url as well as method name and it also queue the exceptions in the client side and submits as batch, the default interval is 10 seconds which you can change from the LogInterval property.

Built-in Web Service

Now it has a pre-built web service which you can use to log exceptions. Just add an asmx file and add the following line:

<%@ WebService Language="c#" Class="Ajax.Logging.ExceptionLogService, Ajax.Logging" %>

And in the web service listener set the path and the method name like the following:

<AjaxLogging:WebServiceListener ServicePath="~/ExceptionLogService.asmx" ServiceMethod="Log"  />
Provider Model

To record the exception in server side it uses the Asp.net Provider Model. To create a new provider you have create a class which inherits from ExceptionLogProvider and overriding the Log method.

Built-in Health Monitoring Provider

Now it comes with a Built-in Provider which uses the Asp.net health monitoring system to log the exceptions. It raises a custom event WebClientExceptionEvent to record the exception. Certainly the required settings for health monitoring needs to be present in the configuration file. The following shows a logged exception in the Event Log.

Event Log

You can download the full source and a working sample from codeplex.

A small note about Asp.net Future

The upcoming release of Asp.net known as Asp.net Future also has the support to log client side exceptions. But unfortunately it does not allow to log any other exceptions except the window.onerror, also it is not as much extensible as the above, no support for listeners, not automatic logging etc. You will find the details in this link.

kick it on DotNetKicks.com

So I got a new Blog. Like any other Asp.net developer, I also had the dream to have a blog in Asp.net and when I saw Joe Stagner calling all Asp.net experts to start blogging with Scott Guthrie I jumped right away to filling up the form. Now few things that are really bothering me:

  • Why it does not have any full screen theme, usually my post contains a fair amount of code and I am afraid that it won't fit in this half screen layout.

  • Could not find a way to import my older posts from my previous blog. Thanks that it supports feedburner at least my feed reader will not be bothered(?)

  • How can I disable the advertisement which shows in an awkward place, at least grant me the privilege to show it in an different place. Certainly, my previous blog has the advertisement but it was shown in much more controlled way so the initial focus of the reader was right on my post rather than the Ad.

My new article has been just published for September Ajax Column in DotNetSlackers.com. In this article, I have answered few common issues which I found often discussed in Asp.net Ajax Web Service Forum. In this column, I have covered:

  • Web Service Method, succeededCallback and failedCallback methods signature.
  • Complex data type interchange which includes Array, Dictionary, Custom Class.
    • GenerateScriptType Attribute
    • ScriptIgnore Attribute
    • Custom JavaScriptConverter class
  • Progress Indicator for long running task.
  • Soap Header/Custom Http Header

Check it out.

kick it on DotNetKicks.com

In this post, I will show you how to compress the Asp.net Ajax Web Service response, To understand the benefits of compression let us start with a simple example, Consider you have an web service which returns a large data like the following:

[WebMethod()]
public string GetLargeData()
{
    using (StreamReader sr = File.OpenText(Server.MapPath("~/DataFile.txt")))
    {
        return sr.ReadToEnd();
    }
}

The web method reads an large text file (around 100KB) and returns it contents. Once we call this method from a page the network activity in the firebug shows like the following:

Plain

Now, lets examine the HttpModule which compress the Ajax Web Service response. The following shows the complete code of this module:

using System;
using System.IO;
using System.IO.Compression;
using System.Globalization;
using System.Web;


public class JsonCompressionModule : IHttpModule
{
    public JsonCompressionModule()
    {
    }

    public void Dispose()
    {
    }

    public void Init(HttpApplication app)
    {
        app.PreRequestHandlerExecute += new EventHandler(Compress);
    }

    private void Compress(object sender, EventArgs e)
    {
        HttpApplication app = (HttpApplication)sender;
        HttpRequest request = app.Request;
        HttpResponse response = app.Response;

        //Ajax Web Service request is always starts with application/json
        if (request.ContentType.ToLower(CultureInfo.InvariantCulture).StartsWith("application/json"))
        {
            //User may be using an older version of IE which does not support compression, so skip those
            if (!((request.Browser.IsBrowser("IE")) && (request.Browser.MajorVersion <= 6)))
            {
                string acceptEncoding = request.Headers["Accept-Encoding"];

                if (!string.IsNullOrEmpty(acceptEncoding))
                {
                    acceptEncoding = acceptEncoding.ToLower(CultureInfo.InvariantCulture);

                    if (acceptEncoding.Contains("gzip"))
                    {
                        response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
                        response.AddHeader("Content-encoding", "gzip");
                    }
                    else if (acceptEncoding.Contains("deflate"))
                    {
                        response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
                        response.AddHeader("Content-encoding", "deflate");
                    }
                }
            }
        }
    }
}

Next, register this module in the web.config like the following:

<httpModules>
    <add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
    <add name="JsonCompressionModule" type="JsonCompressionModule"/>
</httpModules>

This time the network activity shows like the following:

Compressed Json

So by adding this little module, we have saved 74KB. Now consider the impact of this in an highly traffic ajax web application :-). You will find the complete source code in the bottom of this post. If you want to learn more optimization tips check out my previous post Implement Yahoo's YSlow in your Asp.net pages and Combine Multiple JavaScript and CSS Files and Remove Overheads.

Download: Full Source

kick it on DotNetKicks.com

In my previous post, I have shown the data binding capabilities of ListView control. In this post, I will show the Data Editing power of ListView. Like the previous DataGrid, DataList and GridView controls the ListView control has the EditItemTemplate and it also comes up with the long awaited InsertItemTemplate, in the past there was a lot of attempt to allow the users to insert data through Header or Footer Template which is no longer required. In this post I will show you the full data editing capabilities of ListView which includes Insert, Edit and Delete with both LinqDataSource and code. We will follow the previous post example for the data editing.

The first thing you have to do to add the data editing capabilities is defining the Edit item and Insert item templates. Instead of row kind of style editing like the DataGrid and GridView we will follow the form style editing, this is the another powerful features of ListView that you have the full control on how the html is generated. The following shows the screen-shots of both EditItemTemplate and InsertItemTemplate view:

EditItemTemplate

InsertItemTemplate

The following shows the EditItem and InsertItem Template:

<EditItemTemplate>
    <tr>
        <td colspan="9">
            <table>
                <tbody>
                    <tr>
                        <th>
                            Company:
                        </th>
                        <td>
                            <asp:TextBox ID="CompanyNameTextBox" runat="server" Text='<% #Bind("CompanyName")%>' ValidationGroup="edit"></asp:TextBox>
                            <asp:RequiredFieldValidator ID="CompanyValidator" runat="server" ControlToValidate="CompanyNameTextBox" ErrorMessage="<div>Company cannot be blank.</div>" SetFocusOnError="true" Display="Dynamic" ValidationGroup="edit"></asp:RequiredFieldValidator>
                        </td>
                    </tr>
                    <tr>
                        <th>
                            Contact:
                        </th>
                        <td>
                            <asp:TextBox ID="ContactNameTextBox" runat="server" Text='<% #Bind("ContactName")%>' ValidationGroup="edit"></asp:TextBox>
                            <asp:RequiredFieldValidator ID="ContactValidator" runat="server" ControlToValidate="ContactNameTextBox" ErrorMessage="<div>Contact cannot be blank.</div>" SetFocusOnError="true" Display="Dynamic" ValidationGroup="edit"></asp:RequiredFieldValidator>
                        </td>
                    </tr>
                    <tr>
                        <th>
                            Title:
                        </th>
                        <td>
                            <asp:TextBox ID="ContactTitleTextBox" runat="server" Text='<% #Bind("ContactTitle")%>'></asp:TextBox>
                        </td>
                    </tr>
                    <tr>
                        <th>
                            Address:
                        </th>
                        <td>
                            <asp:TextBox ID="AddressTextBox" runat="server" TextMode="MultiLine" Columns="20" Rows="4" Text='<% #Bind("Address")%>' ValidationGroup="edit"></asp:TextBox>
                            <asp:RequiredFieldValidator ID="AddressValidator" runat="server" ControlToValidate="AddressTextBox" ErrorMessage="<div>Address cannot be blank.</div>" SetFocusOnError="true" Display="Dynamic" ValidationGroup="edit"></asp:RequiredFieldValidator>
                        </td>
                    </tr>
                    <tr>
                        <th>
                            City:
                        </th>
                        <td>
                            <asp:TextBox ID="CityTextBox" runat="server" Text='<% #Bind("City")%>' ValidationGroup="edit"></asp:TextBox>
                            <asp:RequiredFieldValidator ID="CityValidator" runat="server" ControlToValidate="CityTextBox" ErrorMessage="<div>City cannot be blank.</div>" SetFocusOnError="true" Display="Dynamic" ValidationGroup="edit"></asp:RequiredFieldValidator>
                        </td>
                    </tr>
                    <tr>
                        <th>
                            Postal Code:
                        </th>
                        <td>
                            <asp:TextBox ID="PostalCodeTextBox" runat="server" Text='<% #Bind("PostalCode")%>' ValidationGroup="edit"></asp:TextBox>
                            <asp:RequiredFieldValidator ID="PostalCodeValidator" runat="server" ControlToValidate="PostalCodeTextBox" ErrorMessage="<div>Postal code cannot be blank.</div>" SetFocusOnError="true" Display="Dynamic" ValidationGroup="edit"></asp:RequiredFieldValidator>
                        </td>
                    </tr>
                    <tr>
                        <th>
                            Country:
                        </th>
                        <td>
                            <asp:TextBox ID="CountryTextBox" runat="server" Text='<% #Bind("Country")%>' ValidationGroup="edit"></asp:TextBox>
                            <asp:RequiredFieldValidator ID="CountryValidator" runat="server" ControlToValidate="CountryTextBox" ErrorMessage="<div>Country cannot be blank.</div>" SetFocusOnError="true" Display="Dynamic" ValidationGroup="edit"></asp:RequiredFieldValidator>
                        </td>
                    </tr>
                    <tr>
                        <th>
                            Phone:
                        </th>
                        <td>
                            <asp:TextBox ID="PhoneTextBox" runat="server" Text='<% #Bind("Phone")%>' ValidationGroup="edit"></asp:TextBox>
                            <asp:RequiredFieldValidator ID="PhoneValidator" runat="server" ControlToValidate="PhoneTextBox" ErrorMessage="<div>Phone cannot be blank.</div>" SetFocusOnError="true" Display="Dynamic" ValidationGroup="edit"></asp:RequiredFieldValidator>
                        </td>
                    </tr>
                    <tr>
                        <th></th>
                        <td>
                            <asp:LinkButton ID="UpdateButton" CommandName="Save" runat="server" Text="Update" ValidationGroup="edit"></asp:LinkButton>
                            <asp:LinkButton ID="CancelButton" CommandName="Cancel" runat="server" Text="Cancel" CausesValidation="false"></asp:LinkButton>
                        </td>
                    </tr>
                </tbody>
            </table>
        </td>
    </tr>
</EditItemTemplate>
<InsertItemTemplate>
    <tr>
        <td colspan="9">
            <table>
                <tbody>
                    <tr>
                        <th>
                            ID:
                        </th>
                        <td>
                            <asp:TextBox ID="CustomerIDTextBox" runat="server" Text='<% #Bind("CustomerID")%>' ValidationGroup="add"></asp:TextBox>
                            <asp:RequiredFieldValidator ID="IDValidator" runat="server" ControlToValidate="CustomerIDTextBox" ErrorMessage="<div>ID cannot be blank.</div>" SetFocusOnError="true" Display="Dynamic" ValidationGroup="add"></asp:RequiredFieldValidator>
                        </td>
                    </tr>
                    <tr>
                        <th>
                            Company:
                        </th>
                        <td>
                            <asp:TextBox ID="CompanyNameTextBox" runat="server" Text='<% #Bind("CompanyName")%>' ValidationGroup="add"></asp:TextBox>
                            <asp:RequiredFieldValidator ID="CompanyValidator" runat="server" ControlToValidate="CompanyNameTextBox" ErrorMessage="<div>Company cannot be blank.</div>" SetFocusOnError="true" Display="Dynamic" ValidationGroup="add"></asp:RequiredFieldValidator>
                        </td>
                    </tr>
                    <tr>
                        <th>
                            Contact:
                        </th>
                        <td>
                            <asp:TextBox ID="ContactNameTextBox" runat="server" Text='<% #Bind("ContactName")%>' ValidationGroup="add"></asp:TextBox>
                            <asp:RequiredFieldValidator ID="ContactValidator" runat="server" ControlToValidate="ContactNameTextBox" ErrorMessage="<div>Contact cannot be blank.</div>" SetFocusOnError="true" Display="Dynamic" ValidationGroup="add"></asp:RequiredFieldValidator>
                        </td>
                    </tr>
                    <tr>
                        <th>
                            Title:
                        </th>
                        <td>
                            <asp:TextBox ID="ContactTitleTextBox" runat="server" Text='<% #Bind("ContactTitle")%>'></asp:TextBox>
                        </td>
                    </tr>
                    <tr>
                        <th>
                            Address:
                        </th>
                        <td>
                            <asp:TextBox ID="AddressTextBox" runat="server" Text='<% #Bind("Address")%>' TextMode="MultiLine" Columns="20" Rows="4" ValidationGroup="add"></asp:TextBox>
                            <asp:RequiredFieldValidator ID="AddressValidator" runat="server" ControlToValidate="AddressTextBox" ErrorMessage="<div>Address cannot be blank.</div>" SetFocusOnError="true" Display="Dynamic" ValidationGroup="add"></asp:RequiredFieldValidator>
                        </td>
                    </tr>
                    <tr>
                        <th>
                            City:
                        </th>
                        <td>
                            <asp:TextBox ID="CityTextBox" runat="server" Text='<% #Bind("City")%>' ValidationGroup="add"></asp:TextBox>
                            <asp:RequiredFieldValidator ID="CityValidator" runat="server" ControlToValidate="CityTextBox" ErrorMessage="<div>City cannot be blank.</div>" SetFocusOnError="true" Display="Dynamic" ValidationGroup="add"></asp:RequiredFieldValidator>
                        </td>
                    </tr>
                    <tr>
                        <th>
                            Postal Code:
                        </th>
                        <td>
                            <asp:TextBox ID="PostalCodeTextBox" runat="server" Text='<% #Bind("PostalCode")%>' ValidationGroup="add"></asp:TextBox>
                            <asp:RequiredFieldValidator ID="PostalCodeValidator" runat="server" ControlToValidate="PostalCodeTextBox" ErrorMessage="<div>Postal code cannot be blank.</div>" SetFocusOnError="true" Display="Dynamic" ValidationGroup="add"></asp:RequiredFieldValidator>
                        </td>
                    </tr>
                    <tr>
                        <th>
                            Country:
                        </th>
                        <td>
                            <asp:TextBox ID="CountryTextBox" runat="server" Text='<% #Bind("Country")%>' ValidationGroup="add"></asp:TextBox>
                            <asp:RequiredFieldValidator ID="CountryValidator" runat="server" ControlToValidate="CountryTextBox" ErrorMessage="<div>Country cannot be blank.</div>" SetFocusOnError="true" Display="Dynamic" ValidationGroup="add"></asp:RequiredFieldValidator>
                        </td>
                    </tr>
                    <tr>
                        <th>
                            Phone:
                        </th>
                        <td>
                            <asp:TextBox ID="PhoneTextBox" runat="server" Text='<% #Bind("Phone")%>' ValidationGroup="add"></asp:TextBox>
                            <asp:RequiredFieldValidator ID="PhoneValidator" runat="server" ControlToValidate="PhoneTextBox" ErrorMessage="<div>Phone cannot be blank.</div>" SetFocusOnError="true" Display="Dynamic" ValidationGroup="add"></asp:RequiredFieldValidator>
                        </td>
                    </tr>
                    <tr>
                        <th></th>
                        <td>
                            <asp:LinkButton ID="InsertButton" CommandName="Insert" runat="server" Text="Save" ValidationGroup="add"></asp:LinkButton>
                            <asp:LinkButton ID="CancelButton" CommandName="Cancel" runat="server" Text="Cancel" CausesValidation="false"></asp:LinkButton>
                        </td>
                    </tr>
                </tbody>
            </table>
        </td>
    </tr>
</InsertItemTemplate>

To edit existing record, you have to put a Button in the ItemTemplate/AlternatingItemTemplate which commandName is set to Edit, you can also use ImageButton/LinkButton instead of regular button as in this example I am using LinkButton. The same holds true for deleting record, in this case the commandName has to be Delete. Next, in the EditItemTemplate you have to add buttons which commandName sets to Update and Cancel.

To add record, you must have to set the InsertItemTemplate position, the supported values are FirstItem, LastItem and None. Since we are using form view for adding record it does not look appealing showing it all the time, instead we will put a button in the LayoutTemplate, upon click the form view will appear at the end. Next, in the InsertItemTemplate two new buttons are required which commandName sets to Insert and Cancel. At last, as we are showing this template on the new button click, we need to put few lines of code to show/hide the template as well as the new button. The following shows the code:

private void CloseInsert()
{
    lvwCustomers.InsertItemPosition = InsertItemPosition.None;
    ((LinkButton) lvwCustomers.FindControl("NewButton")).Visible = true;
}

protected void NewButton_Click(object sender, EventArgs e)
{
    lvwCustomers.EditIndex = -1;
    lvwCustomers.InsertItemPosition = InsertItemPosition.LastItem;
    ((LinkButton) sender).Visible = false;
}

protected void lvwCustomers_ItemEditing(object sender, ListViewEditEventArgs e)
{
    CloseInsert();
}

protected void lvwCustomers_ItemCanceling(object sender, ListViewCancelEventArgs e)
{
    if (e.CancelMode == ListViewCancelMode.CancelingInsert)
    {
        CloseInsert();
    }
}

protected void lvwCustomers_ItemInserted(object sender, ListViewInsertedEventArgs e)
{
    CloseInsert();
}

That's it we are all set to Add/Edit/Delete the records with LinqDataSource control.

Now we will see how to do the same with code. It is almost same as the LinqDataSource except few things, we need to hook the ItemCommand event and add few empty event handlers, the following shows the code:

protected void lvwCustomers_ItemCommand(object sender, ListViewCommandEventArgs e)
{
    switch(e.CommandName)
    {
        case "Insert":
        {
            InsertCustomer(e.Item);
            break;
        }
        case "Update":
        {
            UpdateCustomer(e.CommandArgument as string, e.Item);
            break;
        }
        case "Delete":
        {
            DeleteCustomer(e.CommandArgument as string);
            break;
        }
    }
}

protected void lvwCustomers_ItemInserting(object sender, ListViewInsertEventArgs e)
{
}

protected void lvwCustomers_ItemEditing(object sender, ListViewEditEventArgs e)
{
    CloseInsert();
    lvwCustomers.EditIndex = e.NewEditIndex;
    BindList();
}

protected void lvwCustomers_ItemUpdating(object sender, ListViewUpdateEventArgs e)
{
}

protected void lvwCustomers_ItemCanceling(object sender, ListViewCancelEventArgs e)
{
    if (e.CancelMode == ListViewCancelMode.CancelingInsert)
    {
        CloseInsert();
    }
    else
    {
        lvwCustomers.EditIndex = -1;
    }

    BindList();
}

protected void lvwCustomers_ItemDeleting(object sender, ListViewDeleteEventArgs e)
{
}

As you can see that in ItemCommand event we are checking the commandName and based upon that we are calling the helper function which does the actual db operation. The reason behind having those empty event handlers like ItemInserting, ItemUpdating and ItemDeleting is, if those left unhandled you will get a nasty exception that the event was not handled. While playing with the ListView data editing my first try was these events to do the db operation instead of the ItemCommand, but found that the event argument always left empty(except the itemIndex) as well as the EditItem and InsertItem property of ListView if the ListView is not bind with any DataSource control.

Now lets see what those db operation functions does, the following shows the code:

private void InsertCustomer(ListViewItem insertItem)
{
    if (IsValid)
    {
        using (NorthwindDataContext db = new NorthwindDataContext())
        {
            Customer customer = new Customer();

            customer.CustomerID = ((TextBox)insertItem.FindControl("CustomerIDTextBox")).Text;
            customer.CompanyName = ((TextBox)insertItem.FindControl("CompanyNameTextBox")).Text;
            customer.ContactName = ((TextBox)insertItem.FindControl("ContactNameTextBox")).Text;
            customer.ContactTitle = ((TextBox)insertItem.FindControl("ContactTitleTextBox")).Text;
            customer.Address = ((TextBox)insertItem.FindControl("AddressTextBox")).Text;
            customer.City = ((TextBox)insertItem.FindControl("CityTextBox")).Text;
            customer.PostalCode = ((TextBox)insertItem.FindControl("PostalCodeTextBox")).Text;
            customer.Country = ((TextBox)insertItem.FindControl("CountryTextBox")).Text;
            customer.Phone = ((TextBox)insertItem.FindControl("PhoneTextBox")).Text;

            db.Customers.Add(customer);

            db.SubmitChanges();
            CloseInsert();
            BindList();
        }
    }
}

private void UpdateCustomer(string customerID, ListViewItem editItem)
{
    if (IsValid)
    {
        using (NorthwindDataContext db = new NorthwindDataContext())
        {
            Customer customer = db.Customers.Where(c => c.CustomerID == customerID).Single();

            customer.CompanyName = ((TextBox) editItem.FindControl("CompanyNameTextBox")).Text;
            customer.ContactName = ((TextBox)editItem.FindControl("ContactNameTextBox")).Text;
            customer.ContactTitle = ((TextBox)editItem.FindControl("ContactTitleTextBox")).Text;
            customer.Address = ((TextBox)editItem.FindControl("AddressTextBox")).Text;
            customer.City = ((TextBox)editItem.FindControl("CityTextBox")).Text;
            customer.PostalCode = ((TextBox)editItem.FindControl("PostalCodeTextBox")).Text;
            customer.Country = ((TextBox)editItem.FindControl("CountryTextBox")).Text;
            customer.Phone = ((TextBox)editItem.FindControl("PhoneTextBox")).Text;

            db.SubmitChanges();
            lvwCustomers.EditIndex = -1;
            BindList();
        }
    }
}

private void DeleteCustomer(string customerID)
{
    using(NorthwindDataContext db = new NorthwindDataContext())
    {
        Customer customer = db.Customers.Where(c => c.CustomerID == customerID).Single();
        db.Customers.Remove(customer);
        db.SubmitChanges();
    }

    Pager dpCustomers = (Pager)lvwCustomers.FindControl("dpCustomers");
    dpCustomers.CurrentPageNo = 1;

    BindList();
}

That's it. You will find the complete source code of both of this example in the bottom of this post. In my next post, I will show the new GroupTemplate of ListView.

Download: Full Source

kick it on DotNetKicks.com

In my previous post, I have presented few cool css examples of the new Asp.net ListView control. In this post, I will show how you can setup the data binding of the ListView control with the new LinqDataSource control as well as through code, which outputs the previous post examples.

Like its ancestor, the ListView comes up with few templates and some of them are really new. In order to bind data we first have to define the LayoutTemplate. The LayoutTemplate is a new template that acts as a container for either group or item templates. Since our previous example presents the data in tabular format, we will exclude the GroupTemplate from discussing in this post. One of the required thing you have to keep in mind while defining the LayoutTemplate is, you must have to declare an itemContainer/groupContainer where the item/group will be repeated. For example, I have declared the tbody as the itemContainer like the following:

<LayoutTemplate>
    <table>
        <thead>
            <tr>
                <th>Company</th>
                <th>Contact</th>
                <th>Title</th>
                <th>Address</th>
                <th>City</th>
                <th>Postal Code</th>
                <th>Country</th>
                <th>Phone</th>
            </tr>
        </thead>
        <tbody id="itemContainer" runat="server"></tbody>
        <tfoot>
            <tr>
                <th style="text-align:right" colspan="8">
                    <asp:DataPager runat="server" ID="dpCustomers" PageSize="10">
                        <Fields>
                            <asp:NumericPagerField ButtonCount="5"/>
                        </Fields>
                    </asp:DataPager>
                </th>
            </tr>
        </tfoot>
    </table>
</LayoutTemplate>

As you can see, the LayoutTemplate also contains another new control DataPager that I will discuss later. Another important thing is that unlike the Repeater control where usually the HeaderTemplate contains the starting tag and the FooterTemplate contains the ending tag of the container , the LayoutTemplate contains the both except the repeating part.

Next, we have to define the ItemTemplate and the AlteringItemTemplate like the following:

<ItemTemplate>
    <tr>
        <td>
            <asp:Label ID="lblCompany" runat="server" Text='<% #Eval("CompanyName")%>'></asp:Label>
        </td>
        <td>
            <asp:Label ID="lblContact" runat="server" Text='<% #Eval("ContactName")%>'></asp:Label>
        </td>
        <td>
            <asp:Label ID="lblTitle" runat="server" Text='<% #Eval("ContactTitle")%>'></asp:Label>
        </td>
        <td>
            <asp:Label ID="lblAddress" runat="server" Text='<% #Eval("Address")%>'></asp:Label>
        </td>
        <td>
            <asp:Label ID="lblCity" runat="server" Text='<% #Eval("City")%>'></asp:Label>
        </td>
        <td>
            <asp:Label ID="lblPostalCode" runat="server" Text='<% #Eval("PostalCode")%>'></asp:Label>
        </td>
        <td>
            <asp:Label ID="lblCountry" runat="server" Text='<% #Eval("Country")%>'></asp:Label>
        </td>
        <td>
            <asp:Label ID="lblPhone" runat="server" Text='<% #Eval("Phone")%>'></asp:Label>
        </td>
    </tr>
</ItemTemplate>
<AlternatingItemTemplate>
    <tr class="odd">
        <td>
            <asp:Label ID="lblCompany" runat="server" Text='<% #Eval("CompanyName")%>'></asp:Label>
        </td>
        <td>
            <asp:Label ID="lblContact" runat="server" Text='<% #Eval("ContactName")%>'></asp:Label>
        </td>
        <td>
            <asp:Label ID="lblTitle" runat="server" Text='<% #Eval("ContactTitle")%>'></asp:Label>
        </td>
        <td>
            <asp:Label ID="lblAddress" runat="server" Text='<% #Eval("Address")%>'></asp:Label>
        </td>
        <td>
            <asp:Label ID="lblCity" runat="server" Text='<% #Eval("City")%>'></asp:Label>
        </td>
        <td>
            <asp:Label ID="lblPostalCode" runat="server" Text='<% #Eval("PostalCode")%>'></asp:Label>
        </td>
        <td>
            <asp:Label ID="lblCountry" runat="server" Text='<% #Eval("Country")%>'></asp:Label>
        </td>
        <td>
            <asp:Label ID="lblPhone" runat="server" Text='<% #Eval("Phone")%>'></asp:Label>
        </td>
    </tr>
</AlternatingItemTemplate>

At last, we will add a LinqDataSource control which will refer the Customers table of Northwind database. Certainly you have to add a LinqToSQLClasses previously and define the data model. The following shows the LinqDataSource:

<asp:LinqDataSource ID="linqCustomers" runat="server" ContextTypeName="NorthwindDataContext" TableName="Customers"/>

One of the benefits of using LinqDataSource over the other DataSource controls is it produces very optimized SQL without writing a single line of code. For example, the following SQL is generated when I moved to page two by clicking the DataPager.

exec sp_executesql N'SELECT TOP 10 [t1].[CustomerID], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Address], [t1].[City], [t1].[Region], [t1].[PostalCode], [t1].[Country], [t1].[Phone], [t1].[Fax]
FROM (
    SELECT ROW_NUMBER() OVER (ORDER BY [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax]) AS [ROW_NUMBER], [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax]
    FROM [dbo].[Customers] AS [t0]
    ) AS [t1]
WHERE [t1].[ROW_NUMBER] > @p0',N'@p0 int',@p0=10

Like the other data controls, both the ListView and DataPager are tightly coupled with the DataSource controls.

So far we have seen how to bind data with the DataSource Control, now lets see how to bind data with code. The first issue you will face while binding pageable data that like the GridView built-in paging, there is no way you can utilize the DataPager control unless you use the DataSource controls. So you have to roll your own pager, in this example I have substitute the DataPager control with the server side version of my AjaxDataControls pager. Thus the LayoutTemplate becomes:

<LayoutTemplate>
    <table>
        <thead>
            <tr>
                <th>Company</th>
                <th>Contact</th>
                <th>Title</th>
                <th>Address</th>
                <th>City</th>
                <th>Postal Code</th>
                <th>Country</th>
                <th>Phone</th>
            </tr>
        </thead>
        <tbody id="itemContainer" runat="server"></tbody>
        <tfoot>
            <tr>
                <th colspan="8" style="text-align:right">
                    <cc1:Pager ID="pager" runat="server" OnPageChange="pager_PageChange" SliderSize="5" ShowFirstAndLast="false" ShowPreviousAndNext="true"/>
                </th>
            </tr>
        </tfoot>
    </table>
</LayoutTemplate>

The next issue you will face once you put the pager in the LayoutTemplate is, you will not be able to get the reference of the control by FindControl() until you bind the data of the ListView. So we have to first bind with a dummy data to get the reference, once done we will bind with the original data like the following:

private void BindList()
{
    using (NorthwindDataContext db = new NorthwindDataContext())
    {
        //Unless we bind the data we will not be able to refer the pager control
        //so bind it with a dummy data
        lvwCustomers.DataSource = new Customer[] { new Customer() };
        lvwCustomers.DataBind();

        Pager pager = (Pager)lvwCustomers.FindControl("pager");
        //Calculate the startIndex based upon the Current and RowPerPage
        int startIndex = (pager.CurrentPageNo - 1) * pager.RowPerPage;

        //Now bind it with the actual data.
        lvwCustomers.DataSource = db.Customers.Skip(startIndex).Take(pager.RowPerPage);
        lvwCustomers.DataBind();

        //We also need to know the total row in order to properly render the pager
        pager.TotalRow = db.Customers.Count();
    }
}

Next we have to handle the PageChange event of the pager to render the new page records. The following shows the code of this event:

protected void pager_PageChange(object sender, PageChangeEventArgs e)
{
    ((Pager)sender).CurrentPageNo = e.PageNo;
    BindList();
}

That's it. You will find the full source code of both these example in the bottom of this post. In my next post, I will show how we can leverage the data editing capabilities of ListView control.

Download: Full Source

kick it on DotNetKicks.com

More Posts Next page »