UpdatePanel-based ASP.NET AJAX Application

Sunday, 28 December 2014

UpdatePanel-based ASP.NET AJAX Application


Design for Change in UpdatePanel-based ASP.NET AJAX Applications


UpdatePanel Fundamentals and Problems

The AJAX capabilities of ASP.NET AJAX Framework 1.0 mainly come out of two great server controls ScriptManager and UpdatePanel.
The non-visual component ScriptManager is responsible for the overall AJAX plumbing.
UpdatePanel gives developers direct control on which part needs to be partially updated and what element can trigger asynchronous postbacks.
 Looking at a simple AJAX application (Listing 1), the use of ScriptManager and UpdatePanel is very easy and straightforward.
An UpdatePanel consists of two sections: ContentTemplate and Triggers. All the content in the ContentTemplate section is able to get updated asynchronously.
 The Triggers section allows you to register the triggers which will update the content of the UpdatePanel asynchronously.
UpdatePanel has one Boolean property called ChildrenAsTriggers. When it is turned on, all triggers in the content section automatically
 become asynchronous triggers of that UpdatePanel.

There are a few issues you need to be clear about when you use UpdatePanel in your ASP.NET AJAX applications:

Determine the area in the Web Form that needs to be updated asynchronously. This is very simple: just wrap that area with an UpdatePanel control.

Specify the asynchronous triggers. You can declaratively register the trigger in the Triggers section.

Asynchronous postback event handling. You can handle the asynchronous postback events the same way as the normal postback.

Some controls like Repeater, GridView, and UserControl can also act as asynchronous triggers for the UpdatePanel with their specific or custom events
such as ItemCommandEvent.

In addition to the declarative approach, ASP.NET AJAX also allows you to register asynchronous triggers and update multiple UpdatePanels programmatically.

You may cheer for such a powerful yet simple AJAX framework. However, things in the real world turn out to be not that simple. If you have a Web Form with
a dozen UpdatePanels (some possibly nested) and plenty of asynchronous triggers, you will have a totally different view of the situation: you will find
that the advantages above may turn into headaches if you’ve used these techniques in a casual way. Your application ends up being hard to maintain and
change. The following list gives you the problematic issues around UpdatePanel-based AJAX applications within large-scale AJAX applications:

The declarative approach may not work all the time; for example, you cannot register a nested trigger if it has no instance for reference.
You must resort to a programmatic approach as a remedy. However, you will find that you cannot use either approach in a consistent manner.
You can use a trigger to update more than one UpdatePanel; an UpdatePanel can get updated via more than one trigger.
The declarative approach cannot give you a centralized view of how UpdatePanel and triggers are related.
Event handling is a clean and quick way to handle asynchronous postback events. But this may not work well for Web Forms containing many asynchronous
triggers with their corresponding dozen separate event handlers.
The use of complex triggers as asynchronous triggers, like Repeater, GridView, and UserControl, makes the code logic complicated and difficult to understand.

Lacking guidelines and best practices, the inadvertent and ad hoc use of ASP.NET AJAX technology inevitably ends up with an application where it is
hard to make changes.

Is there a best practice to get around the issues above? The answer is yes. In the rest of this article I will outline the new approach to systematically
resolving these issues. First of all, I will show how to loosely couple UpdatePanels and asynchronous triggers at design time. Next,
 I will show you how to specify UpdatePanels and asynchronous triggers in a consistent manner, avoid complex triggers, and minimize the number of
 UpdatePanels. Last, I will illustrate how to link asynchronous triggers with related UpdatePanels and how to centralize the handling of the asynchronous
 postback events.

Decouple Asynchronous Triggers and UpdatePanels at Design Time
Decoupling asynchronous triggers and UpdatePanels at design time is a preliminary step to consistently specifying UpdatePanels and asynchronous triggers
 and centralizing the handling of asynchronous postback events. Linking asynchronous triggers with their related UpdatePanels still happens,
 but programmatically in the code. Decoupling enables you to specify the UpdatePanels and asynchronous triggers independently of each other.

