Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Load and Display Page Contents Asynchronously with Full Postback Support

4.87/5 (72 votes)
14 Sep 2010Ms-PL9 min read 4   4.3K  
An AJAX UpdatePanel with less communication overhead and better performance
Image 1

Introduction

I was very interested in reading the article, Incremental Page Display Pattern for User Controls [^] by Acoustic. He built a solution for real asynchronous loading of partial content. His solution was good but had one weakness: he was unable to react to postbacks. Therefore, controls were static. They could only display initial content, and users were unable to interact with the controls. I decided to write a completely new control that could support the following features:

  • Developers would not even have to write one line of JavaScript.
  • Loads user controls (*.ascx) really asynchronously.
  • The user should be able to interact with the user controls.
  • It should not be necessary to render the whole page on updates, like the AJAX UpdatePanel does. To save server resources, only the user control should be rendered.
  • There should only be a small communication overhead. That means that only a part of the viewstate should be transferred back to the server, that belongs to the user control. AJAX UpdatePanel transfers the viewstate of the whole page and all the controls back to the server, which could be a large one.
  • The control should support some minor features like auto updating every n milliseconds.

Usage Scenarios

Example scenarios where you could use the PartialUpdatePanel instead of the AJAX UpdatePanel might be:

  • Autonomous sections of your page that require PostBack-support but not the environment information of the whole page (e.g. data lists with paging support where the user can browse through news, feeds, mails, etc.)
  • User feedback when your control has to complete long operations. In this case, use a PartialUpdatePanel with render method "Clientside". The surrounding page will be displayed with a waiting message. The user gets feedback that something is going on he has to wait for.

Basic Concepts

The complete control consists of a server-side control, a client-side script part, and an HttpHandler. The rendering scenario of an ASCX control is described in the following image:

Image 2

  1. The ASPX Page contains a PartialUpdatePanel which has its UserControlPath property set to the ASCX control.

    ASP.NET
    <iucon:PartialUpdatePanel runat="server" ID="Panel1"
    
                 UserControlPath="~/PostBackSample.ascx">
        <ErrorTemplate>
            Unable to refresh content
        </ErrorTemplate>
    
        <LoadingTemplate>
            <div style="margin-left: 84px; margin-top: 10px;">
    
              <asp:Image ID="Image1" runat="server"
                         ImageUrl="~/images/loading.gif" />
    
            </div>
            <div style="text-align: center">
    
                Updating...
            </div>
        </LoadingTemplate>
    </iucon:PartialUpdatePanel>
  2. When the page loads, some JavaScript calls the PartialUpdatePanelHandler. It sends the path to the user control and also other data like the viewstate of the control, via HTTP-POST.

    C#
    // create request
    var request = new Sys.Net.WebRequest();
    request.set_url('PartialUpdatePanelLoader.ashx');
    request.set_httpVerb('POST');
    request.set_body(this._createRequestBody(eventTarget, eventArgument));
    request.set_userContext(this);
    request.add_completed(this._loadingComplete);
  3. The user control needs a Page object to be rendered in. This job is done by PanelHostPage.

    C#
    // this code is part of PartialUpdatePanelHandler
    if (context.Request.Form["__USERCONTROLPATH"] != null)
    {
        PanelHostPage page = new PanelHostPage(
           context.Request.Form["__USERCONTROLPATH"],
           context.Request.Form["__CONTROLCLIENTID"]);
    
          ((IHttpHandler)page).ProcessRequest(context);
    
        context.Response.Clear();
          context.Response.Write(page.GetHtmlContent());
    }
  4. The user control gets normally rendered by simply adding it to the Controls collection of the Page.

    C#
    protected override void CreateChildControls()
    {
        // Load Control
        if (_controlPath != null)
            _mainForm.Controls.Add(LoadControl(ResolveUrl(_controlPath)));
    
        base.CreateChildControls();
    }
  5. The page output is sent back to the HttpHandler.

  6. The contents are transferred to the client, and inserted into the active HTML-document via DOM-operations.

    C#
    contentPanel.innerHTML = sender.get_responseData();

That's all.

ViewState

The contents of a PartialUpdatePanel are handled like this were the whole page. So, it has its own viewstate which is stored in a hidden field. The hidden field is transferred to the HttpHandler and loaded by the PanelHostPage. The logic of loading, serializing, and deserializing the viewstate is done in two overridden methods in PanelHostPage.

C#
private string _pageViewState;

