Thursday, March 26, 2009

Using JQuery to intercept the click of an asp:Button

I was working on an interesting problem today. The architecture of a different project I am working on does not use MVC, but ASP.NET controls. We have separated them into a control/controller model so that business logic interacts with a controller, which passes POCOs to a control, and the control knows how to render them. The control knows no business logic, security, etc, while the controller doesn't understand anything about the UI domain EXCEPT for the fact that it is interacting with something derived from "Control" that lives in Web.UI.

Many of the examples in JQuery show you how to "unbind" and event, but I quickly found this wasn't working for me. What I wanted to do was place a traditional ASP.NET button on the page (quite frankly, the button was already buried in another control, so I wanted to work with this rather than come up with a hack to do a lightweight input button). I needed to intercept the click event, perform a call back, and then based on the result either display an error or allow the originally intended event (a postback) to fire.

After lots of searching I finally settled with decorating my functions with alerts until I figured out exactly what was going on. So let's get started.

First, the button: easy enough.

   <asp:Button ID="btnExample" runat="server" Text=" Click Me "/>

On the server side, we can do this:

   btnExample.OnClick += MyClickHandler; 
   ...
   protected void MyClickHandler(object sender, EventArgs e)
   {
       // do something IF all is well 
   }

Of course the server should ALSO check what I'm wiring in for validation, but I'm simplifying this for now. Now the fun part. What we want to do is find the button (I'll leave the JQuery up to you - you can search on the button, emit the client id and go on that, etc. In my case, it's the third button in a div with class "buttonBar"). Let's wire this up:

 $(document).ready(function() {
        var nextButton = $(".buttonBar input:button:eq(2)");
        window.criteria_postback = $(nextButton).attr("onclick");        
        $(nextButton).removeAttr("onclick");
        $(nextButton).unbind().click(function(event) {

            window.criteria_event = event;
            
            event.preventDefault();
        
            $(this).attr("disabled", "disabled");
            $(this).val(" Please wait... ");

            //load cmdString with something useful for the server 

            WebForm_DoCallback('__Page', cmdString, window.Criteria_CallbackHandler, null, null, false);          
            
        });

So let's walk through this. I found for some reason (probably documented somewhere that I just didn't find) that unbinding the click event wasn't working. So instead, I decided to save the existing click event (window.criteria_postback). The reason I save it to the window is because I need to reference it in an eval() expression later on. Microsoft sandboxes evals so they can't see variables unless they are explicitly scoped to global.

Next, we get rid of the attribute (this prevents the event from firing).

Then I wire in my own click event. I SAVE the event to the global space, prevent the default (going through with it), then disable my button so the user doesn't double click.

Finally, I wire in the callback. A few interesting things about callbacks. First, if this code was embedded in a control, I'd need to have a reference to the control instead of the __Page for the control to pick it up. Second, and most importantly, even when you wire your own callback, you MUST let the ClientScript KNOW to listen!. In other words, I told my page to implement ICallbackHandler which exposes:

 public void RaiseCallbackEvent(string eventArgument)
 {
   // do something with the event from the client
 }
 ...
 public string GetCallbackResult() 
 {
   // return a result to the client
 }

Some people mistakenly believe this is enough to be able to capture callbacks, but unless you actually ask for the callback reference, the page/control won't listen! So it's important that even if you don't use the result, you at least make a call like this:

// generate the callback handlers 
Page.ClientScript.GetCallbackEventReference(this, "args", string.Empty, string.Empty);

Now it is happy and listening. We're almost done ... we've intercepted the original postback and saved it, and we've wired in our own callback. In my case, I have a select grid that is harvesting check boxes, so I want to build a light weight list of identifiers. I build that list on the client, then use the callback to send it down and store it on the server for later. A pattern that works well for me with the call back is like this:


private string _callbackResult; 

public void RaiseCallbackEvent(string eventArgument)
{
   try
   {
      ...
      _callbackResult = string.Empty;
   }
   catch(Exception ex) 
   {
     _callbackResult = ex.Message;
   }
}

public string GetCallbackResult()
{
   return _callbackResult; 
}

And now we come back to the client side. Part of the Callback is a delegate to handle the return. In our case, we passed in window.Criteria_CallbackHandler. Here is how we "catch" the result:

window.Criteria_CallbackHandler = function(result, context) {

    if (result == null || result == '') {
        eval(window.criteria_postback + ";onclick(window.criteria_event);");
        return; 
    }
    else {
        alert(result);
    }

    var nextButton = $(".buttonBar input:button:eq(2)");
    
    $(nextButton).val(" Next ");
    $(nextButton).removeAttr("disabled");

}

So what is going on? If I had no issues, I do something interesting. When I saved the "onclick" earlier, you'll find the ASP.NET page wired it in like this:

void onclick(event) { blah; }

So I am basically spitting the function back out in my "eval", then immediately calling it with the event that I saved earlier! This has the result of "business as usual" which in my case is to post to a new page and pick off the list I persisted in the callback to do something with it. Otherwise, I alert the error to the customer (a prettier solution may be to have a div for errors and just write this into the div instead of using the generic alert box), and then regardless, I restore my button so it can be clicked again.

There you go ... buttons, JQuery, and callback handlers all in one post!

Jeremy Likness