views:

125

answers:

3

To prevent duplicate requests (i.e. pressing F5 right after clicking a command button), I've setup my page base class to ignore the request if it's detected as a duplicate.

When I say 'ignore' I mean Response.End()

Now I thought I've seen this work before, where there's an issue, I just Response.End() and the users page just does nothing. I don't know the exact circumstance in which this worked, but I'm unable to repeat it now.

Now when I call Response.End(), I just get an empty browser. More specifically, I get this html.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML><HEAD>
<META http-equiv=Content-Type content="text/html; charset=utf-8"></HEAD>
<BODY></BODY></HTML>

I setup the following test app to confirm the problem is not elsewhere in my app. Here it is:

Add the following to an aspx form

<asp:Label ID="lbl" Text="0" runat="server" /><br />
<asp:Button ID="btnAdd1" Text="Add 1" runat="server" /><br />
<asp:Button ID="btnAdd2" Text="Add 2" runat="server" /><br />
<asp:Button ID="btnAdd3" Text="Add 3" runat="server" /><br />

And here's the code behind file

using System;

namespace TestDupRequestCancellation
{
 public partial class _Default : System.Web.UI.Page
 {
  protected void Page_Init(object sender, EventArgs e)
  {
   btnAdd1.Click += btnAdd1_Click;
   btnAdd2.Click += btnAdd2_Click;
   btnAdd3.Click += btnAdd3_Click;
  }

  protected void Page_Load(object sender, EventArgs e)
  {
   if (!IsPostBack)
    CurrentValue = 0;
   else if (Int32.Parse(lbl.Text) != CurrentValue)
    Response.End();
  }

  protected void Page_PreRender(object sender, EventArgs e)
  {
   lbl.Text = CurrentValue.ToString();
  }

  protected int CurrentValue
  {
   get
   {
    return Int32.Parse(Session["CurrentValue"].ToString());
   }
   set
   {
    Session["CurrentValue"] = value.ToString();
   }
  }

  void btnAdd3_Click(object sender, EventArgs e)
  {
   CurrentValue += 3;
  }

  void btnAdd2_Click(object sender, EventArgs e)
  {
   CurrentValue += 2;
  }

  void btnAdd1_Click(object sender, EventArgs e)
  {
   CurrentValue += 1;
  }
 }
}

When you load the page, clicking any button does what is expected, but if you press F5 at any time after pressing one of the buttons, it will detect it as a duplicate request and call Response.End() which promptly ends the task. Which leaves the user with an empty browser.

Is there anyway to leave the user with the page as it was, so they can just click a button?

Also; please note that this code is the simplest code I could come up with to demonstrate my problem. It's not meant to demonstrate how to check for dup requests.

EDIT: Another change that will allow me to achieve the same results would be to disable all my event handlers. Is that possible?

A: 

Don't force the response to end. Let it end gracefully (fall through), without going through your "main" logic for legitimate requests. If this proves to be too much work, then your code probably needs to be refactored. Or, the lazy way out, you can setup a global boolean indicating the validity of the requests (set in your Page_Load), and then check it throughout the rest of your code as needed.

EDIT

The more I think about this, I am more leaning to a more user-friendly approach. Instead of ending the request, how about redirecting them to a page dedicated to informing them of how to avoid duplicate requests in the site? This is what my bank web site does.

Josh Stodola
I could check from every event handler, but wanted a way to do this as unintrusively as possible from a base class. I was considering just turning off all the event handlers, but I'm not sure that can be done ... can it?
Timid Developer
@Timid Please read my edits. Sorry :)
Josh Stodola
Thanks, I'll give you an upvote as soon as I get enough rep.
Timid Developer
"instead of ending the request, how about redirecting them to a page dedicated to informing them of how to avoid duplicate requests in the site?" Trying to tell your users how to use their browsers is one of the cardinal sins of web app design.
Frank Farmer
@Frank Who said anything about "telling users how to use their browser"? Tell them how to use your site effectively. I know where you're coming from, but this is not a perfect world and there are exceptions to every rule.
Josh Stodola
A: 

See if sending HTTP response code 204 No Content does what you want.

Kevin Reid
That didn't work either. Thanks anyway.
Timid Developer
+1  A: 

If you're checking validators before executing your events, then hooking into your validation functionality might be the most unintrusive way to accomplish this.

try this:

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

namespace TestDupRequestCancellation
{
    public partial class _Default : System.Web.UI.Page
    {
        protected void Page_Init(object sender, EventArgs e)
        {
            btnAdd1.Click += btnAdd1_Click;
            btnAdd2.Click += btnAdd2_Click;
            btnAdd3.Click += btnAdd3_Click;
            AddFirstPostbackValidator();
        }

        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
                CurrentValue = 0;
        }

        protected void Page_PreRender(object sender, EventArgs e)
        {
            lbl.Text = CurrentValue.ToString();
        }

        protected int CurrentValue
        {
            get
            {
                return Int32.Parse(Session["CurrentValue"].ToString());
            }
            set
            {
                Session["CurrentValue"] = value.ToString();
            }
        }

        private void AddFirstPostbackValidator()
        {
            CustomValidator val = new CustomValidator();
            val.ID = "DuplicatePostbackValidator";
            val.ErrorMessage = "Browser refresh detected. Command ignored.";
            val.ServerValidate += val_ServerValidate;
            Validators.Add(val);
            form1.Controls.AddAt(form1.Controls.IndexOf(lbl) + 1, val); // user feedback
        }

        void val_ServerValidate(object source, ServerValidateEventArgs args)
        {
            args.IsValid = Int32.Parse(lbl.Text) == CurrentValue;
        }

        void btnAdd3_Click(object sender, EventArgs e)
        {
            if (!IsValid) 
                return;
            CurrentValue += 3;
        }

        void btnAdd2_Click(object sender, EventArgs e)
        {
            if (!IsValid)
                return;
            CurrentValue += 2;
        }

        void btnAdd1_Click(object sender, EventArgs e)
        {
            if (!IsValid)
                return;
            CurrentValue += 1;
        }
    }
}
John MacIntyre
This will allow me to prevent events from executing. thanks.
Timid Developer