Magnus Rahl
Jan 20, 2012
  17245
(1 votes)

Async Pages part 3: Async Pages with databinding and events

In the previous post How to use asynchrony in your Pages we saw a basic example of asynchronous processing. Compared to how the code would have looked if it was written for synchronous processing things were a bit shifted in order. That has implications for such things as databinding and especially events.

The order of things

Remember that the click events of buttons etc. occur quite late in the ASP.NET Page lifecycle, after the Load stage, but before the PreRender stage. If you were to set up your asynchronous calls in OnLoad as in the example in the previous post, you wouldn’t be able to take events into account. But remember that asynchronous processing will run all the way to the PreRender stage before detaching. This can be used to include event actions in how you do your asynchrony.

Also, if you do data binding to display data that you get from some external source, you probably don’t want to re-bind the data unless there’s been a change, like if a paging button has been clicked. This can be achieved by using a simple flag to control if the asynchronous tasks are registered or not.

A more advanced UserControl

Description and markup

This example user control gets an Atom feed from EPiServer World and displays the posts by databinding a ListView. On load it shows only some posts, as set in a property, but it as a toggle to show all posts, to demonstrate events.

The ascx code looks like this:

<%@ Control Language="C#" AutoEventWireup="false" CodeBehind="FeedReader.ascx.cs" Inherits="EPiServer.Templates.AsyncControls.FeedReader" %>
<%@ Import Namespace = "System.ServiceModel.Syndication" %>
<asp:ListView runat="server" ID="lvFeed" ItemPlaceholderID="plhItemContainer">
    <LayoutTemplate>
        <ul>
            <asp:PlaceHolder runat="server" ID="plhItemContainer" />
        </ul>
        <asp:LinkButton runat="server" Text="Toggle show all" OnCommand="ToggleShowAll" CommandName="Show" Visible="<%#ShowAllLink%>" />
    </LayoutTemplate>
    <ItemTemplate>
        <li>
            <h3><%# FormatTitle((SyndicationItem)Container.DataItem) %></h3>
            <p>
                <%# FormatSummary((SyndicationItem)Container.DataItem) %>
                <br />
                <%# FormatLink((SyndicationItem)Container.DataItem) %>
            </p>
            
        </li>
    </ItemTemplate>
</asp:ListView>

Very basic list calling some helper methods and properties to display the data bound to it.

Setting up asynchrony

To demonstrate another set of methods this example uses the Page.RegisterAsyncTask method instead of Page.AddOnPreRenderCompleteAsync. This method has some extra opportunities which I will describe soon. But first, here’s some code:

protected override void OnInit(EventArgs e)
{
    base.OnInit(e);
    // Register a haandler for PreRenderComplete (there's no
    // OnPreRenderComplete to override)
    Page.PreRenderComplete += PagePreRenderComplete;
    if (!IsPostBack)
    {
        // Initial load, set flag used to bind data in the view
        _needRefresh = true;
    }
}
protected override void OnPreRender(EventArgs e)
{
    base.OnPreRender(e);
    // OnPreRender is the last chance to register async page events.
    // The reason we do it here rather than sooner is that now the
    // click event handlers have run so any way they affect the
    // loading logic is now known.
    // In this case the _needRefresh flag can be set either on initial
    // page load or if the toggle show all link was clicked
    if (_needRefresh)
    {
        var task = new PageAsyncTask(
            AsyncGetFeedBegin,
            AsyncGetFeedEnd,
            AsyncGetFeedTimeout,
            null,
            false);
        Page.RegisterAsyncTask(task);
    }
}

First, as you can see there’s a field called _needRefresh which is set to true on the first request. It basically sits in the place where a synchronous page could do it’s data binding. But we’re going to get the data source asynchronously so we have to flag that a rebind is required (which it is on the first request).

Second, we register an event handler for the PreRenderComplete event. This will be used to do the data binding. There are two reasons this is done differently than in the previous post. Firstly, the data binding (but not the fetching of the data to bind) will occur on the “rendering” thread, saving the “fetching” thread the work. But more importantly it has to do with the capabilities of the Page.RegisterAsyncTask method.

It’s last argument (false in the example) tells ASP.NET whether tasks should be run in parallel or not. Yes, you can register multiple tasks and have them finish in parallel or sequence. In any case they will all finish before the PreRenderComplete stage, so you can collect data from different sources and then combine them when rendering the data. Though this example uses only one task and one source of data.

The method also takes an extra delegate, AsyncGetFeedTimeout in the example, which is called if the async operation times out. The default timeout is 45 seconds, but can be changed by setting the AsyncTimeout attribute of the Page directive or in the system.web/pages element in the config. We won’t use it at all in this example.

Handling the click event

As mentioned, when the PreRender stage is reached, click event handlers will have run. The handler for the Click event of the toggle button will set the private member _showAll to true or false. Again, this is probably where you would have done your databinding if it were a synchronous page, but again we have to flag and wait. It also sets the _needRefersh flag to signal that it’s value has been changed. Here’s the code:

protected void ToggleShowAll(object sender, CommandEventArgs e)
{
    // Toggle the show/hide command and the _showAll flag and
    // mark the page as needing a refresh so that the
    // data can be fetched and bound again
    const string show = "Show";
    const string hide = "Hide";
    var button = sender as LinkButton;
    if (button == null) return;
    _showAll = e.CommandName == show;
    button.CommandName = _showAll ? hide : show;
    _needRefresh = true;
}

