Sitecore MVC - how to handle multiple forms on page
Asked Answered
B

3

7

I've been looking at Sitecore MVC but I'm stuck at how to handle a case where my page has two controller renderings and each contains a form. I want the individual controllers to handle their HttpPost and return the whole page after post.

I've set up a simple example. Both controllers are similar:

public class ExampleController : Sitecore.Mvc.Controllers.SitecoreController
{
    public override ActionResult Index()
    {
        return View("Index");
    }

    [HttpPost]
    public ActionResult Index(string formPostData)
    {
        ViewBag.SaveForLater = formPostData;
        return Index();
    }
}

The views look like this:

@using Sitecore.Mvc
@using (Html.BeginRouteForm(Sitecore.Mvc.Configuration.MvcSettings.SitecoreRouteName, FormMethod.Post))
{
    @Html.AntiForgeryToken()
    var term = ViewBag.SaveForLater as string;
    if (!string.IsNullOrEmpty(term))
    {
        <p>Submitted: @term</p>
    }
    <p>
        @Html.Sitecore().FormHandler("Example", "Index")
        <input type="text" name="formPostData" placeholder="Enter something" />
        <input type="submit" name="submit" value="Search" />
    </p>
}

With this setup both forms submit their data but the returned page consists only of the partial view and not the whole page.

If I replace the line @Html.Sitecore().FormHandler("Example", "Index") with @Html.Sitecore().FormHandler() then the whole page is returned but the post action for both forms is processed.

Neither scenario is ideal. I must be missing something and would appreciate a pointer.

Bristling answered 15/4, 2014 at 12:48 Comment(0)
A
14

Unfortunately there are multiple ways how you can integrate with Sitecore MVC and Sitecore doesn't provide many best practice examples. One example you can find here.

In our projects we do it a bit different, because we want to use as much as possible the conventions etc. from default ASP.NET MVC. I try to include a complete simple example in this post.

We add two different hidden fields to the form with the current action and the current controller, the view looks like this:

@model Website.Models.TestViewModel

@using (Html.BeginForm())
{
    @Html.LabelFor(model => model.Text)
    @Html.TextBoxFor(model => model.Text)

    <input type="submit" value="submit" />

    <input type="hidden" name="fhController" value="TestController" />
    <input type="hidden" name="fhAction" value="Index" />
}

With this simple ViewModel:

namespace Website.Models
{
    public class TestViewModel
    {
        public string Text { get; set; }
    }
}

Then we have created a custom attribute which checks if the current controller/action is the same as posted:

public class ValidateFormHandler : ActionMethodSelectorAttribute
{
    public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
    {
        var controller = controllerContext.HttpContext.Request.Form["fhController"];
        var action = controllerContext.HttpContext.Request.Form["fhAction"];    

        return !string.IsNullOrWhiteSpace(controller)
            && !string.IsNullOrWhiteSpace(action)
            && controller == controllerContext.Controller.GetType().Name
            && methodInfo.Name == action;
    }
}

The controller actions then gets the new attribute:

namespace Website.Controllers
{
    public class TestController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        [ValidateFormHandler]
        public ActionResult Index(TestViewModel model)
        {
            return View(model);
        }
    }
}

We always return the view resolved by ASP.NET MVC. By convention this is the view with the same name as the action within the folder with the same name as the controller.

This approach works very well for us. If you would like to add the AntiForgeryToken, this also works fine.

