Living with ASP.NET AJAX: MasterPages, UpdatePanels and cross-content AsyncPostBackTriggers

One of the great things about the ASP.NET AJAX UpdatePanel control is the ability to trigger a refresh of the UpdatePanel’s contents based on the event of another control on the same page. This means you can have multiple UpdatePanels hosted in your page and only have them update themselves when necessary based on the events of the other controls on the page (you will need to explicitly set the UpdateMode property of the UpdatePanels to Conditional to achieve this). The markup to achieve this is very simple:

<asp:UpdatePanel ID="updToolbar" runat="server" UpdateMode="Conditional">
    <ContentTemplate>
        <asp:Button ID="btnRefresh" runat="server" Text="Refresh" />
    </ContentTemplate>
</asp:UpdatePanel>
<asp:UpdatePanel ID="updDynamicContent" runat="server" UpdateMode="Conditional">
    <ContentTemplate>
        <asp:GridView ID="gvListOfThings" runat="server"></asp:GridView>
    </ContentTemplate>
    <Triggers>
        <asp:AsyncPostBackTrigger ControlID="btnRefresh" EventName="Click" />
    </Triggers>
</asp:UpdatePanel>

Here we have two UpdatePanels defined with the second one including a declaration for an AsyncPostBackTrigger that wires up the click event of the button contained in the other UpdatePanel. When the user clicks the button, the second UpdatePanel will refresh its contents. Easy.

Issues arise however when you start to use MasterPages with multiple ContentPlaceHolders defined, a fairly common scenario when designing ASP.NET UI beyond the basic tutorials. For some reason (I admit I don’t know why) you cannot specify a control in a different Content pane as the source of an AsyncPostBackTrigger. So this:

<asp:Content ID="conMain" runat="server" ContentPlaceHolderID="cphMain">
    <asp:UpdatePanel ID="updToolbar" runat="server" UpdateMode="Conditional">
        <ContentTemplate>
            <asp:Button ID="btnRefresh" runat="server" Text="Refresh" />
        </ContentTemplate>
    </asp:UpdatePanel>
</asp:Content>
<asp:Content ID="conRight" runat="server" ContentPlaceHolderID="cphRight">
    <asp:UpdatePanel ID="updDynamicContent" runat="server" UpdateMode="Conditional">
        <ContentTemplate>
            <asp:GridView ID="gvListOfThings" runat="server">
            </asp:GridView>
        </ContentTemplate>
        <Triggers>
            <asp:AsyncPostBackTrigger ControlID="btnRefresh" EventName="Click" />
        </Triggers>
    </asp:UpdatePanel>
</asp:Content>

Results in this:

Server Error in ‘/AjaxSample’ Application.


A control with ID ‘btnRefresh’ could not be found for the trigger in UpdatePanel ‘updDynamicContent’.

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.InvalidOperationException: A control with ID ‘btnRefresh’ could not be found for the trigger in UpdatePanel ‘updSummary’.

Not much fun! So how do we go ahead and make this possible?

A Solution

To make this possible and stay with the declarative model of defining AnsycPostBackTriggers as much as possible we can create a small custom server control to proxy the event across the Content panes of our content page. We can drop this control into the same content pane as the UpdatePanel we wish to define the trigger on, set the trigger to wire up to the control’s EventProxied event, and then in the handler for the actual control’s event we wish to update on we call the proxy control’s ProxyEvent method.

First of all, add a new class file to your website called EventProxy.cs and paste in the following code:

public class EventProxy : Control, IPostBackEventHandler
{
    public EventProxy() { }
    public void RaisePostBackEvent(string eventArgument) { }
    public event EventHandler<EventArgs> EventProxied;
    protected virtual void OnEventProxy(EventArgs e)
    {
        if (this.EventProxied != null)
        {
            this.EventProxied(this, e);
        }
    }
    public void ProxyEvent(EventArgs e)
    {
        OnEventProxy(e);
    }
}

This very basic control defines a public event EventProxied and a public method ProxyEvent(EventArgs) which just raises the EventProxied event. The control must implement the IPostBackEventHanlder interface in order for it to be allowed to be delcared as the source of an AsyncPostBackTrigger.

Now in your content page add a @Register directive to register the application’s dll as a source of controls with the following line:

<%@ Register Assembly="AJAXSample" Namespace="AJAXSample.Controls" TagPrefix="as" %>

(Be sure to change the assembly name and namespace to match those of your application)

Now we can change our markup sample from before to use the new control like so:

<%@ register assembly="AJAXSample" namespace="AJAXSample.Controls" tagprefix="as" %>
<asp:Content ID="conMain" runat="server" ContentPlaceHolderID="cphMain">
    <asp:UpdatePanel ID="updToolbar" runat="server" UpdateMode="Conditional">
        <ContentTemplate>
            <asp:Button ID="btnRefresh" runat="server" Text="Refresh" OnClick="btnRefresh_Click" />
        </ContentTemplate>
    </asp:UpdatePanel>
</asp:Content>
<asp:Content ID="conRight" runat="server" ContentPlaceHolderID="cphRight">
    <asp:UpdatePanel ID="updDynamicContent" runat="server" UpdateMode="Conditional">
        <ContentTemplate>
            <asp:GridView ID="gvListOfThings" runat="server">
            </asp:GridView>
        </ContentTemplate>
        <Triggers>
            <asp:AsyncPostBackTrigger ControlID="epRefreshButtonClicked" EventName="EventProxied" />
        </Triggers>
    </asp:UpdatePanel>
    <as:EventProxy ID="epRefreshButtonClicked" runat="server" />
