Tags: , , | Posted by Kevin Babcock on 5/14/2009 10:56 PM | Comments (5)

When creating a server control from scratch you must implement the IPostBackEventHandler interface if you want the control to handle postback events. If you’re extending an existing control and the control already implements this interface, then you simply need to override the IPostBackEventHandler methods to add your own postback events.

The interface requires that a single method be implemented:

public void RaisePostBackEvent(string eventArgument)
{
    // raise control-specific events here
}

RaisePostBackEvent is called on the IPostBackEventHandler control that raised the postback to the server. If an argument was passed to the server during the postback, it will be passed to the eventArgument parameter of the RaisePostBackEvent() method. But how does the server know which control raised the postback and how are any arguments passed back to the page? Let’s take a step back and look at how forms are submitted in HTML/JavaScript and how ASP.NET abstracts this behavior to provide us with the postback.

Submitting a Form

There are two mechanisms by which a form can be submitted to the server: an HTML submit button element or JavaScript. In order to use an HTML submit button, you must add the following markup to your form:

<input type="submit" value="Submit Form" />

This will render a simple button element which will display the text “Submit Form.” When clicked, the button will submit the form back to the server using an HTTP POST request. To do the same in JavaScript you simply need to retrieve the form DOM element and call its submit() function:

function submitForm() {
    document.forms[0].submit();
}

This will also cause the form to be submitted back to the server using an HTTP POST request.

ASP.NET Postbacks

The two methods described above are all that is required to create web applications that require any kind of interaction between client and server. ASP.NET provides many different kinds of controls which we can use to post our forms back to the server, but underneath all the layers of abstraction lie those two fundamental mechanisms.

For a page that uses any control except the Button and ImageButton (which we’ll take a look at momentarily), ASP.NET generates a simple JavaScript function and two hidden <input /> elements to handle form submissions:

<input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
<input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" />

function __doPostBack(eventTarget, eventArgument) {
    if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
        theForm.__EVENTTARGET.value = eventTarget;
        theForm.__EVENTARGUMENT.value = eventArgument;
        theForm.submit();
    }
}

Any time you interact with an ASP.NET control in a way the should cause a postback, this function is called. The two hidden fields, __EVENTTARGET and __EVENTARGUMENT, are used to keep track of the control which caused the postback and any data that should be associated with the control, respectively. These two fields can be access on the server in either the Page.Form or Page.Params collection. The first contains a collection of all form variables posted back to the server, and the second is a combined collection of form variables, query string variables, server variables, and cookies. The following code snippets are identical:

protected void Page_Load(object sender, EventArgs e)
{
    var control = Request.Form["__EVENTTARGET"];
    var args = Request.Form["__EVENTARGUMENT"];
}
protected void Page_Load(object sender, EventArgs e)
{
    var control = Request.Params["__EVENTTARGET"];
    var args = Request.Params["__EVENTARGUMENT"];
}

This is how the server knows which control raised the postback and can call its RaisePostBackEvent() method, passing in the __EVENTARGUMENT data to the expected parameter.

The only exceptions to this rule are the Button and ImageButton controls. These controls are rendered as HTML submit button elements and therefore do not require any JavaScript to post the form and its data back to the server. These controls can still be identified when a postback is received by the server because they will be the only Button controls represented in the form collection.

Enabling a Server Control to Cause a Postback

So now that we know how postbacks work in ASP.NET, we need to add this functionality to the control that we want to implement the IPostBackEventHandler interface. After all, if the control doesn’t actually cause a postback, then the functions we implement as a part of that interface will be useless because they will never be called. Lucky for us, adding the client script necessary to cause a postback is pretty easy.

There are really two ways you can force your control to initiate a postback to the server. The first is to simply add a client event handler which fires off the __doPostBack() function, like so:

protected override void AddAttributesToRender(HtmlTextWriter writer)
{
    var script = String.Format("__doPostBack('{0}', '');", this.ClientID);
    writer.AddAttribute(HtmlTextWriterAttribute.Onclick, script);
    base.AddAttributesToRender(writer);
}

