6

I am new to Sitecore MVC and I am trying to set up a simple form post to work. First I have a content item that has a number of renderings set up in the presentation details. One of those renderings is a Controller Rendering. In the Controller Rendering, Controller = myFeature and Controller Action = Index.

The code for myFeatureController looks like this:

public class myFeatureController : SitecoreController
{
    private readonly IMyRepository _myRepository;

    public myController(IMyRepository myRepository)
    {
        _myRepository = myRepository;
    }

    [HttpGet]
    public ActionResult Index()
    {
        var viewModel = new myViewModel();

        viewModel.Value1 = myRepository.GetValue();

        return View(viewModel);
    }

    [HttpPost]
    public ActionResult SaveValue(myViewModel viewModel)
    {
        _myRepository.SaveData(viewModel.Value1);

        return View(viewModel);
    }
}

And the code for my view looks like this:

@model myViewModel

@using (Html.BeginRouteForm(Sitecore.Mvc.Configuration.MvcSettings.SitecoreRouteName, FormMethod.Post))
{
    @Html.Sitecore().FormHandler("myFeature", "SaveValue")
    @Html.TextBoxFor(x => x.Value1)
    <input type="submit" value="Save" />
}

When I load the page for the first time the Index method runs fine. But when I click the Save button to post the form I get the following error:

The view SaveValue was not found or no view engine supports the searched locations...

Obviously I am doing something wrong here. Any ideas?

Thanks, Corey

7

The issue is that you are not specifying a ViewName in the return of the SaveValue() action, therefore it is trying to locate a View with the name of the action, i.e. myFeature/SaveValue.cshtml. The error is telling you it could not find this file.

You have two options. Either specify the specific view to render for the action:

[HttpPost]
public ActionResult SaveValue(myViewModel viewModel)
{
    _myRepository.SaveData(viewModel.Value1);
    return View("~/Views/myFeature/Index.cshtml", viewModel);
}

Or change the Action Name to match the view (i.e. Index). Since the method signatures are different this will compile and the Accept Verbs Attribute will ensure the correct action is called depending on whether a Get or Post is made:

[HttpPost]
public ActionResult Index(myViewModel viewModel)
{
    _myRepository.SaveData(viewModel.Value1);
    return View(viewModel);
}

Don't forget to change the helper in the form as well:

@Html.Sitecore().FormHandler("myFeature", "Index")

The above is best used with AJAX posts, since you have the issue of only the specific view being returned. You could redirect in your [HttpPost] action to the thank you page (using return this.Redirect("/url/to/redirect-page")) but this causes issues with server side form validation.

using return View will interrupt Sitecore’s page load and return that view only.

...

A controller rendering executes much later in page life cycle than a true ‘controller’. By the time your [HttpPost] action executes, the page is already partially loaded.

I would recommend dealing with this issue in a similar manner to how it is solved in Sitecore Habitat demo.

Create a new attribute that will be used to determine the selection of an action method on form submission:

public class ValidateRenderingIdAttribute : ActionMethodSelectorAttribute
{
    internal const string FormUniqueid = "uid";

    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[FormUniqueid]))
        {
            return true;
        }

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

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

Update your controller action to include this new attribute:

[HttpPost]
[ValidateRenderingId]
public ActionResult Index(myViewModel viewModel)
{
    if (ModelState.IsValid())
    {
        _myRepository.SaveData(viewModel.Value1);
        return View("~/Views/Comments/Thanks.cshtml", viewModel);
    }
    return View(viewModel);
}

Note the above relies on ModelState for validation and checks the data passes validation which may have been set on the model using attributes, and if so returns a different different "Thank you" view (since you probably do not want to show the form again). The model/form data is passed if you want to show the data in the confirmation.

Create a new HTML Helper which returns a unique rendering id hidden field:

public static class HtmlHelperExtensions
{
    public static MvcHtmlString AddUniqueFormId(this HtmlHelper htmlHelper)
    {
        var uid = htmlHelper.Sitecore().CurrentRendering?.UniqueId;
        return !uid.HasValue ? null : htmlHelper.Hidden(ValidateRenderingIdAttribute.FormUniqueid, uid.Value);
    }
}

Finally, change the view to just use a regular MVC BeginRouteForm and add the unique form input helper:

@using (Html.BeginRouteForm(MvcSettings.SitecoreRouteName, FormMethod.Post))
{
    @Html.AddUniqueFormId()
    @Html.TextBoxFor(x => x.Value1)
    <input type="submit" value="Save" />
}

By following this approach you are using an approach which is much closer to standard ASP.Net MVC so hopefully it is easier to follow/workaround the in-built Sitecore problems. It also has the benefit that should you require multiple forms on the same page this this code can handle that too.

You can find examples from the Sitecore Habitat repo: - AddUniqueFormId - ValidateRenderingIdAttribute - Example usage in Rendering View and Controller.

Further Reading

I would recommend reading these posts for further background information:

| improve this answer | |
  • Ok, that sort of worked. However now when I click the submit button it only returns the myFeature view to the screen. It doesn't return any of the other renderings that are defined in the presentation details for the content item. So I don't have the main layout, header or footer. All I am seeing on the screen after I submit is the view of the myFeature component. – Corey Burnett Oct 6 '17 at 13:49
  • I've done a bit more reading on this. As someone that is new to MVC I'm really surprised how difficult it is to post simple forms in Sitecore MVC. You would think that something like a search page would be an extremely normal type of page to have on a site and that this wouldn't be so difficult. Most of what I have read involves hacks and work arounds to get it to function properly. – Corey Burnett Oct 7 '17 at 12:07
  • @CoreyBurnett: Yes, I agree. The simplest workaround is to post using AJAX, but I have updated the answer to provide a solution to deal with the problem. – jammykam Oct 7 '17 at 20:24
  • Well I implemented everything you said. And now I am getting the following error when I submit the form - A public action method 'Index' was not found on controller 'MyProj.Feature.Menu.Controllers.HeaderController'. which is from a different rendering. – Corey Burnett Oct 7 '17 at 22:12
  • This is so complicated to figure out. I am considering maybe just changing my HttpPost controller action to simply redirect back to the same page and pass the selected form values in the query string. Seems silly. But I've spent like 10 hours trying to get a simple form post to work in Sitecore MVC. Something that would take me like 2 minutes using WebForms. :) – Corey Burnett Oct 7 '17 at 23:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.