Detecting and Preventing a User From Submitting a Form Twice
Users get impatient when something is taking a long time to run server-side. This usually results in having a form submitted more than once. Ok, maybe its not impatience, maybe its that they didn't think they clicked the button.... I don't know why.... I just have to fix the problem of duplicate records in the database.
We use a pretty simple method of tracking whether a user has submitted a form more than one. This is usually used in conjunction with other methods, such as BusyBoxes or javascript click prevention. Ultimately, this method is our absolute failsafe to ensure we only submit the form once.
This has been used and tested (and approved) by a number of different payment gateways and banks we use in Australia.
How do we do it
All our pages inherit from a common base page (inherited from System.Web.UI.Page). We override the OnInit method and add a hidden field which is a Guid. We only set this field when the page first loads.
protected override void OnInit(EventArgs e) { Literal lit = new Literal(); lit.ID = "__PAGEGUID"; lit.Visible = false; this.Controls.Add(lit); if (!IsPostBack) { lit.Text = Guid.NewGuid().ToString(); } base.OnInit(e); } public Guid GetPageGuid() { Literal lit = this.FindControl("__PAGEGUID") as Literal; if (lit == null) { throw new System.Exception("Could not find __PAGEGUID control"); } else { Guid g = new Guid(lit.Text); return g; } }
This gives us a unique reference for every page we use. When its comes time to checking that a page is unique we do the following.
NOTE: In the scenario below, we're tracking that users don't click the "Order" button more than once when we're firing an order through to Microsoft Dynamics NAV.
/* * Ensure we only submit the order once. */ try { OrderHelper.EnsureOrderIsUnique(this.GetPageGuid()); } catch (DuplicateOrderException dupExc) {
// Do something with the error. (Code omitted)
}
The method that does the "EnsureOrderIsUnique" is described below. I've used a static List to store the page Guids in this case. When we take credit card payments, we store the Guid in the database along with the payment information. Its up to you how to store the Guid... it just needs to be accessible by all processes.
private static List<Guid> _submittedOrders = new List<Guid>(); /// <summary> /// Keeps track of all the Order Guid we've placed (similiar to Payment Gateway). /// If an order is not unique, then a DuplicateOrderException is thrown. /// </summary> /// <param name="orderGuid"></param> public static void EnsureOrderIsUnique(Guid orderGuid) { lock (((ICollection)_submittedOrders).SyncRoot) { if (_submittedOrders.Contains(orderGuid)) { throw new DuplicateOrderException("Order has already been placed"); } _submittedOrders.Add(orderGuid); } }
Thats it.... pretty simple really. And it works under a number of different scenarios:
- people double-clicking on a button at the end of a wizard
- people finishing a wizard, pressing back on the browser, then pressing finish again
- people opening up new browser windows and submitting both..