protected override object LoadPageStateFromPersistenceMedium()
{
    PartialPageStatePersister persister =
            PageStatePersister as PartialPageStatePersister;
    persister.PageState = _pageViewState;
    persister.Load();

    return new Pair(persister.ControlState, persister.ViewState);
}

protected override void SavePageStateToPersistenceMedium(object state)
{
    PartialPageStatePersister pageStatePersister =
            this.PageStatePersister as PartialPageStatePersister;
    if (state is Pair)
    {
        Pair pair = (Pair)state;
        pageStatePersister.ControlState = pair.First;
        pageStatePersister.ViewState = pair.Second;
    }
    else
    {
        pageStatePersister.ViewState = state;
    }
    pageStatePersister.Save();

    _pageViewState = HttpUtility.UrlEncode(pageStatePersister.PageState);
}

The custom PageStatePersister PartialPageStatePersister uses a LosFormatter to load and store the ViewState and ControlState correctly.

The contents of _pageViewState are transferred back to the client again as a hidden input field.

Event Handling

In ASP.NET, event handling is quite simple. When a user clicks on a button, for example, the ClientID of this button is passed back to the page in the __EVENTTARGET field. This behaviour could be easily reproduced in our scenario. All we have to do is add OnClientClick calls for every button, call from there a method that loads the partial content, and add the event source to the __EVENTTARGET field that is passed to our HttpHandler. We have nothing more to do, the rest is up to the ASP.NET event pipeline.

Using the Control

Before using the control, you have to register the HttpHandler in your web.config:

XML
<httpHandlers>
  <add verb="*" path="*.ashx" validate="false"

       type="iucon.web.Controls.PartialUpdatePanelHandler"/>
</httpHandlers>

Also add a value to the appSettings. Change the value to your individual one!

XML
<appSettings>
   <add key="PartialUpdatePanel.EncryptionKey" value="k39#9sn1"/>
</appSettings>

Then, simply add a PartialUpdatePanel to your page and set the UserControlPath property. Add some controls to the LoadingTemplate and ErrorTemplate. The contents of the LoadingTemplate become visible when update operations are in progress. The contents of the ErrorTemplate are shown if an error occurs during the update (e.g. server timeout). If you set AutoRefreshInterval to a value greater then 0, the panel refreshes every n milliseconds automatically.

There is also a mechanism to communicate with the control via JavaScript or server-side code. Parameters is a collection that takes key-value-pairs. They are passed to the control via HTTP-POST when it is rendered. The following sample shows how to provide some parameters in the ASPX-page, set values via JavaScript, and read them in your ASCX control with the help of the class ParameterCollection.

ASP.NET
<%-- PartialUpdatePanel with named parameters --%>
<iucon:PartialUpdatePanel runat="server" ID="PartialUpdatePanel4"

                          UserControlPath="~/ParameterSample.ascx">
    <Parameters>
        <iucon:Parameter Name="MyParameter" Value="Hello world" />

        <iucon:Parameter Name="Counter" Value="0" />

    </Parameters>
    <ErrorTemplate>

        Unable to refresh content
    </ErrorTemplate>

</iucon:PartialUpdatePanel>
<%-- Change the value of the parameter "Counter" and refresh the panel --%>

<script type="text/javascript">
    var counter = 0;

    function updateParameterSample()
    {
        $find("PartialUpdatePanel4").get_Parameters()["Counter"] = ++counter;
        $find("PartialUpdatePanel4").refresh();
    }

</script>

<input type="button" onclick="updateParameterSample(); return false;"

       value="Click to update panel with counter" />

The code-behind of the ASCX reads the parameter values from the ParameterCollection:

C#
public partial class ParameterSample : System.Web.UI.UserControl
{
    protected void Page_Load(object sender, EventArgs e)
    {
        iucon.web.Controls.ParameterCollection parameters =
                  new iucon.web.Controls.ParameterCollection.Instance;

        Label1.Text = "Called " + parameters["Counter"] + " times";
    }
}

You can also change values of your parameters. They will be transferred back and become accessible via JavaScript.

The property InitialRenderBehaviour controls if the control should be rendered by the server during the normal page rendering (InitialRenderBehaviour.Serverside). If this property is set to InitialRenderBehaviour.Clientside, the client sends a request to the server to render the control when the page is already transferred to the browser. This is useful to the user if a part of the page needs longer time to render, but the user should be able to see other parts already. He does not need to wait for the whole page, but can already play with some parts while others are still loading. InitialRenderBehaviour.None causes the control not to render until it is requested via the JScript call $find("PartialUpdatePanel4").refresh(); as shown in one of the above samples. In this case, the <InitialTemplate> contents are shown until a refresh-call occurs.

