Tuesday, February 17, 2009

ASP.NET MVC postback support

In my unwanted quest to port a MonoRail app to ASP.NET MVC, I have to use existing master pages and user controls.

Bad news is, some of these user controls post back and then all hell breaks loose. Specifically, I started getting this exception on every postback:

Line 338: <% if (Model.PaymentOptions.Count > 0) {%>
Line 339: <div class="fleft" style="margin:2px 20px 2px 0px; padding: 0px;">
Line 340: <% Html.RenderPartial("ListingDetailPaymentOption", Model.PaymentOptions[0]);%>
Line 341: </div>
Line 342: <% }%>
[HttpException]: Unable to validate data.
   at System.Web.Configuration.MachineKeySection.GetDecodedData(Byte[] buf, Byte[] modifier, Int32 start, Int32 length, Int32& dataLength)
   at System.Web.UI.ObjectStateFormatter.Deserialize(String inputString)
[ViewStateException]: Invalid viewstate. 
	Client IP: 127.0.0.1
	Port: 18559
	User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.0.6) Gecko/2009011913 Firefox/3.0.6 (.NET CLR 3.5.30729)
	ViewState: /wEPDwUKLTc0NTAwNTg1NA9kFgJmD2QWAmYPZBYCZg9kFgICBw9kFgICBQ9kFgICAw9kFgICAw9kFgICAg9kFgJmDxYCHgtfIUl0ZW1Db3VudAIFFgpmD2QWAmYPFQk8LzIwMDZfTGFuZF9Sb3Zlcl9SYW5nZV9Sb3Zlcl9TcG9ydF9TdXBlcmNoYXJnZWRfMTEyNjg3LnhodG1sBDIwMDYpTGFuZCBSb3ZlciBSYW5nZSBSb3ZlciBTcG9ydCBTdXBlcmNoYXJnZWQ1L3Bob3Rvcy9pbWdzaG93Uy5hc3B4P2lkPTI0MTgmc2l6ZT10XzEyMHg5MCZZZWFyPTIwMDY8LzIwMDZfTGFuZF9Sb3Zlcl9SYW5nZV9Sb3Zlcl9TcG9ydF9TdXBlcmNoYXJnZWRfMTEyNjg3LnhodG1sBDIwMDYpTGFuZCBSb3ZlciBSYW5nZSBSb3ZlciBTcG9ydCBTdXBlcmNoYXJnZWQCMTkHJDk1MC4wMGQCAQ9kFgJmDxUJJy8yMDA2X01lcmNlZGVzX0UzNTBfU2VkYW5fXzExMjk4MC54aHRtbAQyMDA2FE1lcmNlZGVzIEUzNTAgU2VkYW4gKy9waG90b3MvaW1nc2hvdy5hc3B4P2lkPTY4MjE4JnNpemU9dF8xMjB4OTAnLzIwMDZfTWVyY2VkZXNfRTM1MF9TZWRhbl9fMTEyOTgwLnhodG1sBDIwMDYUTWVyY2VkZXMgRTM1MCBTZWRhbiACMTIHJDQ4OC4wMGQCAg9kFgJmDxUJLy8yMDA2X01lcmNlZGVzX0NMUzUwMF9Db3VwZV80X0Rvb3JfMTA4NjI4LnhodG1s...