Aubrette answered 15/4, 2014 at 14:15 Comment(10)
Thanks @kevin-brechbuhl - so you're actually doing what I'm trying. The line @Html.Sitecore().FormHandler("Example", "Index") basically does exactly what you say (i.e. adds the controller and action names as hidden fields) and seems to cause the behaviour you've modeled with an attribute to occur internally. If I include that line only one of the HttpPost actions is processed. But, at the point I return the view from that action (return View(model); above) I'm losing the rest of the page. It's only the content of that view being rendered in the browser, not the whole page. Any idea why?Bristling
@getsetcode If I remember correctly this is because of the Sitecore FormHandler and the hidden fields from Sitecore. Sitecore only renders this action if the hidden fields from Sitecore are available. This is why we create our own hidden fields and do not use the ones from Sitecore. Try to remove the @Html.Sitecore().FormHandler("Example", "Index").Abeyant
Again, thank you @kevin-brechbuhl for your help. I tried what you recommend and am now encountering the exception System.InvalidOperationException: Could not invoke action method which I guess makes sense because one of the two HttpPost actions is not being hit due to your ValidateFormHandler. I feel I'm close but is that a further step you've missed? It appears Sitecore wants to hit both post actions whether I like it or not :(Bristling
It's a similar result to what this guy found - experimentsincode.com/?p=425 - this is the article I started with this morning and it's what sent me down the @Html.Sitecore().FormHandler() route (see comment at bottom of article). Your thoughts would be much appreciated.Bristling
@getsetcode This blog post was the starting point for our solution. I said at the top of my answer that there are multiple ways to integrate with Sitecore MVC. One is with the @Html.Sitecore().FormHandler() and the Index() view of the SitecoreController. The other way is the way as I described. I edited my post and add a complete example. I've tried this in a clean Sitecore 7.2 solution and it worked fine. You could also add multiple controller renderings with different actions and it should work as well. Can you please try exactly my code again?Abeyant
I am indebted to you. The mistake I'd made was to use Html.BeginRouteForm(Sitecore.Mvc.Configuration.MvcSettings.SitecoreRouteName, FormMethod.Post) instead of Html.BeginForm(). Thank you so much for persisting, I really appreciate it.Bristling
good stuff, thanks for the clear example. i'd upvote this 10 times if i could!Serge
I've searched far and wide on the internet for a solution to post to a different controller. You saved me after a long struggle! <3Armful
imortant thing for this to work is @using (Html.BeginForm()). If we add any parameter to BegineForm, valiation and routes break. Any idea why?Vet
@Vet This is because the url is then different. You can use the parameters, but then you need to specify the "action" as well, to the current request url (the current absolute url). You can specify the action like described here: https://mcmap.net/q/404357/-html-beginform-with-an-absolute-urlAbeyant
M
0

You should create the main Index view having two partial view , like this:

You can define main view model like this

public class MainViewModel
{
    public LoginViewModel LoginModel { get; set; }

    public RegisterViewModel RegisterModel { get; set; }
}

then the separate model:

 public class RegisterViewModel
{
 // Your model properties
   }

public class LoginViewModel
{
  // Your model properties  
}

then define your action for main view as:

 public ActionResult MainView()
    {

        MainViewModel model = new MainViewModel
        {
            LoginModel = new LoginViewModel(),
            RegisterModel = new RegisterViewModel()
        };

        return View(model);
    }

Your main view as

 @Html.Partial("_Login", Model.LoginModel)
  @Html.Partial("_Register", Model.RegisterModel)

after that you can saperately create your views ,thanks

Monocoque answered 15/4, 2014 at 12:56 Comment(2)
Thanks, but can you provide more detail? There isn't a 'main index view'. This is Sitecore so I have 2 MVC views both added to my layout as controller renderings. They each have their own controller. I'm not sure how your suggestion is relevant...?Bristling
@getsetcode point still stands. This isn't a sitecore solution. In normal MVC yes! But sitecore MVC is not normal. lolKaczmarek
P
0

Sitecore Habitat does it similarly as above, but using a unique rendering ID.

public class ValidateRenderingIdAttribute : ActionMethodSelectorAttribute
{
    public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
    {
        var ignoreCase = StringComparison.InvariantCultureIgnoreCase;

        var httpRequest = controllerContext.HttpContext.Request;
        var isWebFormsForMarketersRequest = httpRequest.Form.AllKeys
          .Any(key => key.StartsWith("wffm", ignoreCase) && key.EndsWith("Id", ignoreCase));

        if (isWebFormsForMarketersRequest)
        {
            return false;
        }
        string renderingId;
        if (!httpRequest.GetHttpMethodOverride().Equals(HttpVerbs.Post.ToString(), ignoreCase) || string.IsNullOrEmpty(renderingId = httpRequest.Form["uid"]))
        {
            return true;
        }

        var renderingContext = RenderingContext.CurrentOrNull;
        if (renderingContext == null)
        {
            return false;
        }

        Guid id;
        return Guid.TryParse(renderingId, out id) && id.Equals(renderingContext.Rendering.UniqueId);
    }
}

Link to repo

Proteinase answered 30/5, 2016 at 23:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.