ASP.NET
<iucon:PartialUpdatePanel runat="server" ID="PartialUpdatePanel1"
       UserControlPath="~/ExternalRefreshSample.ascx"
    InitialRenderBehaviour="None">
    <ErrorTemplate>

        Unable to refresh content
    </ErrorTemplate>
    <LoadingTemplate>
        Updating...
    </LoadingTemplate>
    <InitialTemplate>
        Nothing useful here until refresh() gets called
    </InitialTemplate>

</iucon:PartialUpdatePanel>

If you want to prevent flickering with LoadingTemplate and ContentTemplate, you can use the property DisplayLoadingAfter to set a timespan in milliseconds. The loading template won't be shown before this time expires. This is useful if your PartialUpdatePanel refreshes very quickly.

By using the property RenderAfterPanel you can load contents in a sequential order. If your site uses e.g. 10 PartialUpdatePanels with the InitialRenderBehaviour set to Clientside you will fire 10 requests to the Web server at the same time. This can cause a lot of load for the server. RenderAfterPanel means that the current PartialUpdatePanel will not be refreshed after another one was loaded. So the contents are loaded step by step and the load for the server gets reduced.

You can dynamically add and run JavaScript code from your UserControls. The methods ScriptManager.RegisterStartupScript and ScriptManager.RegisterClientScriptBlock are supported as long as you set addScriptTags to true. The following code shows how to show an alert box when the user clicks on a button in a UserControl hosted in the PartialUpdatePanel.

C#
protected void Button1_Click(object sender, EventArgs e)
{
    string script = string.Format("alert('{0}');", TextBox1.Text);

    ScriptManager.RegisterStartupScript(this, GetType(), "alert", script, true);
}

The internal transport and execution of the JavaScript code is a bit tricky. You cannot simply add a <script> tag in your HTML code. Then, when the HTML code updates the DOM-tree of your document, <script> nodes won't be executed. Internally, the PartialUpdatePanel transports all JavaScript snippets in a hidden div. The div is created by the ScriptRenderer class. The PartialUpdatePanel JavaScript code that updates the document with the newly rendered contents reads the div, uses Sys._ScriptLoader.getInstance() to execute the transported JavaScript, and at last, removes the div from the document via DOM operations.

Properties

EncryptUserControlPathEncrypts the path to your UserControl using DES algorithm and the value defined in your web.config.
This property is set to true by default.
Attention: If you set EncryptUserControlPath, you cannot change the UserControlPath via JavaScript.
UserControlPathVirtual path to your UserControl
AutoRefreshIntervalForces the Panel to refresh every n milliseconds
DisplayLoadingAfterRender the current UserControl after another successfully loaded
ParametersDescribed above in this article ;-)
InitialRenderBehaviourSets the initial rendermode to Serverside, Clientside or None

AJAX Control Toolkit Support

It was a really hard job to get some controls of the AJAX Control Toolkit working with the PartialUpdatePanel. The main problem was that the controls are instantiating themselves in the client during runtime by calling $create() in Sys.Application.add_init.