Here we’ve overridden the control’s AddAttributesToRender() method and added our own onclick client event handler to call __doPostBack(). I don’t really like this method because it feels clumsy. And remember, if you don’t have any other ASP.NET controls on your page that cause __doPostBack() to be rendered to the page, then you will see JavaScript exceptions when your control tries to call that function.

A better way to add the onclick client event handler would be to use the Page.ClientScriptManager’s GetPostBackEventReference() method:

protected override void AddAttributesToRender(HtmlTextWriter writer)
{
    writer.AddAttribute(HtmlTextWriterAttribute.Onclick, 
        Page.ClientScript.GetPostBackEventReference(this, String.Empty));
    base.AddAttributesToRender(writer);
}

Not only will this add the event handler for us, but it will also ensure the __doPostBack() function is inserted into our page for us.

Bringing it all Together

Now that we’ve taken a look at how this all works, let’s build a control so we can see these concepts in action. For this example I’m going to extend the Panel control to create a simple ClickablePanel, which will fire off a postback to the server any time a user clicks inside the panel. The class should implement the IPostBackEventHandler interface by defining a RaisePostBackEvent() method. It will also define a single Click event, and will override the Panel’s AddAttributesToRender() method as demonstrated above. Here’s what the implementation should look like:

using System;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace MyViewState.Examples.Web.Controls
{
    public class ClickablePanel : Panel, IPostBackEventHandler
    {
        public ClickablePanel()
            : base()
        { }

        private EventHandler _click;
        public event EventHandler Click
        {
            add { _click += value; }
            remove { _click -= value; }
        }

        protected virtual void FireClickEvent()
        {
            if (_click != null)
                _click(this, new EventArgs());
        }

        protected override void AddAttributesToRender(HtmlTextWriter writer)
        {
            writer.AddAttribute(HtmlTextWriterAttribute.Onclick, 
                Page.ClientScript.GetPostBackEventReference(this, String.Empty));
            base.AddAttributesToRender(writer);
        }

        public void RaisePostBackEvent(string eventArgument)
        {
            FireClickEvent();
        }
    }
}

Now you can add a ClickablePanel to any ASP.NET page or user control and handle its Click event to add your own logic.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="MyViewState.Examples.Web._Default" %>
<%@ Register Assembly="MyViewState.Examples.Web" Namespace="MyViewState.Examples.Web.Controls" TagPrefix="controls" %>

<!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>Example - Implementing IPostBackEventHandler in an ASP.NET Control</title>
</head>
<body>
    <form id="form1" runat="server">
        <controls:ClickablePanel ID="ClickablePanel" runat="server"
            BackColor="Blue"
            Height="200px"
            Width="400px">
        </controls:ClickablePanel>
        <asp:Literal ID="litMessage" runat="server" />
    </form>
</body>
</html>
using System;
using MyViewState.Examples.Web.Controls;

namespace MyViewState.Examples.Web
{
    public partial class _Default : System.Web.UI.Page
    {
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            ClickablePanel.Click += new EventHandler(ClickablePanel_Click);
        }

        protected void ClickablePanel_Click(object sender, EventArgs e)
        {
            var message = String.Format("You clicked the panel at {0}.", DateTime.Now.ToLongTimeString());
            litMessage.Text = message;
        }
    }
}

Now when we run this, we'll see the following markup when the page is rendered to the browser:

<!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>
    <title>
        Example - Implementing IPostBackEventHandler in an ASP.NET Control
    </title>
</head>
<body>
    <form name="form1" method="post" action="Default.aspx" id="form1">
        <div>
            <input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" 
                value="/wEPDwUKMTUzMzI3OTIxNGRkj5tANgOC/iyQIg7/hT+BzVmVUQo=" />
        </div> 
        <div> 
            <input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
            <input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" />
        </div>
        <div onclick="__doPostBack('ClickablePanel','')" id="ClickablePanel" 
            style="background-color:Blue;height:300px;width:500px;">
        </div>    
        <script type="text/javascript">
            //<![CDATA[
            var theForm = document.forms['form1'];
            if (!theForm) {
                theForm = document.form1;
            }
            function __doPostBack(eventTarget, eventArgument) {
                if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
                    theForm.__EVENTTARGET.value = eventTarget;
                    theForm.__EVENTARGUMENT.value = eventArgument;
                    theForm.submit();
                }
            }
            //]]>
        </script> 
    </form>
