ASP.NET: PostBackUrl and How It Can Break ViewState
I hadn’t done pure ASP.NET Web Forms development for quite a while and I had to create a really simple edit form. The requirement was to show a confirmation message if there are unsaved changes when the user clicks on a button which would redirect him to another page. It’s really straight forward but as most seemingly simple things it turned out that it is somewhat trickier. I will explain every step I took down the road to get it done and what problems I encountered.
Buttons and PostBackUrl
I started with two buttons on the page:
- Save button that triggers a postback to the server and persist the changes that the user made
- Return button that posts back to the previous page – has its PostBackUrl property set to the url of the corresponding page
Then I wrote some JavaScript code that detects if there is any kind of change between the initial state of the page data and the current one when the user clicks the Return button. If nothing is changed the user was taken to the previous page. So far so good. But if there are some changes a confirmation message appears (I’m using a custom modal dialog, not the default browser’s confirmation dialog implementation) that notifies the user of unsaved pending changes and whether or not he/she wishes to navigate away from the current page anyway. This is where everything goes wrong. By default the ASP.NET Button control renders some JavaScript code for the click event of the button when PostBackUrl is set and it is executed regardless of my event handler.
Note: I am not attaching event handlers directly as attributes on DOM elements. Had I done this nothing would have messed up in my page. But this is really a bad way of doing client side development that is against unobtrusive JavaScript style.
Anyway this JavaScript code changes the form element’s action attribute with the value set for PostBackUrl and if the user decides that he/she wants to stay on the page and to save his/her changes the next time he/she clicks the Save button it will try to post back to the wrong page. Let’s say my page is called Default.aspx and I have another page Defautl2.aspx. Here is the rendered button:
<input type="submit" id="btnReturn" onclick="javascript:WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions("btnReturn", "", false, "", "Default2.aspx", false, false))" value="Return" name="btnReturn">
As you can see there is a JavaScript function WebForm_DoPostBackWithOptions that gets called and receives a single parameter a helper object with all the necessary data to initiate a post back. As you can see the fifth parameter for the WebForm_PostBackOptions constructor is the page to which the post back would occur. Inside the WebForm_DoPostBackWithOptions the action attribute of the page’s form element is changed to Default2.aspx. This won’t be a problem if I was not preventing the postback from occurring. However I do prevent it and from now on any subsequent postback triggered by a control on the page would post back to Default2.aspx. But since the ViewState of the first page differs from that of the second one an exception is thrown:
Validation of viewstate MAC failed. If this application is hosted by a Web Farm or cluster, ensure that <machineKey> configuration specifies the same validationKey and validation algorithm. AutoGenerate cannot be used in a cluster.
What is my solution then?
New form element
I decided to go for a solution with another form element for the second page and get rid of the PostBackUrl property on the button. Now when the user decides not to leave the page no JavaScript code changes the action attribute of the form element and everything is the way it is supposed to be. If he/she chooses to go ahead and go to the previous page then the second form element is submitted and the user is taken to the previous page. Keep in mind that you should not define more than one form server element with runat=”server” attribute because ASP.NET supports only a single form element. You could either create it as a standard HTML element or add it dynamically on the page.
Summary
This is not the first time I encounter this kind of exception but it was the first time I did for this kind of scenario and I was a bit puzzled in the beginning. So I needed to inspect what was going on behind the scenes and come up with a simple and quick solution. It was obvious that something is wrong when the button is clicked and debugging the injected function that makes the actual postback revealed it. There might be more elegant and clean solutions for resolving this issue so everyone is welcomed to share them.