C#
Sys.Application.add_init(function() {
    $create(AjaxControlToolkit.AutoCompleteBehavior, {...}, null, null, $get(
        "PartialUpdatePanel7_myTextBox"));
);

Now when a partial post back is finished, the same code runs a second time and the function Sys.Application.addComponent which gets called by $create$ fails because the component was already instantiated.

To solve this problem, I had to make quite a dirty hack. Before the client scripts in the partial update response are executed, I change the function reference of Sys.Application.addComponent to a local one which checks if the component was already instantiated.

C#
_addComponent : function(component) {
    var id = component.get_id();
    if (typeof(Sys.Application._components[id]) === 'undefined')
        Sys.Application._components[id] = component;
}

This prevents addComponent from failing and throwing an exception. When all scripts ran successfully, the original function pointer is restored.

One Last Word

If you use this control, I would be happy if you send me the URL of your project so I can see the control in action. And... please don't forget to vote for this article.

History & Updates

13th Sep, 2010

Version 1.8

  • Now works with .NET 4.0
14th Nov, 2008

Version 1.6

Special thanks to grown from CodePlex for his excellent work on the PartialUpdatePanel!
  • Added encryption support for UserControl path
  • Added support for custom ScriptManager types (includes ToolkitScriptManager)
  • Added support for ToolkitScriptManager.CombineScriptsHandlerUrl
  • Change UserControlPath using JavaScript during runtime
  • Manipulate Parameters serverside during roundtrip
  • Fixed issue using validators and rendering in Clientside mode
  • Fixed a bug with recreating components
9th July, 2008

Version 1.5.2

  • Fixed a bug with supporting the state of RadioButtons
  • Added support for globalization
  • Showing up extended exception information
10th June, 2008

Version 1.5

  • Added support for some Controls from the ASP.NET AJAX Control Toolkit
  • Added support for ScriptManager.RegisterDataItem
  • Added support for ScriptManager.RegisterClientScriptInclude
  • New example called ToolkitSample shows some Toolkit controls inside the PartialUpdatePanel
20th May, 2008

Version 1.4

  • ControlState is now transferred correctly between postbacks using a custom PageStatePersister
  • New property DisplayLoadingAfter
  • New property RenderAfterPanel
  • Minor bug fixes
29th Mar, 2008

Version 1.3

  • Added support for ScriptManager.RegisterStartupScript to run JScript code after partial postbacks
  • Parameters are no more transported via HTTP-GET but via HTTP-POST
  • Output bug fix: the <form> tag was rendered more than once when using controls with the InitialRenderMode server-side
28th Feb, 2008

Version 1.2.1

  • Minor bug fix: Viewstate of initially server-side rendered controls were not handled correctly on normal postbacks
27th Feb, 2008

Version 1.2

  • Added the property InitialRenderBehaviour to PartialUpdatePanel
  • Added IRequiresSessionState to PartialUpdatePanelHandler (thanks to aliascodeproject)
  • UrlEncode-d the viewstate in PanelHostPage (thanks to Acoustic)
20th Feb, 2008Initial release

The project is hosted on CodePlex [^]. The updates there are uploaded in smaller intervals. So if you are interested in this project, you should visit this site regularly.

Known Problems

The ModalPopupExtender runs fine only in FireFox. Internet Explorer has some problems I have not solved yet. So if you want to show a modal popup from within a PartialUpdatePanel, create the ModalPopupExtender in the parent page. To show the dialog, call ScriptManager.RegisterStartupScript in the code behind of your PartialUpdatePanel UserControl.

Example:

ASP.NET
<script type="text/javascript">
function showModalPopupViaClient() {
    var modalPopupBehavior = $find('programmaticModalPopupBehavior');
    modalPopupBehavior.show();
}

function hideModalPopupViaClient() {
    var modalPopupBehavior = $find('programmaticModalPopupBehavior');
    modalPopupBehavior.hide();
}
</script>

<asp:Button runat="server" ID="hiddenTargetControlForModalPopup" style="display:none"/>

<ajaxToolkit:ModalPopupExtender runat="server" ID="programmaticModalPopup"
    BehaviorID="programmaticModalPopupBehavior"
    TargetControlID="hiddenTargetControlForModalPopup"
    PopupControlID="programmaticPopup"
    BackgroundCssClass="modalBackground"

    DropShadow="True"
    PopupDragHandleControlID="programmaticPopupDragHandle"
    RepositionMode="RepositionOnWindowScroll" >
</ajaxToolkit:ModalPopupExtender>
<asp:Panel runat="server" CssClass="modalPopup" ID="programmaticPopup"

    style="display:none;width:350px;padding:10px">
<asp:Panel runat="Server" ID="programmaticPopupDragHandle"
    Style="cursor: move;background-color:#DDDDDD;border:solid 1px Gray;
    color:Black;text-align:center;">
    ModalPopup shown and hidden in code


</asp:Panel>

<iucon:PartialUpdatePanel runat="server" ID="PartialUpdatePanel4"
    UserControlPath="~/ToolkitSample.ascx" />

You can now use this sample to see how to use ModalPopup with an invisible TargetControl.
The ModalPopupExtender
this popup is attached to has a hidden target control. The popup is hidden
<asp:LinkButton runat="server" ID="hideModalPopupViaServer" Text="on the server side
    in code behind" OnClick="hideModalPopupViaServer_Click" /> and

<a id="hideModalPopupViaClientButton" href="#">on the client in script</a>.

<br />
</asp:Panel>

The code behind in your PartialUpdatePanel UserControl will look like:

C#
public partial class ToolkitSample : System.Web.UI.UserControl
{
    protected void Page_Load(object sender, EventArgs e)
    {
        ScriptManager.RegisterStartupScript(this, GetType(), "showPopup",
            "showModalPopupViaClient()", true);
    }
}

This will execute showPopup after a partial PostBack and the modal popup gets shown.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)