</asp:Content>

Change your page’s code-behind to call the EventProxy control’s ProxyEvent method in the handler for the button’s click event like so:

protected void btnRefresh_Click(object sender, EventArgs e) { epRefreshButtonClicked.ProxyEvent(e); }

And that’s it! We now have a reusable control that allows us to declaratively use the event of a control in a different content pane as the trigger for a partial postback of our UpdatePanel. You’ll need to add an instance of the EventProxy control to your content pane for each event of a control in a different content pane you wish to use as an AsyncPostBackTrigger. The only code we have to write each time is to call the appropriate EventProxy control’s ProxyEvent method from the event handler for the event we wish to use as the trigger.

I hope this helps someone.


11 Comments on “Living with ASP.NET AJAX: MasterPages, UpdatePanels and cross-content AsyncPostBackTriggers”

  1. Norm says:

    Hey, I just wanted to let you know that this helped me today. Nice workaround. Thx!

  2. Alexey Pervanyuk says:

    Thanks, it was useful.

  3. JC says:

    Thanks, just what I was looking for.

  4. Raise says:

    Great stuff, thx

  5. Dennis says:

    Thanks, it’s very useful.

  6. Varun says:

    This is simply awesome

  7. JoelMMCC says:

    That’s a neat trick and all, but why not just do this instead?

    Basically, in ASP.NET declarative syntax, if a parameter names a control to be referred to, that control must reside within the same “Naming Container” as the control with the parameter.

    If not, you can jump through the event proxy class hoop you did, or use code to assign the parameter programmaticaly using the referenced control’s ClientID instead of its ID parameter, OR just use the “$” syntax to prepend any Naming Containers’ Control names (which effectively does the same thing as programmaticaly assigning the ClientID parameter)!

    Note that you must prepend all Naming Containers that are between the control being referenced and the control doing the referencing if they differ.

    Sometimes the Naming Container Control isn’t so obvious. For instance, if you have an AJAX Control Toolkit “Accordion” control (a nifty way to do client-side Wizard-like forms that are way cooler than traditional Wizards!), and the triggering control is inside a different AccordionPanel (with ID=”apTrigger”) than the UpdatePanel is, then you’d need to do this:

    Instead of what you might guess:

    The “_content” gets added to the name in this case because the AccordionPanel control requires that its content be contained within a “…” block (this is because there would also be a “…” block, and both count as separate Naming Containers).

    If there is more than one nesting of Naming Containers between the Trigger and the UpdatePanel, append all that are in the “path” between them. So, if the AccordionPanel were also inside a different Content control as per your example, just do this:

    Think of it as like unto a pathname for a hard disk or URL file located in another subdirectory, with “$” instead of “\” or “/” (respectively), and the Naming Containers being analogous to subdirectories.

  8. JoelMMCC says:

    Ack! It stripped out my examples! Lemme try this again:

    That’s a neat trick and all, but why not just do this instead?

    <asp:AsyncPostBackTrigger ControlID=”conMain$btnRefresh” EventName=”Click” />

    Basically, in ASP.NET declarative syntax, if a parameter names a control to be referred to, that control must reside within the same “Naming Container” as the control with the parameter.

    If not, you can jump through the event proxy class hoop you did, or use code to assign the parameter programmaticaly using the referenced control’s ClientID instead of its ID parameter, OR just use the “$” syntax to prepend any Naming Containers’ Control names (which effectively does the same thing as programmaticaly assigning the ClientID parameter)!

    Note that you must prepend all Naming Containers that are between the control being referenced and the control doing the referencing if they differ.

    Sometimes the Naming Container Control isn’t so obvious. For instance, if you have an AJAX Control Toolkit “Accordion” control (a nifty way to do client-side Wizard-like forms that are way cooler than traditional Wizards!), and the triggering control is inside a different AccordionPanel (with ID=”apTrigger”) than the UpdatePanel is, then you’d need to do this:

    <asp:AsyncPostBackTrigger ControlID=”apTrigger_content$TriggerControlID” EventName=”TheEvent” />

    Instead of what you might guess:

    <asp:AsyncPostBackTrigger ControlID=”apTrigger$TriggerControlID” EventName=”TheEvent” />

    The “_content” gets added to the name in this case because the AccordionPanel control requires that its content be contained within a “<Content>…</Content>” block (this is because there would also be a “<Header>…</Header>” block, and both count as separate Naming Containers).

    If there is more than one nesting of Naming Containers between the Trigger and the UpdatePanel, append all that are in the “path” between them. So, if the AccordionPanel were also inside a different Content control as per your example, just do this:

    <asp:AsyncPostBackTrigger ControlID=”conMain$apTrigger_content$btnRefresh” EventName=”Click” />

    Think of it as like unto a pathname for a hard disk or URL file located in another subdirectory, with “$” instead of “\” or “/” (respectively), and the Naming Containers being analogous to subdirectories.

  9. Leo says:

    Hi Demian, I have made my own GridView which inherits from GridView and I created an ImageButton in the GridView’s pager. this button exports the grid’s content to excel, so I need to send a full postback to get the generated file.
    In the aspx I declared an UpdatePanel and inside this my grid. What i’m traying to do is to add a PostBackTrigger in the updatePanel an add the ImageButton.ID to the trigger.ControlID, I do this in the grid’s Render event, but always get the same error:
    A control with ID ‘MyImgButtonID’ could not be found for the trigger in UpdatePanel ‘UpdatePanel1’

    How could I resolve this?
    Thanks and sorry for my poor english

    Regards


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s