</body>
</html>

Notice the control is rendered as a <div> element with an onclick event handler that calls __doPostBack(). And if we click on the panel, the form is posted back to the server, the Click event is raised, and the message below the panel is updated.

screenshot

Wrap Up

Implementing an interface with a single method should be a pretty simple task. I went a little overboard describing how to implement the IPostBackEventHandler interface, but hopefully now you have all the why’s and how’s to take with you when building your own ASP.NET server controls. If you’re interested in taking this knowledge even further, then I recommend checking out the IPostBackDataHandler, which you must implement if your control needs to examine any data that is posted back by the client.

kick it on DotNetKicks.com

Comments

Scott Elliott on 6/12/2009 10:33 AM Love this, however our solution is vb.net...and is not fully working.  I created my .ascx, converted the above code stuck it in:

Imports System
Imports System.Web.UI
Imports System.Web.UI.WebControls

Namespace Controls
    Partial Public Class ClickablePanel
        Inherits Panel
        Implements IPostBackEventHandler
        Implements INamingContainer

        Public Sub New()
            MyBase.New()
        End Sub

        Public Event Click(ByVal sender As System.Object, ByVal e As System.EventArgs)

        Protected Overridable Sub FireClickEvent()
            RaiseEvent Click(Me, New System.EventArgs())
        End Sub

        Protected Overloads Overrides Sub AddAttributesToRender(ByVal writer As HtmlTextWriter)
            writer.AddAttribute(HtmlTextWriterAttribute.Onclick, Page.ClientScript.GetPostBackEventReference(Me, String.Empty))
            MyBase.AddAttributesToRender(writer)
        End Sub

        Public Sub RaisePostBackEvent(ByVal eventArgument As String) Implements System.Web.UI.IPostBackEventHandler.RaisePostBackEvent
            FireClickEvent()
        End Sub
    End Class
End Namespace

------

When the page is run, what I get is:

Parser Error Message: 'Autoweb.CustomerSales.Controls.ClickablePanel' is not allowed here because it does not extend class 'System.Web.UI.UserControl'.

Source Error:


Line 1:  <%@ Control Language="vb" AutoEventWireup="false" CodeBehind="ClickablePanel.ascx.vb" Inherits="xxxx.xxxx.Controls.ClickablePanel" %>

"xxxx.xxxx" is just a mask for our namespace, not that anyone cares, but just to be a little anonymous.

So anyway, I don't quite understand this since your code inherits Panel and so does mine, yet you can use yours as a user control, but mine is barking at me!

What gives?
Scott Elliott on 6/12/2009 11:28 AM ok ... I've narrowed it down to your <%@ Register Assembly="MyViewState.Examples.Web" Namespace="MyViewState.Examples.Web.Controls" TagPrefix="controls" %> ... when I do the same thing swapping your namespaces for mine, it doesn't work.  For some reason my class doesn't even register in the class view.  So all I get now is that ClickablePanel is not defined.
Scott Elliott on 6/12/2009 11:57 AM Ok ... just for other people that may be wondering ... I fixed it.

Whatever you do, in VS2008, DO NOT create your class file in the App_Code folder like I did.

For whatever reason it's bugged.  If you do so, your class will NOT show up in the Class View and will not be defined, thus my error.

Workaround: Create your class file on the root, then once complete, you can move it inside the App_Code folder for storage.

Crazy things!  Sorry for wasting anyone's time.
Kevin Babcock on 6/14/2009 7:32 PM @Scott: That is indeed a strange error, but I'm glad you figured out how to get it working. Thanks for contributing to the discussion!
Joseph D. Cook on 7/15/2009 5:01 PM Thank you, this was very helpful; I do have a related question. I am using a clickable panel in the ItemTemplate of a Repeater. Other controls that have an inherent OnClick such as buttons, when placed in the item template, will cause the containing Repeater’s OnItemCommand to fire when clicked. The clickable panel does not cause this behavior to occur; I can still capture the panel’s click event with its own handler but I lose any reference to which iteration of the repeater the panel raising the event lives in. Any thoughts?

Add comment




biuquote
  • Comment
  • Preview
Loading