It turns out that decoupling is very easy. First of all, turn off the Boolean property ChildrenAsTriggers for all UpdatePanels. This means that
asynchronous triggers in the ContentTemplate section of the UpdatePanel will not update a nested UpdatePanel. Second, don’t register triggers in the
 Triggers section of the UpdatePanel (there is only one exception, which I will discuss in the next section on avoiding unnecessary UpdatePanels).
 Next, you will learn a couple of simple ways to make asynchronous triggers.


Rule of Thumb-Use UpdatePanel
By design, any triggers residing at the ContentTemplate section automatically become asynchronous triggers, for example, Button, LinkButton, etc.
This works for nested UpdatePanels, UserControls within an UpdatePanel, and iterative controls such as Repeater or GridView within an UpdatePanel.
 For example, if you want to make a Button in the ItemTemplate of a Repeater an asynchronous trigger, you can wrap the whole Repeater into an UpdatePanel;
 or you can add UpdatePanel in the ItemTemplate. The latter allows you to fine tune the control instead of making asynchronous triggers since the former
 method transforms any other triggers into asynchronous triggers as a side effect. Remember to turn off ChildrenAsTriggers for all the UpdatePanels.

Avoid Unnecessary UpdatePanels
The overuse of UpdatePanel to specify asynchronous triggers may result in too many trivial UpdatePanels in the page. This could be an issue for maintenance.
 To mitigate this problem you can create a dummy UpdatePanel in the page without any content and register all page-level asynchronous triggers in its
Triggers section:

<asp:UpdatePanel ID="upTrigger" runat="server"
 UpdateMode="Conditional" RenderMode="Inline"
 ChildrenAsTriggers="false">
        <Triggers>
            <asp:AsyncPostBackTrigger
ControlID ="ddlStartYear"
EventName ="SelectedIndexChanged" />
            <asp:AsyncPostBackTrigger
ControlID ="btnSearch" EventName ="Click" />
            <asp:AsyncPostBackTrigger
ControlID ="btnSave" EventName ="Click" />
            <asp:AsyncPostBackTrigger
ControlID ="btnSort" EventName ="Click" />
            ………
        </Triggers>
</asp:UpdatePanel>
This is the only place to register asynchronous triggers in the Triggers section of the UpdatePanel. By doing so, you can greatly reduce the number of
UpdatePanels.

Remember to turn off the Boolean property ChildrenAsTriggers for all UpdatePanels.

Avoid Complex Triggers and Custom Events
ScriptManager has a useful property called AsynchronousPostbackElementID, the client ID of the asynchronous trigger, which reveals which element
in the page produces the asynchronous postback event. You will find that even if you use complex triggers as asynchronous triggers
 AsynchronousPostbackElementID still represents the client ID of the simple controls such as Button, LinkButton, DropDownList, RadioButton,
 RadioButtonList, etc, which actually produces the asynchronous postbacks. To make code easier to understand, my approach avoids using controls
 such as asynchronous triggers. In the next section you will see some samples of AsynchronousPostbackElementID.

Centralize the Handling of Asynchronous Postback Events

AsynchronousPostBackElementID contains all the information needed to identify the element in the Web Form triggering the asynchronous postback.

AsynchronousPostBackElementID looks like the following:

btnInOuterPanel //button
chkTest //check box
rbTest //radio button
rblTest$0 //radio button list
btnInInnerPanel //button inside UpdatePanel
//trigger button in the ItemTemplate of a Repeater
//Repeater is registered as the async trigger
rptComplextTrigger$ctl00$btnInComplextTrigger
//button in the ItemTemplate of a Repeater
//which is in an UpdatePanel
rptWithUpTriggerWrapper$ctl01$btnTest
//radio button list is in an UpdatePanel which is
// in the ItemTemplate of a Repeater
rptUpTriggerWrapperIndivisually$ctl00$rblTest2$2
//button inside the UserControl
WebUserControl1$btnUserControl
Mostly AsynchronousPostBackElementID is the same as the ID of that control if it is at the page level. When the asynchronous trigger is nested in other
 controls, for example, rptWithUpTriggerWrapper$ctl01$btnTest, you need to traverse the control tree to locate the instance of the asynchronous trigger.
