Sitecore MVC Form Post – Simplified

During Sitecore User Virtual Summit 2014 Kern Herskind presented Sitecore MVC implementation and discussed on various aspect of it. It took me to surprise during discussion on MVC Form Post when I found that my Form Post article was listed in that, he talked about the short coming of my approach which was a valid one due to various concepts involved in implementation.

A discussion with him made me look at other possibilities. I feel that my solution has little overhead in implementation and various part needs to be connected properly to make it work. I looked at below 3 approaches and all of them have pros and cons but 1st one seems promising, at least it has huge improvement over previous one and it reduces the various wire up point which I had in implementation.

Lets discuss these approaches in details and their pros and cons.

Page Post and execute GET request

The idea behind this method is to let execute the Form Post but not write on the output stream and rather hold the result in buffer and execute page once again by changing the request type as HTTP GET. Once it reaches the method which is equivalent to POST, instead of executing the action method it will assign the Form Post result from buffer, all other method will continue as it is.

  1. Process

    • Execute the Form Post but hold the result
    • Return PageContext.Current.PageView as view result to execute the page
    • Add POST method result instead of executing corresponding action method’s GET
  2. Pros

    • Seamlessly handle the form validation, success or redirects
    • Allow HTTP Verbs on the action method
  3. Cons

    • Semantically incorrect to call GET during POST processing
    • Both method name should be same but alternative is to specify the GET method as part of ImportMethodName property

ExportResult & ImportResult attribute

    public abstract class ResultTransfer : ActionFilterAttribute
    {
        protected static readonly string Key = typeof(ResultTransfer).FullName;
        protected static readonly string OriginalRequestTypeKey = Key + "_OriginalRequestType";

        protected virtual string CreateResultKey(ControllerContext ctrlContext, string actionMethodName)
        {
            return Key + "_" + ctrlContext.Controller.GetType().Name + "_" + actionMethodName;
        }
    }

    public class ExportResult : ResultTransfer
    {
        public string ImportMethodName { get; set; }

        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            //Don't Export if we are redirecting
            if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult))
                return;

            if (!filterContext.HttpContext.Request.RequestType.Equals("POST", System.StringComparison.OrdinalIgnoreCase))
                return;

            // Keep the result from post method
            var resultKey = this.CreateResultKey(filterContext, string.IsNullOrWhiteSpace(ImportMethodName) ? filterContext.ActionDescriptor.ActionName : ImportMethodName);
            filterContext.HttpContext.Items[resultKey] = filterContext.Result;

            // Start the GET request for page
            filterContext.HttpContext.Items[OriginalRequestTypeKey] = filterContext.HttpContext.Request.RequestType;
            this.SetHttpMethod("GET");
            
            IView pageView = PageContext.Current.PageView;
            filterContext.Result = new ViewResult { View = pageView };
            
            base.OnActionExecuted(filterContext);
        }

        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            // Restore the original request type
            var originalRequestType = filterContext.HttpContext.Items[OriginalRequestTypeKey] as string;
            if (!string.IsNullOrWhiteSpace(originalRequestType))
                this.SetHttpMethod(originalRequestType);

            base.OnResultExecuted(filterContext);
        }

        private void SetHttpMethod(string httpMethod)
        {
            var a = System.Web.HttpContext.Current.Request.GetType().GetField("_httpMethod", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            a.SetValue(System.Web.HttpContext.Current.Request, httpMethod);
            System.Web.HttpContext.Current.Request.RequestType = httpMethod;
        }
    }

    public class ImportResult : ResultTransfer
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            if (!filterContext.HttpContext.Request.RequestType.Equals("GET", System.StringComparison.OrdinalIgnoreCase))
                return;

            var requestItems = filterContext.HttpContext.Items;

            var resultKey = this.CreateResultKey(filterContext, filterContext.ActionDescriptor.ActionName);
            var originalRequestType = requestItems[OriginalRequestTypeKey] as string;

            if ("POST".Equals(originalRequestType, System.StringComparison.OrdinalIgnoreCase) && requestItems[resultKey] != null)
                filterContext.Result = requestItems[resultKey] as ActionResult;

            base.OnActionExecuting(filterContext);
        }
    }

Example



        [HttpGet]
        [ImportResult]
        public ActionResult Index()
        {
            var model = new SearchModel();
            return View(model);
        }

        [HttpPost]
        [ExportResult(ImportMethodName="Index")]
        public ActionResult Search(SearchModel search)
        {
            if (ModelState.IsValid)
            {
                search.Result = new List<string> { "Hello!", "Hi!!!" };
            }

            return View(search);
        }

Page Post without ExecuteFormHandler