[HttpException]: 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.
   at System.Web.UI.ViewStateException.ThrowError(Exception inner, String persistedState, String errorPageMessage, Boolean macValidationError)
   at System.Web.UI.ViewStateException.ThrowMacValidationError(Exception inner, String persistedState)
   at System.Web.UI.ObjectStateFormatter.Deserialize(String inputString)
   at System.Web.UI.ObjectStateFormatter.System.Web.UI.IStateFormatter.Deserialize(String serializedState)
   at System.Web.UI.Util.DeserializeWithAssert(IStateFormatter formatter, String serializedState)
   at System.Web.UI.HiddenFieldPageStatePersister.Load()
   at System.Web.UI.Page.LoadPageStateFromPersistenceMedium()
   at System.Web.UI.Page.LoadAllState()
   at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
   at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
   at System.Web.UI.Page.ProcessRequest()
   at System.Web.UI.Page.ProcessRequestWithNoAssert(HttpContext context)
   at System.Web.UI.Page.ProcessRequest(HttpContext context)
   at System.Web.Mvc.ViewPage.RenderView(ViewContext viewContext)
   at System.Web.Mvc.ViewUserControl.RenderViewAndRestoreContentType(ViewPage containerPage, ViewContext viewContext)
   at System.Web.Mvc.ViewUserControl.RenderView(ViewContext viewContext)
   at System.Web.Mvc.WebFormView.RenderViewUserControl(ViewContext context, ViewUserControl control)
   at System.Web.Mvc.WebFormView.Render(ViewContext viewContext, TextWriter writer)
   at System.Web.Mvc.HtmlHelper.RenderPartialInternal(String partialViewName, ViewDataDictionary viewData, Object model, ViewEngineCollection viewEngineCollection)
   at System.Web.Mvc.Html.RenderPartialExtensions.RenderPartial(HtmlHelper htmlHelper, String partialViewName, Object model)
   at ASP.views_listingdetail_index_aspx.__Render__control2(HtmlTextWriter __w, Control parameterContainer) in c:\trabajo\web\WEB2\Views\ListingDetail\Index.aspx:line 340
   at System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children)
   at System.Web.UI.Control.RenderChildren(HtmlTextWriter writer)
   at System.Web.UI.Control.Render(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children)
   at System.Web.UI.Control.RenderChildren(HtmlTextWriter writer)
   at System.Web.UI.Control.Render(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children)
   at System.Web.UI.Control.RenderChildren(HtmlTextWriter writer)
   at System.Web.UI.Control.Render(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children)
   at System.Web.UI.Control.RenderChildren(HtmlTextWriter writer)
   at System.Web.UI.HtmlControls.HtmlForm.RenderChildren(HtmlTextWriter writer)
   at System.Web.UI.HtmlControls.HtmlContainerControl.Render(HtmlTextWriter writer)
   at System.Web.UI.HtmlControls.HtmlForm.Render(HtmlTextWriter output)
   at System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.HtmlControls.HtmlForm.RenderControl(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children)
   at System.Web.UI.Control.RenderChildren(HtmlTextWriter writer)
   at System.Web.UI.Control.Render(HtmlTextWriter writer)
   at Templates.master.Render(HtmlTextWriter writer) in C:\trabajo\web\WEB2\templates\master.master.vb:line 29
   at System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children)
   at System.Web.UI.Control.RenderChildren(HtmlTextWriter writer)
   at System.Web.UI.Control.Render(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children)
   at System.Web.UI.Control.RenderChildren(HtmlTextWriter writer)
   at System.Web.UI.Control.Render(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children)
   at System.Web.UI.Control.RenderChildren(HtmlTextWriter writer)
   at System.Web.UI.Page.Render(HtmlTextWriter writer)
   at System.Web.Mvc.ViewPage.Render(HtmlTextWriter writer)
   at System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter)
   at System.Web.UI.Control.RenderControl(HtmlTextWriter writer)
   at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
   at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
   at System.Web.UI.Page.ProcessRequest()
   at System.Web.UI.Page.ProcessRequestWithNoAssert(HttpContext context)
   at System.Web.UI.Page.ProcessRequest(HttpContext context)
   at ASP.views_listingdetail_index_aspx.ProcessRequest(HttpContext context) in c:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\root\ffa83e12\a51f62df\App_Web_index.aspx.1cc8d514.vtl-v5gs.0.cs:line 0
   at System.Web.Mvc.ViewPage.RenderView(ViewContext viewContext)
   at System.Web.Mvc.WebFormView.RenderViewPage(ViewContext context, ViewPage page)
   at System.Web.Mvc.WebFormView.Render(ViewContext viewContext, TextWriter writer)
   at System.Web.Mvc.ViewResultBase.ExecuteResult(ControllerContext context)
   at System.Web.Mvc.ControllerActionInvoker.InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult)
   at System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClass11.b__e()
   at System.Web.Mvc.ControllerActionInvoker.InvokeActionResultFilter(IResultFilter filter, ResultExecutingContext preContext, Func`1 continuation)
   at System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClass11.<>c__DisplayClass13.b__10()
   at System.Web.Mvc.ControllerActionInvoker.InvokeActionResultWithFilters(ControllerContext controllerContext, IList`1 filters, ActionResult actionResult)
   at System.Web.Mvc.ControllerActionInvoker.InvokeAction(ControllerContext controllerContext, String actionName)
   at System.Web.Mvc.Controller.ExecuteCore()
   at System.Web.Mvc.ControllerBase.Execute(RequestContext requestContext)
   at System.Web.Mvc.ControllerBase.System.Web.Mvc.IController.Execute(RequestContext requestContext)
   at System.Web.Mvc.MvcHandler.ProcessRequest(HttpContextBase httpContext)
   at System.Web.Mvc.MvcHandler.ProcessRequest(HttpContext httpContext)
   at System.Web.Mvc.MvcHandler.System.Web.IHttpHandler.ProcessRequest(HttpContext httpContext)
   at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

