ASP.Net, disappearing Recaptcha, UpdatePanels and Partial PostBacks: Fixed once and for all

Posted on Feb 04, 2014 in  | No comments

Background

I recently ran into a problem where I had a ‘form‘ in a UserControl which contained the Recaptcha control.  As it happens somebody put this control into an UpdatePanel and onto a page, eventually noticing that the Recaptcha was not re-displayed after a PostBack.  A quick Google identified a very large number of people suffering the same problem, with a few different idea circulated as to how to fix it.  After trying a few I noted that either the particular solution was not appropriate to my case, or simply did not work – so I decided I would investigate and fix the problem the ‘old fashioned’ way… brute force.

Why does this happen?

Firstly, I’ll mention we are using v1.0.5.0 of the Recaptcha control and my particular approach may not be applicable to newer versions.  With the disclaimer out of the way…

Putting the Recaptcha control onto your ASP.NET page results in the following HTML being emitted to the page:

<script type="text/javascript">
	var RecaptchaOptions = { theme : 'clean', tabindex : 0 };
</script>
<script type="text/javascript" src="http://www.google.com/recaptcha/api/challenge?k=..."></script>
<script type="text/javascript" src="http://www.google.com/recaptcha/api/js/recaptcha.js"></script>
<noscript>
	...yada yada yada...
</noscript>

If you look at the page after it has run in your browser window you’ll see more stuff than I have written in there.  That’s because at the end of the ‘recaptcha.js’ file Google does something a bit nasty (in ASP.NET terms):

<script>
Recaptcha._init_options(RecaptchaOptions);
if ( RecaptchaOptions && "custom" == RecaptchaOptions.theme )
{
  if ( RecaptchaOptions.custom_theme_widget )
  {
	Recaptcha.widget = Recaptcha.$(RecaptchaOptions.custom_theme_widget);
	Recaptcha.challenge_callback();
  }
} else {
  document.write('<div style="display: none" id="recaptcha_widget_div"></div>');
  document.write('<script> Recaptcha.widget = Recaptcha.$("recaptcha_widget_div"); Recaptcha.challenge_callback(); </script>');
}
</script>

Actually, I’ve paraphrased there for readability and to get the syntax highlighter to correctly identify the script.  The actual code is slightly different but functionally equivalent, at least for the purposes of this discussion – try not to get bogged down in it, it won’t affect the results.

"What’s nasty about that" I hear you say.  Well in and of itself there’s nothing wrong there, but this bit is definitely the source of our PostBack problem. See when the AJAX PostBack infrastructure executes it gathers up the response send from the server, picks out the part contained in the UpdatePanel we are executing and ‘pastes’ it back into the right place in the HTML document.  Which, for regular HTML works a treat.

Unfortunately in this case it definitely will not work.  Even adding myPanel.Update() and ScriptManager.RegisterClientScriptBlock() is not going to really cut the mustard for a few reasons but the important 2 are:

  1. Injected script blocks don’t get re-executed during the ASP.NET AJAX PostBack cycle without a bit of help, and
  2. document.write() works fine when the page is processing initially but can’t be used in PostBack processing because the document has already been closed; so there is nowhere for the code to go

What do the other blogs say; and why are they wrong?

Wrong is probably too strong a word – let’s say they are mostly incomplete for my scenario.  Generally the advice is to ensure that the UpdatePanel has had its Update() method called, and to put Recaptcha.reload() into a ScriptManager.RegisterClientScriptBlock() call on the server side.  The Recaptcha processing is almost entirely client side, and the problem is caused by the Recaptcha control being ‘updated’ by AJAX, so it’s safe to assume your UpdatePanel is being updated.  That takes care of the first suggestion.  The reload() suggestion is close but actually won’t work because even though the Recaptcha JavaScript object exists the target HTML element has been lost in the PostBack cycle.  You do need to call it, but that’s only 1/3 of the solution.

Whatever, how do I fix it?

There are 2 pieces needed to fix this issue:

  1. A target container for the Recaptcha control when in the process of posting back
  2. A script block that will re-initialise the Recaptcha code and renew the image on PostBack

The first bit looks like this:

<div runat="server" id="pbTarget" visible="false"></div>
<recaptcha:RecaptchaControl ID="recaptcha" runat="server" Theme="clean" />

I made the div runat=”server” because I use some server-side code to only turn it on when I need it.  This will be the container for the PostBack version of the Recaptcha control.  Remember I am inside a UserControl so you may need some minor alterations on the next bit.  This is the server-side code in the event handler called when the user submits the form:

protected void btnSubmit_Click(object sender, EventArgs e)
{
  recaptcha.Validate();
  if (!Page.IsValid || !recaptcha.IsValid)
  {
    pbTarget.Visible = true;
    ScriptManager.RegisterClientScriptBlock( 
      recaptcha, 
      recaptcha.GetType(), 
      "recaptcha",
      "Recaptcha._init_options(RecaptchaOptions);"
      + "if ( RecaptchaOptions && "custom" == RecaptchaOptions.theme )"
      + "{"
      + "  if ( RecaptchaOptions.custom_theme_widget )"
      + "  {"
      + "    Recaptcha.widget = Recaptcha.$(RecaptchaOptions.custom_theme_widget);"
      + "    Recaptcha.challenge_callback();"
      + "  }"
      + "} else {"
      + "  if ( Recaptcha.widget == null || !document.getElementById("recaptcha_widget_div") )"
      + "  {"
      + "    jQuery("#" + pbTarget.ClientID + "").html('<div id="recaptcha_widget_div" style="display:none"></div>');"
      + "    Recaptcha.widget = Recaptcha.$("recaptcha_widget_div");"
      + "  }"
      + "  Recaptcha.reload();"
      + "  Recaptcha.challenge_callback();"
      + "}", 
      true
    );

    return;
  }
  else
  {
    //normal page processing here...

Actually, I’ve paraphrased that too before I get caned for the idiotic use of string concatenation – I was just trying to make the syntax highlighter behave.

What I have done, with the help of jQuery, is to re-inject the div that the Recaptcha code wants to paint into, then re-initialise the control with the challenge_callback() method.  There is a tiny lag when the reload() method is firing where you get to see the old code, but it’s fast enough to not bother me.  The main reason the reload() didn’t work on its own is that Recaptcha.widget is null since the DOM object had been destroyed.  With our new and improved code we restore the div and re-attach it to the Recaptcha.widget property before calling reload()

All’s well that ends well.