3

If I google for "multiple models in one view" I can only find results about how I can pass models to a view. But I'm interested in the "from view to controller" direction.

So let's assume:

  • I have 3 different forms and 1 table (WebGrid) in one view.
  • And I have one model per form and one model for the table.
  • Let my model classes be ModelF1, ModelF2, ModelF3 and ModelT.

All the examples I have seen until now uses a container ViewModel like

class MyViewModel {
    ModelF1 inst1,
    ModelF2 inst2,
    ModelF3 inst3,
    ModelT instT
}

And then they pass it between view <-> controller in 2 ways.

But I want to catch my models this way without using a viewmodel:

class MyController {
    ActionResult Index() {
        return new View(modelF1Instance, modelF2Instance, modelF3Instance, modelTInstance);
    }

    ActionResult Form1Action(ModelF1 inst1, ModelT instT) {
        // process models after a form1 submit
    }

    ActionResult Form2Action(ModelF2 inst2, ModelT instT) {
        // process models after a form2 submit
    }

    ActionResult Form3Action(ModelF3 inst3, ModelT instT) {
        // process models after a form3 submit
    }
}

Is this possible without parsing the whole form elements in a CustomBinder?

1

1 Answer 1

1

Firstly you can only send a strongly typed view model back to your view using

return View(model);

