Thursday, April 04, 2013

Multiple submit event handlers on Ajax forms in a wizard style application

.. this was driving me crazy!

I was working on an application (ASP.NET, MVC3) in which I had to implement a wizard like piece of functioality. You know of the 'Next-Next-Previous-Next-Finish'-type.
To make things nice for the user I used AJAX. To make things simpler for me I have one control that is loaded and depending on the page that the user is currently on I load different controls into the form. I return a PartialView and that replaces part
That is all not too complicated and works like a charm.
Uhm, no it doesn't
The first page shows up. I hit the submit button and the page went by smoothly and I could see the ControllerAction being hit. Great! I just increased the page counter and onwards to the next page. The next page arrives in the browser and I hit the submit button again.
Uh oh! Things go wrong now. My ControllerAction is hit twice! Worse even on the next page, it gets hit three times! Oh my, oh my.
Inspection of the problem
Looking at the html closer I quickly discovered that the form had multiple event handlers tied to the 'submit' event. This was the cause of the forms being submitted multiple times.
How come these multiple event handlers then?
Apparently the jQuery Ajax (jquery.unobtrusive-ajax.min.js) has some trouble with the Ajax replacing a part of the DOM and then reattaches some of the existing 'submit' Event handlers. The extra event handler on each page is thus explained.
Root cause is that the AJAX call reloads part of the DOM including the FORM-element and not the content of the FORM and thus somehow the old submit eventhandlers are not cleared. Hence, multiple event handlers.
Okay, so let me Google that for you
Or Bing it. Or Yahoo it.
Many people have encountered similar problems. But none quite the same as mine or at least the provided solutions did not solve my problem. And so I experimented with many solutions and all of them failed.
Thought about somehow preventing all the event handlers but the first to fire. Or just to delete all event handlers except the first. Couldn't find a good way of doing that.

Back to the drawing board
Completely rebuilding the Views and PartialViews to refresh the DOM inside the FORM element was not an option. So that was ruled out.
In the end we decided to go a slightly other way. We decided to step away from a normal submit button as that triggers (all) the submit event handler(s) of the FORM and makes us lose control of the mess we got in. We created a simple button that calls a JavaScript function that does the submit of the FORM through jQuery AJAX. This way we never ever call the submit event handler of the form.
Problem solved.
Please find below the simple JavaScript function that does all the clever stuff.
function onSingleSubmitButtonClicked(button)
{
  var frm = $('form');
  var data =  frm.serialize();
  var url = frm.attr('action');
  var method = frm.attr('data-ajax-method');
  var target = frm.attr('data-ajax-update');
  $.ajax({
    type: method,
    url: url,
    data: data,
    beforeSend: function() {showLoader();},
    complete: function(response) {$(target).html(response.responseText);hideLoader();}
  });
}
We attach this function to the click event of input[type=button] and that is all. Below a step by step run through.
  1. The function finds the one form on our page. When you have more forms on a page you may have to pass an id or when the button is inside the form find the parent of the button of type FORM.
  2. Next we serialize all the input fields from the form.
  3. We then grab the action attribute from the form and use that as the url to post the data to.
  4. We read the post or get method from the appropriate method.
  5. The update target where we will put the response from the Ajax call into the DOM.
  6. Then we make the Ajax call. The serialized data is included and we also show a loader overlay before we send the request and hide it again after the call is complete.
Probably some optimization is possible, but that is a simple solution to the weird problem. Just take things in your own hands. You can't always trust the browser or jQuery do things the way you would expect them to do.