2
votes

It's been a while since I used Umbraco. Currently setting up an Umbraco 7 instance which has a lot of forms. I'm struggling to handle ModelState correctly. Hopefully someone can explain what I'm doing wrong. I've read a lot of articles these last couple of days and it looks like there are many ways to do this, but nothing I've tried works for me. I'll explain where I am at the moment.

My page controller which uses Ditto to return a strongly typed page model:

    public override ActionResult Index(RenderModel model)
    {
        var customModel = model.Content.As<MyCustomPage>();

        // Init the model that I want to bind to the form
        ViewBag.EditFormModel = new EditFormModel()
        {
            MyProperty = "init value"
        };

        return this.CurrentTemplate(customModel);
    }

My form model:

public class EditFormModel
{
    public int Id { get; set; }

    [System.ComponentModel.DisplayName("Label")]
    [System.ComponentModel.DataAnnotations.Required]
    public string MyProperty{ get; set; }
}

In my view:

@{
    // Get the model returned after postback if available, otherwise the initialised model
    var editFormModel = TempData["EditFormModel"] as LocationEditFormModel ?? ViewBag.EditFormModel as LocationEditFormModel;
}

@Html.Partial("CustomEditForm", editFormModel)

That partial view:

    @model EditFormModel
    @using (Html.BeginUmbracoForm("PostForm", "PostFormSurface", FormMethod.Post))
    {
        @Html.ValidationSummary(false, string.Empty)
        @Html.AntiForgeryToken()
        @Html.EditorFor(m => m.MyProperty)
        <input type="submit" class="btn btn-primary" value="Submit" />
    }

My surface controller:

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult PostForm(LocationSearchFormModel model)
    {
        bool passesServerSideValidation = this.Validate(model);

        if (passesServerSideValidation)
        {
            // Save and perform a redirect
        }
        else
        {
            // I read something that suggested I should clear model state here but it doesn't seem to make a difference
            ModelState.Clear();

            ModelState.AddModelError(string.Empty, "Not valid");
        }

        // Add the model to temp data so we can retain values on postback
        TempData["SearchFormModel"] = model;

        return CurrentUmbracoPage();
    }

The problem is that when I submit the form, at the point I hit the line return CurrentUmbracoPage(); I can observe that ModelState.IsValid is false as I'd expect. But as I step through, the next line hit is the Index method in the page's controller. Immediately ModelState.IsValid is now true. Therefore my validation summary is not displayed when the form re-loads.

So clearly I'm doing something wrong here, but nothing I've read yet has pointed me in the right direction. I'm appreciate your advice.

2

2 Answers

3
votes

Found a solution.

In my setup I have a form that is rendered through a RenderMvcController subtype. The model for it inherits from RenderModel.

I am posting to a SurfaceController subtype. The model for this can't inherit RenderModel, as it needs the IPublishedContent that isn't available on post.

The problem when there are validation errors is that I need to do what my RenderMvcController does, based on the model the SurfaceController got, without leaving the Controller context that holds the ModelState.

I found that the SurfaceController holds a property CurrentPage that IS the IPublishedContent needed to build the instance of RenderModel to render the View. Rendering that View without leaving the Controller context will keep the ModelState available to the View.

Umbraco (or MVC) is even smart enough to reuse the posted values, without the need to copy them in code.

Making a full code example is too much work at the moment. So let me just share the action on the SurfaceController. (Or rather, an anonymized version of it.)

[HttpPost]
public ActionResult Index(MySurfaceControllerModel model)
{
    if (!ModelState.Isvalid)
    {
        return View("MyView", new MyRenderModel(CurrentPage));
    }

    // Do some stuff with my valid model.

    return RedirectToAction("thenextpage");
}

I hope this will keep others from looking for a way to do it as long as I have.

0
votes

What we're basically doing is to return CurrentUmbracoPage() when form is not valid and just perform RedirectToCurrentUmbracoPage() when it's valid and it's not required to keep values for re-posting the form for example. We're using ModelState to check if model is valid.

Your code may be simplified to something like that:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult PostForm(LocationSearchFormModel model)
{
    if (ModelState.IsValid == false || !this.Validate(model))
        return CurrentUmbracoPage();

    TempData["SearchFormModel"] = model;

    return RedirectToCurrentUmbracoPage();
}

It's not tested, byt probably will help you deal with this problem.