Jun 12 2010
cancel all event handlers on postback
recently i had an interesting problem in my project.. which is an asp.net 3.5 web application. it consists of a monster page, which nests close to million user controls within it. these user controls post back at will, and sometimes whimsically too… the page itself had a RAD Navigation control, the wizard king.
now the one common object these user controls and the page itself, was working was my Entity object from Entity Framework. Now the page is basically a wizard, which keeps filling this object, and there are options to save this object explicitly, or create templated items out of it.
This same object (Business Entity) could also be worked by multiple users across different browser sessions/work stations. sort of a disconnected data set. Any persistent action done on this object updates the ‘Last Modified’ date time of this object. (locally and in the database)
My requirement was that, during every page load, i needed to check if this local object was the latest copy, and if stale, redirect the user to a “no donuts for you” page…
Now, in the normal scenario, i would write a simple method in the page load of my monster page, which checks if the object is latest, and if not, just do a
Response.Redirect(donutUrl, true);
But Murphy being who he is, never gives normal scenarios to developers. I had to pop up a Modal window to the user to inform that, “you are working on a stale entity and need to refresh your view.” And on the click of a confirmation, redirect the user.
So i used an AJAX Modal popup, and popped up the message. On the OK click, the user was redirected as i had wished for.
Now as much as it looks hunky-dory, a weird thing was happening… if the user clicked on the “Save My Object So Far” button, the page load did its task, of popping up the popup properly (tongue twisty?) but the event handler for the “Save” button continued to happen behind, and it saved the entity… that is not what i wish for on a Monday morning, and then the next morning, and then the next…
now i had to prevent any further operation after the popup… so i began to put the stuff between by ears into effect… as lame as they were, i thought of the following ideas: (if they are that)
- have a boolean flag, and chain it across the event handlers, so that if the entity was modified, do not proceed with the event handling code. (lame…. if there are too many event handlers.. and there were)
- end the page response, after the popup.. so that no further code gets executed after the page load method. but next to grabbing a candy from my 2 year old cousin quietly, i have not been able to do this.. either a blank page is rendered, or everything happens as unexpected… there are no half-measures with the asp.net page life cycle.. (i cannot use document.Write())
- how about if i manage to suppress all the event handlers on the page, once i detect that the entity is stale.
the 3rd idea seemed to make sense, and i proceeded with a dummy page.. which rendered a label on page load. and then, a button click modified this label. then in the page load, i tried to do a
protected void Page_Load(object sender, EventArgs e) { this.LabelMessage.Text = "Page Load"; this.ButtonAction.Click -= this.ButtonAction_Click; } protected void ButtonAction_Click(object sender, EventArgs e) { this.LabelMessage.Text = "Clicked Me, overwrote Page Load"; }
and holy guacamole… it worked.. the event handler was not executed…
now another problem i noticed, is that i don’t know which control posted back.. also, which event of the control caused the postback.. so i had to solve 2 problems..
- identify the control that posted back
- remove all event handlers for that control..
and the doors of a solution seem to open up… finding the postback control was a standard snippet i use, and reflection rocks, when i have to dig out the protected “Events” property of any Control. i also realized that, the event handler delegates are stored in a linked list format for every event… which also need to be retrieved using reflection..
so i wrote a utility method, which, if given a page, finds the control that posted back, and removes all the event handlers of the control.
this way, you have a central generic method, which can suppress any further event handling in your code.
now that i have typed close to two pages of a newspaper, time to post the snippet, which you’ll scroll through, in the first place, in any case.. so here goes nothing…
// Cancels all the event handling code for the control that posted back. public static void CancelPostbackEvent(Page page) { if (page.IsPostBack) { var postBackControl = Utility.GetPostBackControl(page); if (postBackControl != null) { var controlType = typeof(Control); var postBackControlEventHandlerList = controlType.InvokeMember("Events", System.Reflection.BindingFlags.GetProperty | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, postBackControl, null) as EventHandlerList; if (postBackControlEventHandlerList != null) { var eventHandlerListType = typeof(EventHandlerList); object headEventHandlerListEntry = eventHandlerListType.InvokeMember("head", System.Reflection.BindingFlags.GetField | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, postBackControlEventHandlerList, null); if (headEventHandlerListEntry != null) { var delegatesDictionary = new Dictionary<object, Delegate[]>(); Utility.GetEventHandlersRecursively(delegatesDictionary, headEventHandlerListEntry); foreach (var delegateContainer in delegatesDictionary) { for (var index = delegateContainer.Value.Length - 1; index >= 0; --index) { postBackControlEventHandlerList.RemoveHandler(delegateContainer.Key, delegateContainer.Value[index]); } } } } } } } private static Control GetPostBackControl(Page page) { Control postBackControl = null; var postBackControlName = page.Request.Params.Get("__EVENTTARGET"); if (!String.IsNullOrEmpty(postBackControlName)) { postBackControl = page.FindControl(postBackControlName); } else { foreach (string controlName in page.Request.Form) { var control = page.FindControl(controlName); if (control is System.Web.UI.WebControls.Button) { postBackControl = control; break; } } } return postBackControl; } private static void GetEventHandlersRecursively(Dictionary<object, Delegate[]> delegatesDictionary, object currentEventHandlerListEntry) { if (currentEventHandlerListEntry != null) { var eventHandlerListEntryType = currentEventHandlerListEntry.GetType(); var eventHandler = (Delegate)eventHandlerListEntryType.InvokeMember("handler", System.Reflection.BindingFlags.GetField | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic, null, currentEventHandlerListEntry, null); object key = eventHandlerListEntryType.InvokeMember("key", System.Reflection.BindingFlags.GetField | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic, null, currentEventHandlerListEntry, null); var nextEventHandlerListEntry = eventHandlerListEntryType.InvokeMember("next", System.Reflection.BindingFlags.GetField | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic, null, currentEventHandlerListEntry, null); if (eventHandler != null) { var eventDelegates = eventHandler.GetInvocationList(); if (eventDelegates != null && eventDelegates.Length > 0) { delegatesDictionary.Add(key, eventDelegates); } } if (nextEventHandlerListEntry != null) { Utility.GetEventHandlersRecursively(delegatesDictionary, nextEventHandlerListEntry); } } }
do let me know, if you have a better solution, or if the above code upsets your chihuahua…