AsynchronousPostBackElementID may end with the ID of the asynchronous trigger, but for list controls such as RadioButtonList, CheckBoxList, and so on,
 AsynchronousPostBackElementID ends with an extra sequence number, for example, rptUpTriggerWrapperIndivisually$ctl00$rblTest2$2.

You can also apply this method to the normal postback events, where having too many separate event handlers is a problem. However, you need to identify
 which element is producing the postback in the first place.

You can use simple string matching to identify the triggers. For example, the TriggerIs method below gives you an example of checking the leading control ID
 and the tailing control ID to identify the asynchronous trigger. The Boolean parameter skipTailingSequenceNo indicates whether the function will remove
the tailing sequence number as required for list controls (DropDownList control is an exception):

public static bool TriggerIs(string
 trggierClientId, Control startControl, string
triggerId, bool skipTailingSequenceNo)
{
    string trimTriggerClientId = trggierClientId;
    if (skipTailingSequenceNo)
    {
        int pos =
trggierClientId.LastIndexOf("$");
        if (pos >= 0)
        {
            trimTriggerClientId =
trggierClientId.Substring(0, pos);
        }
     }
     return
trimTriggerClientId.StartsWith
(startControl.ClientID) &&
trimTriggerClientId.EndsWith(triggerId);
}
Now the centralized processing logic for all the asynchronous postback events becomes very straightforward (Listing 2).

In Listing 2, the overridable method OnPreRenderComplete is a good location to host all the logic. You can also apply this method to the normal postback
events, where having too many separate event handlers is a problem. However, you need to identify which element is producing the postback in the first place.



Link Asynchronous Triggers and UpdatePanels Programmatically

The final step is to link asynchronous triggers with their related UpdatePanels, as shown in the centralized processing logic above. If the related
UpdatePanel has an instance at the page level, you can directly reference that instance. If the UpdatePanel is nested in other controls without a
page-level instance you need to traverse the control tree to obtain the instance of the asynchronous trigger and its related UpdatePanel, and then
reference them. Searching the Internet will give you a ton of examples on how to traverse the control tree of the Web Form. In addition,
AsynchronousPostBackElementID also provides the hierarchical information on the instance of the asynchronous trigger. The function LookupAsyncElement
illustrates how to find out the instance of the asynchronous postback element inside a complex control (Listing 3).

Searching the Internet will give you a ton of examples on how to traverse the control tree of the Web Form.

For example, to get the containing UpdatePanel of a RadioButtonList inside a Repeater control, you just call asyncTrigger.Parent.Parent since the
direct Parent refers to the ContentTemplate (Listing 4).

Wrapping Up

So now that I’ve finished outlining this approach, you may realize how it can simplify the issues around making changes to a complex AJAX application.
 This approach is a selective and restrictive use of existing ASP.NET AJAX techniques, but it makes an AJAX application loosely coupled, simplified,
consistent, and straightforward. It may not be very advantageous for a small AJAX application with one or two asynchronous triggers, but it will save
you trouble and headaches in large-scale AJAX applications. For these applications, you should now understand what to change and where to make changes.
 I developed this approach throughout the development of two large-scale AJAX applications. For the first one I used the techniques from books and the
Internet, but the application ended up being messed up and hard to maintain and change. The side benefit is that it inspired me to create this approach.
 So in the second application I applied this approach and the application ended up being very successful. Since the application’s inception, there have
been a lot of changes; all changes were done with ease. I hope you will enjoy this approach.

Event Handlers or If-else Statements

Developers should avoid making a lengthy list of dispatching logic using if-else or switch statements. Several design patterns exist to transform the
long switch statements into more elegant code. You can consider using the built-in Syntactic sugar as the implementation of an observer pattern to
resolve this issue. However, in a Web Form centralizing all the event handling in one location, using the if-else switch statement is a near-perfect
 fit since you don’t want that simple and straightforward logic to scatter in multiple different places, which creates difficulties when you want to
 make changes or do maintenance.