As usual, first thing I did was googling the exception, but the standard answer was: "Postback is not supported in MVC". I also found this Microsoft Connect issue that confirmed that this is a bug.

What I found weird is that the exception was actually throwing in the ViewUserControl (MVC's user control), not the legacy WebForms user controls. I took a look at the source of ViewUserControl.RenderView() and found that the rendering is actually done by creating a fake ViewPage, and that the ViewState validation in this ViewPage was the one failing. But, since in MVC we don't use ViewState, we can just turn it off. So I created this little base class to do that, just inherit from this one instead of ViewUserControl<T>:

public class ViewUserControlWithoutViewState<T> : ViewUserControl<T> where T : class {
    protected override void LoadViewState(object savedState) {}

    protected override object SaveControlState() {
        return null;
    }

    protected override void LoadControlState(object savedState) {}

    protected override object SaveViewState() {
        return null;
    }

    /// <summary>
    /// extracted from System.Web.Mvc.ViewUserControl
    /// </summary>
    /// <param name="viewContext"></param>
    public override void RenderView(ViewContext viewContext) {
        viewContext.HttpContext.Response.Cache.SetExpires(DateTime.Now);
        var containerPage = new ViewUserControlContainerPage(this);
        ID = Guid.NewGuid().ToString();
        RenderViewAndRestoreContentType(containerPage, viewContext);
    }

    /// <summary>
    /// extracted from System.Web.Mvc.ViewUserControl
    /// </summary>
    /// <param name="containerPage"></param>
    /// <param name="viewContext"></param>
    public static void RenderViewAndRestoreContentType(ViewPage containerPage, ViewContext viewContext) {
        string contentType = viewContext.HttpContext.Response.ContentType;
        containerPage.RenderView(viewContext);
        viewContext.HttpContext.Response.ContentType = contentType;
    }

    /// <summary>
    /// Extracted from System.Web.Mvc.ViewUserControl+ViewUserControlContainerPage
    /// </summary>
    private sealed class ViewUserControlContainerPage : ViewPage {
        // Methods
        public ViewUserControlContainerPage(ViewUserControl userControl) {
            Controls.Add(userControl);
            EnableViewState = false;
        }

        protected override object LoadPageStateFromPersistenceMedium() {
            return null;
        }

        protected override void SavePageStateToPersistenceMedium(object state) {}
    }
}
DISCLAIMER: it worked for my specific case, I'm not claiming this covers all possible cases of this exception! Hopefully this problem will be addressed soon by Phil et al...

12 comments:

Anonymous said...

thank you! :)

Bart McLeod said...

This does no longer work with the latest release of MVC (in fact: the first RTM), while the bug still persists.

I wonder how everybody is dealing with this. Are we just not using usercontrols?

Mauricio Scheffer said...

@Bart: thanks, I haven't upgraded to RTM yet, when I do I'll try to update this workaround. I guess Microsoft decided that supporting legacy usercontrols isn't so important.

Mauricio Scheffer said...

@Bart: just upgraded to RTM and this same workaround is still working for me... are you sure it's the same exception/scenario?

Anonymous said...

Thank you. Great post.

Abhang Rane said...

Thanks. Its been a while this bug is around and still MVC team has not updated their source code I guess.

Anonymous said...

How do I use this? I am not sure where to put this code.

Thanks

Mauricio Scheffer said...

@Anonymous: put in anywhere in your web project, then make your usercontrols inherit from ViewUserControlWithoutViewState instead of ViewUserControl

Anonymous said...

Dude - thanks a ton this worked for me in a personal hybrid MVC / webforms project I have.

You the man.

Cheers,
Jon

Altaf Khatri said...

This article describes about how to handle postback in ASP.NET MVC:

http://www.altafkhatri.com/Altaf/How-to-get-Ilist-value-from-ASP-net-mvc-postback/Bind-list-to-ASP-NET-MVC-object/Postback-object-list-binding

Mauricio Scheffer said...

@Altaf: this post is about WebForms postbacks in an MVC application. Your article only deals with pure MVC postbacks which is the normal MVC usage. The problems arise when mixing MVC and WebForms.

Ariel said...

Great Post!

Just a comment: initially didn't work for me, ASP.Net complained that it couldn't find the user base class.

What I did was replace, in your class source, this:

public class ViewUserControlWithoutViewState : ViewUserControl where T : class

with this:

// public class ViewUserControlWithoutViewState : ViewUserControl

And it worked.

I didn't dig into that more, but I'm afraid ASP.Net or even Reflection has some trouble finding types with the where syntax.

¡Muchas gracias!