This is same as Kevin B.’s solution and ValidateFormHandler will take care of all logic.

  1. Process

    • ExecuteFormHandler is not required because Page is handling the POST.
    • Decorate all POST method with ValidateFormHandler
    • Both action method name should be same
  2. Pros

    Handle scenarios like Form Validation, failure and success.

  3. Cons

    • Doesn’t allow HttpGet attribute on action method – this assumes that rendering will act safe during POST
    • Execute GET method during POST
    • POST should have same name as GET
public ActionResult Index()
{
 // return view;
}

[HttpPost, ValidateFormHandler]
public ActionResult Index()
{
  // return view;
}

Always redirect in Post – (Follow PRG)

  1. Process

    • Execute the FormPost pipeline
    • Always do redirect from POST method
    • Export ModelState for failure scenario, success scenario redirect with query string
    • In Get method add the model state to display failure
  2. Pros

    • Allows to define GET on action method
    • Can POST to a method which is not defined as rendering on Page.
  3. Cons

    • Decorate both action method with attributes
    • Always redirect including validation failure
    • If you want to retain the value then use Session or any other cache mechanism.
        [HttpGet]
        [ImportModelState]
        public ActionResult Index(string success)
        {
            var model = new ContactUsModel();
            if (!string.IsNullOrWhiteSpace(success) && success == "true")
            {
                model.Result = "You details has been recorded, we will contact you very soon.";
            }

            return View(model);
        }

        [HttpPost]
        [ExportModelState]
        public ActionResult Index(ContactUsModel model)
        {
            if (ModelState.IsValid)
            {
                // -To Do- Save the model data into data store
                return Redirect(ControllerContext.HttpContext.Request.RawUrl + "?success=true");
            }

            return Redirect(ControllerContext.HttpContext.Request.RawUrl);
        }

ImportModelState & ExportModelState Attribute

Here is the source code for Import/Export Attributes.


    public abstract class ModelStateTransfer : ActionFilterAttribute
    {
        protected static readonly string Key = typeof(ModelStateTransfer).FullName;
    }

    public class ExportModelState : ModelStateTransfer
    {
        public string ImportMethodName { get; set; }

        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            //Only export when ModelState is not valid
            if (!filterContext.Controller.ViewData.ModelState.IsValid)
            {
                //Export if we are redirecting
                if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult))
                {
                    ImportMethodName = string.IsNullOrWhiteSpace(ImportMethodName) ? filterContext.ActionDescriptor.ActionName : ImportMethodName;
                    var privateKey = Key + "_" + filterContext.Controller.GetType().Name + "_" + ImportMethodName;
                    filterContext.HttpContext.Session[privateKey] = filterContext.Controller.ViewData.ModelState;
                }
            }

            base.OnActionExecuted(filterContext);
        }
    }

    public class ImportModelState : ModelStateTransfer
    {
        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            var privateKey = Key + "_" + filterContext.Controller.GetType().Name + "_" + filterContext.ActionDescriptor.ActionName;

            var modelState = filterContext.HttpContext.Session[privateKey] as ModelStateDictionary;
            if (modelState != null)
            {
                //Only Import if we are viewing
                if (filterContext.Result is ViewResult)
                {
                    filterContext.Controller.ViewData.ModelState.Merge(modelState);
                }
                
                //Otherwise remove it.
                filterContext.HttpContext.Session.Remove(privateKey);
            }

            base.OnActionExecuted(filterContext);
        }
    }

Summary
There are other implementation possible which I haven’t included them here and I was trying to simplify the earlier solution. Now usage of these solutions depends upon the problem and complexity of the application. All above options have some limitation and ideal PRG (POST->Redirect->GET) would have been to render the page as is for validation failure and redirect on success but Sitecore’s MVC implementation is little tricky to implement.

The best would be to Sitecore to come out with a standard process for implementation.

Advertisements

About cprakash

A developer, passionate about .Net, Architecture and Security.
This entry was posted in Sitecore, Sitecore MVC and tagged . Bookmark the permalink.

4 Responses to Sitecore MVC Form Post – Simplified

  1. Pingback: Form Post in Sitecore MVC | cprakash

  2. Pingback: SUGCON 2016 in Kopenhagen – Rückblick auf Tag 2 › comspace-Blog

  3. James says:

    trying the ExportResult & ImportResult attribute option because it sounds interesting but the Model is null on the view after the post. When I debug the viewModel is populated and correct but in the cshtml it blows up because the model is null – Ideas?

    • cprakash says:

      Are you setting the model after post? If post action method is able to receive the Model then it’s working fine; the only reason I can think of is that your code is setting the model to null or reassigning to something which is null.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s