Listing 1: A simple AJAX Web form.
<%@ Page Language="C#" AutoEventWireup="true"
CodeFile ="SimpleAjaxForm.aspx.cs" Inherits ="SimpleAjaxForm" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Simple AJAX Form</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:ScriptManager ID="ScriptManager1" runat="server" />
        <asp:UpdatePanel ID="UpdatePanel1" runat="server"
         ChildrenAsTriggers="false">
            <ContentTemplate>
                Current time is : <%= DateTime.Now.ToString() %>
                <br />
                <asp:Button ID="btnRefreshInternal" runat="server"
                 Text="Internal Refresh" />
            </ContentTemplate>
            <Triggers>
                <asp:AsyncPostBackTrigger ControlID="btnRefresh"
                 EventName="Click" />
            </Triggers>
        </asp:UpdatePanel>
        <br />
        <asp:Button ID="btnRefresh" runat="server"
         Text="Refresh" />
    </div>
    </form>
</body>
</html>
   
Listing 2: Centralized processing logic for asynchronous postback events.
protected override void OnPreRenderComplete(EventArgs e)
{
    if (ScriptManager1.IsInAsyncPostBack)
    {
        string el = ScriptManager1.AsyncPostBackSourceElementID;
        //this.lblInfo.Text = el;
   
        if (el == btnInInnerPanel.ClientID)
        {
            //hook code here
            //update panels
            upInner.Update();
   
        }else if (el == btnInOuterPanel.ClientID)
        {
            //hook code here
            //update panels
            upOuter.Update();
        }else if
(TriggerIs(ScriptManager1.AsyncPostBackSourceElementID,
rptComplextTrigger, "btnTest", false))
        {
            //hook code here
            //update panels
        }
        else if
(TriggerIs(ScriptManager1.AsyncPostBackSourceElementID,
rptUpTriggerWrapperIndivisually, "rblTest2", true))
        {
            //hook code here
            //update panels
   
            //Find out the containing UpdatePanel
            Control con =
LookupAsyncElement(rptUpTriggerWrapperIndivisually, el, true);
            UpdatePanel up = con.Parent.Parent as UpdatePanel;
        }
    }
   
    base.OnPreRenderComplete(e);
}
Listing 3: The LookupAsyncElement function.
public static Control LookupAsyncElement(Control startControl,
string asyncPostbackElementId, bool skipTailingSequenceNo)
{
    string[] idArray = asyncPostbackElementId.Split(new string[]
             { "$" }, StringSplitOptions.RemoveEmptyEntries);
    Control tmpControl = startControl;
    Control con = null;
    int loopLength = skipTailingSequenceNo ?
        idArray.Length - 1 : idArray.Length;
    //skip the start control
    for (int kk = 1; kk < loopLength; kk++)
    {
        con = tmpControl.FindControl(idArray[kk]);
        if (con == null)
        {
            return null;
        }
        else
        {
            tmpControl = con;
        }
     }
     return con;
}
Listing 4: Using asyncTrigger.Parent.Parent to identify the containing UpdatePanel.
<asp:Repeater ID="rptUpTriggerWrapperIndivisually" runat="server">
    <ItemTemplate>
        <asp:UpdatePanel ID="upRptTriggerWrapper" runat="server"
         ChildrenAsTriggers="false" RenderMode="inline"
         UpdateMode="conditional">
            <ContentTemplate>
                <asp:RadioButtonList ID="rblTest2" runat="server"
                 AutoPostBack="true">
                    <asp:ListItem Value="111">111</asp:ListItem>
                    <asp:ListItem Value="222">222</asp:ListItem>
                    <asp:ListItem Value="333">333</asp:ListItem>
                </asp:RadioButtonList>
                <asp:Button ID="btnWrapperIndivisually" Text="Test"
                 runat="server" />
            </ContentTemplate>
        </asp:UpdatePanel>
    </ItemTemplate>
</asp:Repeater>
   
// Find out the containing UpdatePanel of the RadioButtonList
Control con = LookupAsyncElement(rptUpTriggerWrapperIndivisually,
 el, true);
UpdatePanel up = con.Parent.Parent as UpdatePanel;