Reinoud van Dalen

November 12, 2014

Ultimate fix for multiple forms on one page with Sitecore MVC

And I say ultimate because others have fixed this before me, but I think this solution is the most complete. 

Having 2 forms on one page is a problem

If we have 2 forms on a MVC page and we submit, all controller actions are evaluated and those decorated with the HttpPost ActionMethodSelector will be selected over those without it. This causes all form submission actions to be handled while you actually only want to handle the one that is submitted by the visitor.

Possible fix: a custom ActionMethodSelector

For a complete understanding of the problem and possible fixes you should visit: http://ctor.io/posting-forms-in-sitecore-controller-renderings-another-perspective/. Kevin Brechbühl has done a great job summarizing and explaining the options.

But lo and behold: the Ultimate fix!

This post inspired me to perfectionalize the suggested method. Instead of specifying the controller and action, wouldn’t it be easier to add a hidden input with the Rendering UniqueId and validate that in the custom MethodSelector? Hell yes, and as added bonus: you can add the same form twice on a page and MVC will be able to call the submission action just for the submitted form. Don’t know why or where you would need that, but yes.. yes we can!

Great! So what do we need?

SitecoreHelper RenderingToken extension

In your form you will need to call a method which will add the hidden input containing the rendering uniqueId like so:

@Html.Sitecore().RenderingToken()

The Extension class looks like this:

public static class SitecoreHelperExtensions
{
public static MvcHtmlString RenderingToken(this SitecoreHelper helper)
{
if (helper.CurrentRendering == null) return null;

var tagBuilder = new TagBuilder("input");
tagBuilder.Attributes["type"] = "hidden";
tagBuilder.Attributes["name"] = "uid";
tagBuilder.Attributes["value"] = helper.CurrentRendering.UniqueId.ToString();

return new MvcHtmlString(tagBuilder.ToString(TagRenderMode.SelfClosing));
}
}

The result of this method:

<input name="uid" type="hidden" value="09f203da-ee7f-44dd-9959-2de246e077a6" />

ValidRenderingTokenAttribute

And you need to decorate you post action methods with an extra attribute:

[HttpPost]
[ValidRenderingToken]
public ActionResult ContactForm(ContactFormModel model)
{
model.Message = "Valid rendering token found right here!";
return View(model);
}

This attribute looks like:

public class ValidRenderingTokenAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
var rendering = RenderingContext.CurrentOrNull;
if (rendering == null) return false;

Guid postedId;
return Guid.TryParse(controllerContext.HttpContext.Request.Form["uid"], out postedId) && postedId.Equals(rendering.Rendering.UniqueId);
}
}

You could also choose to register the attribute globally, adding it to all actions. This way you do not need to add the attribute to every post method but it also means that it creates a bit of overhead for each action. In that case the attribute should look like:

public class ValidRenderingTokenAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
//If it's not a post request then we do not get to vote, we do not want to mingle in non-post requests
if (!controllerContext.HttpContext.Request.GetHttpMethodOverride()
.Equals(HttpVerbs.Post.ToString(), StringComparison.OrdinalIgnoreCase)) return true;

//If no uid is posted then we do not get to vote either, we do not want to obstruct those who choose to live without the uid token
if (string.IsNullOrEmpty(controllerContext.HttpContext.Request.Form["uid"])) return true;

//Ok, so now we know:
//- It's a POST request
//- An uid is posted so a compare validation is intended

//Now we get to vote based on posted uid compared to current rendering uid
Guid postedId;
var renderingContext = RenderingContext.CurrentOrNull;

return renderingContext != null && //we must have a rendering
Guid.TryParse(controllerContext.HttpContext.Request.Form["uid"], out postedId) && //we must have a valid posted uniqueId
postedId.Equals(renderingContext.Rendering.UniqueId); //the id's must match
}
}

And that’s it!

Credits to Kevin Brechbühl and Mike Edwards since they started this idea. Hope this helps anyone using multiple forms in Sitecore MVC.

TAGS: sitecore mvc


Comments
Kevin Brechbühl

Hi Reinoud, very nice and great idea to use the unique id for this. This is truly the ultimate fix for the existing solutions :) Greets, Kevin


Chandra Prakash

Hi Reinoud, simple and small solution for a complex problem. To handle complex scenarios like Form Validation, Multiple Form and Page after POST processing, I have blogged here. http://cprakash.com/2014/11/03/form-post-in-sitecore-mvc/


Mihaela

This is the most elegant solution I've seen so far for this issue, thank you!


Nathanael Mann

Kevin & Mike put me on to this as a solution - great job :D


Chris

Will this work when the Controller Rendering is rendered with a Placeholder? Seems the Attribute by returning false causes the Html.Sitecore().Placeholder() to throw an error because it tries to call the Action named in the Rendering? Am I missing something? Thanks


Reinoud

I'm not sure what you are trying here. If you have a ContactForm controller rendering then you need 2 ContactForm actions. One for HttpGet and one for HttpPost. The post one you decorate with the validation attribute as well. I'll send you an email so can send over some (code) details.


Chris Etter

Just figured I'd post the solution (thanks Reinoud), my issue was I had [HttpGet] attributes on the default functions, and because the ValidToken attribute was blocking the [HttpPost] call, Sitecore complained that it could not call the function it wanted. Removing the [HttpGet] attribute fixed it. Thanks for you help


Kamruz Jaman

Did you know that this exact same code is also posted here: https://blog.horizontalintegration.com/2015/09/25/sitecore-mvc-multiple-forms/ I don't think that's you....


Reinoud

Hi Kamruz, thanks for the heads up. I've seen it before yes and he's not the only one who has copied this post. Time to find that shame bell ;-)


Jake Kula

Hi Reinoud, this works absolutely brilliantly!! Many thanks for posting this solution!