The AsyncGetFeedBegin and AsyncGetFeedEnd are no revolutions if you read the previous post, but remember that these are only registered if the _needRefresh flag is set so we don’t get any data unless something’s changed.

The first one starts a request and saves a handle to it in a private member. The second one picks up the response once it’s finished and gets the result out as a SyndicationFeed object which it saves in the _feedData member. The AsyncGetFeedTimeout method is empty so I skip it:

/// <summary>
/// Hooks into the asynchronous page loading for calling GetFeed async
/// and release the currently processing thread until it completes
/// </summary>
private IAsyncResult AsyncGetFeedBegin(object sender, EventArgs e, AsyncCallback cb, object state)
{
    _request = WebRequest.Create(FeedUrl);
    return _request.BeginGetResponse(cb, state);
}
/// <summary>
/// Called when the async processing is copleted, retreives the result of GetFeed
/// </summary>
/// <param name="ar"></param>
private void AsyncGetFeedEnd(IAsyncResult ar)
{
    var response = (WebResponse)_request.EndGetResponse(ar);
    var reader = XmlReader.Create(response.GetResponseStream());
    SyndicationFeedFormatter formatter = null;
            
    try
    {
        var atom10FeedFormatter = new Atom10FeedFormatter();
        var rss20FeedFormatter = new Rss20FeedFormatter();
                
        if (atom10FeedFormatter.CanRead(reader))
        {
            formatter = atom10FeedFormatter;
        }
        else if (rss20FeedFormatter.CanRead(reader))
        {
            formatter = rss20FeedFormatter;
        }
        else
        {
            throw new Exception("Can only read Atom 1.0 or RSS 2.0 compatible feeds.");
        }
        formatter.ReadFrom(reader);
        _feedData = formatter.Feed;
    }
    finally
    {
        reader.Close();
    }
}

Binding the data

All that remains now is binding the result to the ListView, which we will of course only do if _needRefresh is true. It’s done in the event handler for the PreRenderComplete event which was registered in OnInit. I also include the helper methods used in the rendering:

protected void PagePreRenderComplete(object sender, EventArgs e)
{
    if (_needRefresh && _feedData != null)
    {
        // Show all items if the flag is set or if Limit is zero or negative, otherwise use Limit
        var itemsToShow = (_showAll || Limit < 1) ? int.MaxValue : Limit;
        lvFeed.DataSource = _feedData.Items.Take(itemsToShow);
        lvFeed.DataBind();
    }
}
protected string FormatTitle(SyndicationItem item)
{
    if (item == null || item.Title == null) return String.Empty;
    return HttpUtility.HtmlEncode(item.Title.Text);
}
protected string FormatSummary(SyndicationItem item)
{
    if (item == null || item.Summary == null) return String.Empty;
    return HttpUtility.HtmlEncode(item.Summary.Text);
}
protected string FormatLink(SyndicationItem item)
{
    if (String.IsNullOrEmpty(ReadMoreText) || item == null || item.Links.Count < 1) return String.Empty;
    return String.Format("<a href='{0}' alt='{1}'>{2}</a>",
                            HttpUtility.HtmlAttributeEncode(item.Links.First().Uri.ToString()),
                            HttpUtility.HtmlAttributeEncode(ReadMoreText),
                            HttpUtility.HtmlEncode(ReadMoreText));
}

Source code

The complete example can be downloaded from the code section: Asynchronous pages code sample.

Jan 20, 2012

Comments

Jan 23, 2012 09:30 AM

Great series. Thanks for sharing!

Magnus Rahl
Magnus Rahl Jan 23, 2012 02:42 PM

Glad you found it worth (I hope) the long read :)

Please login to comment.
Latest blogs
Introducing Optimizely Graph Source .NET SDK

Overview Of Optimizely Graph Optimizely Graph is a cutting-edge, headless content management solution designed to integrate seamlessly with any...

Jake Minard | Oct 10, 2024

Content Search with Optimizely Graph

Optimizely Graph lets you fetch content and sync data from other Optimizely products. For content search, this lets you create custom search tools...

Dileep D | Oct 9, 2024 | Syndicated blog

Omnichannel Analytics Simplified – Optimizely Acquires Netspring

Recently, the news broke that Optimizely acquired Netspring, a warehouse-native analytics platform. I’ll admit, I hadn’t heard of Netspring before,...

Alex Harris - Perficient | Oct 9, 2024 | Syndicated blog

Problem with language file localization after upgrading to Optimizely CMS 12

Avoid common problems with xml file localization when upgrading from Optimizely CMS 11 to CMS 12.

Tomas Hensrud Gulla | Oct 9, 2024 | Syndicated blog

Optimizely Autocomplete (Statistics)

A user starts typing in the search input, and it returns suggestions for phrases they might be searching for. How to achieve this?

Damian Smutek | Oct 9, 2024 | Syndicated blog

Optimizely Forms: You cannot submit this form because an administrator has turned off data storage.

Do not let this error message scare you, the solution is quite simple!

Tomas Hensrud Gulla | Oct 4, 2024 | Syndicated blog