View is a method on the base class, not a class to be instantiated with return new View(...

Then to your real question: Yes you can do this, but using a top level ViewModel which contains your different form items is far, far easier in the majority of use cases. The main problem the top level container ViewModel handles really well is value persistence and server-side validation and error messages between round trips.

If you are only worried about the perception of inefficiency from creating a top level ViewModel container, then don't. This is far more efficient than all the workarounds you may have to put in place in order to get well behaved forms working without the top level ViewModel.

There is some example code below. The code below should demonstrate that using models contained within the top level ViewModel is just simpler and neater: some of the forms deliberately don't round trip some of the state. Note the usage of HiddenFor and ModelState.Clear which are both related to what you are trying to do, but even these won't persist the value for inst4.Name for Form4Submit. The various options explored are:

  1. Use a query parameter to denote which form is being posted
  2. Use a different form name, but still with the view model.
  3. Use a redirect-only Action for the form (send new instances, and only part of the viewmodel)
  4. Use a mixture of the above
public class TestController : Controller
{
    //
    // GET: /Test/
    [System.Web.Mvc.HttpGet]
    public ActionResult Index(string msg = null)
    {
        var model = new MyViewModel
        {
            Inst1 = new ModelF1 { Name = "Name of F1" },
            Inst2 = new ModelF2 (),
            InstT = new ModelT {Name = "Name of T"},
            PostNumber = 0,
            Message = msg
        };
        return View(model);
    }

    [System.Web.Mvc.HttpPost]
    public ActionResult Index(MyViewModel model, int option = 1)
    {
        // process models after a form1/2 submit
        model.Message = "You posted " + 
            ((option == 1) ? model.Inst1.Name : model.Inst2.Name)
            + " to Index for "
            + ((option == 1) ? "inst1" : "inst2");
        model.PostNumber ++;
        //  This, and the hiddenFor are required to allow us to update the PostNumber each time
        ModelState.Clear();
        return View(model);
    }

    [System.Web.Mvc.HttpPost]
    public ActionResult Form2Submit(MyViewModel model)
    {
        // process models after a form2 submit
        model.Message = "You posted " + model.Inst2.Name + " to Form2Submit";
        model.PostNumber++;
        ModelState.Clear();
        return View("Index", model);
    }

    [System.Web.Mvc.HttpPost]
    public ActionResult Form3Submit(ModelF3 inst3, ModelT instT)
    {
        // process models after a form3 submit
        var n = instT.Name;
        var msg = "You posted " + inst3.Name + ", " + n + " to Form3Submit";
        //  We no longer have access to pass information back to the view, so lets redirect
        return RedirectToAction("Index", new { msg = msg });
    }

    [System.Web.Mvc.HttpPost]
    public ActionResult Form4Submit(ModelF4 inst4, MyViewModel model)
    {
        // process models after a form4 submit
        var n = model.InstT.Name;
        model.Message = "You posted " + inst4.Name + ", " + n + " to Form4Submit";
        model.PostNumber++;
        ModelState.Clear();
        return View("Index", model);
    }

    public class MyViewModel
    {
        public int PostNumber { get; set; }
        public string Message { get; set; }
        public ModelF1 Inst1 { get; set; }
        public ModelF2 Inst2 { get; set; }
        public ModelT InstT { get; set; }
    }

    public class ModelBase { public string Name { get; set; } }

    public class ModelF1 : ModelBase {}
    public class ModelF2 : ModelBase { }
    public class ModelF3 : ModelBase { }
    public class ModelF4 : ModelBase { }
    public class ModelT : ModelBase { }
}

Then for the multi-form view:

@using MyWebSite.Controllers;
@model TestController.MyViewModel
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
    <body>
        <p>
            @Html.Raw(Model.PostNumber) : @Html.Raw(Model.Message)
        </p>
        <p>
            @Html.LabelFor(m => Model.InstT) : <br />
            @Html.DisplayFor(m => Model.InstT)
        </p> 
        <div>
            <p>Default form submit</p>
            @using (Html.BeginForm())
            {
                <div>
                    @Html.HiddenFor(m => m.PostNumber)
                    @Html.LabelFor(m => Model.Inst1.Name)
                    @Html.TextBoxFor(m => Model.Inst1.Name)
                </div>
                <input type="submit" value="Submit Index" />
            }
        </div>
        <div>
            <p>Use a parameter to denote the form being posted</p>
            @using (Html.BeginForm("Index", "Test", new { option = 2 }))
            {
                <div>
                    @* Omitting these will not persist them between trips
                    @Html.HiddenFor(m => Model.Inst1.Name)
                    @Html.HiddenFor(m => Model.InstT.Name)*@
                    @Html.HiddenFor(m => m.PostNumber)
                    @Html.LabelFor(m => Model.Inst2.Name)
                    @Html.TextBoxFor(m => Model.Inst2.Name)
                </div>
                <input type="submit" value="Submit with option parameter" />
            }
        </div>
        <div>
            <p>Use a different form name, but still use the ViewModel</p>
            @using (Html.BeginForm("Form2Submit", "Test"))
            {
                <div>
                    @Html.HiddenFor(m => Model.Inst1.Name)
                    @Html.HiddenFor(m => Model.InstT.Name)
                    @Html.HiddenFor(m => m.PostNumber)
                    @Html.LabelFor(m => Model.Inst2.Name)
                    @Html.TextBoxFor(m => Model.Inst2.Name)
                </div>
                <input type="submit" value="Submit F2" />
            }
        </div>
        <div>
            <p>Submit with a redirect, and no ViewModel usage.</p>
            @using (Html.BeginForm("Form3Submit", "Test"))
            {
                var inst3 = new TestController.ModelF3();
                <div>
                    @Html.HiddenFor(m => Model.InstT.Name)
                    @Html.LabelFor(m => inst3.Name)
                    @Html.TextBoxFor(m => inst3.Name)
                </div>
                <input type="submit" value="Submit F3" />
            }
        </div>
        <div>
            <p>Submit with a new class, and the ViewModel as well.</p>
            @using (Html.BeginForm("Form4Submit", "Test"))
            {
                var inst4 = new TestController.ModelF4();
                <div>
                    @Html.HiddenFor(m => Model.Message)
                    @Html.HiddenFor(m => Model.PostNumber)
                    @Html.HiddenFor(m => Model.Inst1.Name)
                    @Html.HiddenFor(m => Model.Inst2.Name)
                    @Html.HiddenFor(m => Model.InstT.Name)
                    @Html.LabelFor(m => inst4.Name)
                    @Html.TextBoxFor(m => inst4.Name)
                </div>
                <input type="submit" value="Submit F4" />
            }
        </div>

    </body>
</html>
Sign up to request clarification or add additional context in comments.

1 Comment

Is this not a design flaw that View is knowing about the controller?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.