Update – 01/12/2015- There is a simplified version available here — https://cprakash.com/2015/01/12/sitecore-mvc-form-post-simplified/
This post is 2nd in series of the proposed framework to solve some of the common problem in Sitecore MVC implementation. In this post I am going to talk about the problem with Form Post in Sitecore MVC and how this framework helps to solve it.
There are some talk already in different forums on this topic and it is good to link it here for reference.
-
SDN Forum discussion – http://sdn.sitecore.net/Forum/ShowPost.aspx?PostID=61553
- Martina’s posts on Sitecore MVC Form post – 1- With view rendering 2- With Controller Rendering
- Kevin from Unic has good blog post and a simple solution here
Challenges with current implementation
- Sitecore MVC is not ASP.NET MVC – Yes, Sitecore works differently in terms of MVC, behind the scene it runs a Sitecore controller but for Form Post it simply runs a ExecuteFormHandler pipeline and emit out the response generated by controller/action, which may not be ok, if you are looking for the full page again.
- Form Validation– If your server side model validation fails, it can not get you back to the page with nice error message as like ASP.NET MVC.
- BeginForm is not supported- If you try to use ASP.NET MVC’s BeginForm it will try posting to current page which result into calling Post action on all the available renderings.
- Possible Ajax solution- Yes, it’s a good solution but this gives you additional task of handling JS logic.
As Sitecore executes form post in Request Begin pipeline, it is not possible to get the same page back again unless we execute the Sitecore’s page rendering pipeline, which become tedious to handle. This prompted me to explore the other possibility of solving this problem and the solution resulted into bunch of code but most of it is abstract.
Solution
PRG (Post-Redirect-GET) to rescue, in fact it is a better way to handle the Form Post, it avoids accidental double post and page refresh issues. This solution is based on PRG and uses some of the code from Kazi Manzur‘s MVC Best Practices blog (#13).
How it works
Here is how this solution works.
- Extend the SessionStateTempDataProvider to hold temp data values for request life time and if SitecoreController is executing then keep the temp data value in request’s items object as this data could belongs to one of the controller in execution later.
- On Form Post execution preserve the model state into extended TempData provider, which can be retrieved back on execution after redirect.
- After Form Post execution Redirect to current/new page based on the functionality required. You might need result of the form execution, if yes, store that as well.
- In GET action method, use TempDataModelBinder to retrieve the Model from TempData and initialize the Model object.
- In GET execution, import the model state and return along with view. At this point if you want to make a decision based on GET/POST request, you need additional flag to identify if model is return from Post or not.
Let the code talk
Lets build a sample to see, how it look like.
The controller action method for GET request
The Index action method is dummy and renders the view to accept the input. There is one flag with the model which suggest if the model resulted out of PRG or not. TempDataModelBinder binds the model after PRG and if this is a brand new GET request it will get initialized as like normal MVC. Action method is decorated with ImportModelStateFromTempData attribute which will execute OnActionExecuted method and this achieves the job of sending the ModelState object to view after PRG, if any validation errors are available.
[HttpGet] [ImportModelStateFromTempData] public ActionResult Index([TempDataModelBinder]ContactUsModel model) { if (!model.IsPost) { // Todo- if something is required during get on form post } return View(model); }
Action method generates the View looks like below, it slightly differs from normal MVC views due to usage of @Html.Sitecore().AreaFormHandler() extension helper method which is used to specify the Area/Controller/Action/Namespace which will be used by ExecuteAreaFormHandler pipeline to handle the POST request. It is an extension on top of @Html.Sitecore().FormHandler(). Below example assume you are supplying the parameters manually to AreaFormHander method, but you can use another implementation which pulls up these values from AreaControllerRendering fields. See the definition below.
Current view support both BeginForm and BeginRouteForm with Sitecore Route.
@using Framework.Sc.Extensions.Helpers @model Common.Models.ContactUsModel @*@using (Html.BeginRouteForm(Sitecore.Mvc.Configuration.MvcSettings.SitecoreRouteName, FormMethod.Post, new { role = "form" }))*@ @using(Html.BeginForm()) { @*@Html.Sitecore().AreaFormHandler()*@ @Html.Sitecore().AreaFormHandler("ContactUs", "ContactUsSave", "Common", "Common.Controllers") <div class="form-group"> @Html.LabelFor(m => m.Name, new { @class = "control-label" }) @Html.EditorFor(m => m.Name, new { @class = "form-control" }) @Html.ValidationMessageFor(m => m.Name) </div> <div class="form-group"> @Html.LabelFor(m => m.Email, new { @class = "control-label" }) @Html.EditorFor(m => m.Email, new { @class = "form-control" }) @Html.ValidationMessageFor(m => m.Email) </div> <div class="form-group"> @Html.LabelFor(m => m.PhoneNumber, new { @class = "control-label" }) @Html.EditorFor(m => m.PhoneNumber, new { @class = "form-control" }) @Html.ValidationMessageFor(m => m.PhoneNumber) </div> <button type="submit" class="btn btn-default">Submit</button> <div>@Model.Result</div> }
The Action Method for POST request
The POST action method is pretty much same like normal ones except on 2 points, first one being ExportModelStateToTempData which will fire OnActionExecuted method and store ModelState in TempData to get it back during GET request in PRG. Second one is the Redirect extension method, which accept two parameter, Url (if specified it will redirect to that url else it will redirect to current url) and model object containing result after the method execution.
[HttpPost] [ExportModelStateToTempData] public ActionResult ContactUsSave(ContactUsModel model) { if (ModelState.IsValid) { model.Result = "You details has been recorded, we will contact you very soon."; } return this.Redirect(model); }
Source code for this solution and examples are posted on GitHub.
What you can do
- BeginForm- Yes, you can use it but can not specify controller/action name and let it decide about the POST Url, and make sure you are using AreaFormHandler/FormHandler to let Sitecore know which controller/action to execute for processing.
- Form Validation- Yes, Action Filter will store ModelState in temp data during POST and transfer it to GET method to render in UI. Additional validation message can be added in POST method.
- Multiple Form- Yes, as you are using AreaFormHandler or FormHandler, it will send the POST to appropriate controller/action method.
- Redirect- As this solution is based on PRG pattern, you have to always redirect after POST.
- TempData- Though this solution makes TempData available for complete request, but as in Sitecore Mvc we don’t use TempData so I don’t expect anyone to use it. Currently, it is not tested fully and behavior is unknown; Use it at your own risk.
Disclaimer-
I have tested this solution with 2 simple form on the page with both variation of redirect, BeginForm/BeginRouteForm and AreaFormHandler and it works without any problem. If you have any issue drop me a mail or post your comment.
Pingback: Exception Handling in Sitecore MVC | cprakash
Pingback: Sitecore MVC Form Post – Simplified | cprakash
Where does AreaFormHandler come from?
AreaFormHandler is nothing but a HTML Helper which will emit certain hidden field to identify and process form post with specific area, controller & action.
Source code is here