ASP NET MVC - Model Create With List of Model
This post runs through how to create a form for creating a model with a list of child models using MVC 5 and Razor.
I've been rather quiet on the blogging front lately, I've been working on getting my MVC 5 quiz application up and running on Azure, it's here if anyone wants to give it a go.
One thing I wanted to do while working on the site was allow for a model to be created along with a list of "sub" models; for example when creating a question it would be nice for a user to also create the answers on the same page.
Setup
To demo this we need a model which could "own" some collection of other items. I happened on the perfect example today in the Json.NET source:
From here
Yep, that's a rabbit with a pancake on its head.
Let's create our simple classes for this scenario:
namespace SandboxMvc.Pocos
{
using System;
using System.Collections.Generic;
public class Rabbit
{
public int Id { get; set; }
public string Name { get; set; }
public int IrritationLevel { get; set; }
public IList<Pancake> Pancakes { get; set; }
}
public class Pancake
{
public int Id { get; set; }
public int Thickness { get; set; }
public bool IsCrepe { get; set; }
}
}
Controllers
We need two controller actions, first the create action get and post for the rabbit itself and then the partial view action for a pancake.
namespace SandboxMvc.Controllers
{
using SandboxMvc.Pocos;
using System.Collections.Generic;
using System.Web.Mvc;
public class RabbitController : Controller
{
[HttpGet]
public ActionResult Create()
{
Rabbit rabbity = new Rabbit { Pancakes = new List<Pancake>() };
return View(rabbity);
}
[HttpPost]
public ActionResult Create(Rabbit model)
{
if (!ModelState.IsValid) return View(model);
Session["Rabbit"] = model;
return RedirectToAction(actionName: "Create");
}
}
}
I'm using the session for persistence here because I'm exceedingly lazy.
Views
Let's get Razor to scaffold our Create view first:
@model SandboxMvc.Pocos.Rabbit
@{int index = Model.Pancakes.Count;}
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.IrritationLevel, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.IrritationLevel, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.IrritationLevel, "", new { @class = "text-danger" })
</div>
</div>
<!-- PANCAKES! -->
<div id="pancake-group">
<h4>Pancakes</h4>
</div>
@Ajax.ActionLink(linkText: "+", actionName: "_CreateFields", controllerName: "Pancake", routeValues: new { index = index }, ajaxOptions: new AjaxOptions
{
InsertionMode = InsertionMode.InsertAfter,
UpdateTargetId = "pancake-group",
OnSuccess = "indexIterate"
},
htmlAttributes: new { id = "addPancake", @class = "btn btn-default" })
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
The change we've made is where the comment excitably declares PANCAKES!.
We add a div <div id="pancake-group">
as an update target for our Ajax.
We then include the following Ajax.ActionLink
:
@Ajax.ActionLink(linkText: "+", actionName: "_CreateFields", controllerName: "Pancake",
routeValues: new { index = index },
ajaxOptions: new AjaxOptions
{
InsertionMode = InsertionMode.InsertAfter,
UpdateTargetId = "pancake-group",
OnSuccess = "indexIterate"
},
htmlAttributes: new { id = "addPancake", @class = "btn btn-default" })
This simply calls the _CreateFields
action on our Pancake controller. It passes the index to add as a route value.
Of particular importance are the OnSuccess
value of the AjaxOptions and the id
of the htmlAttributes. These allow us to use the following Javascript to increment our link target:
var currentIndex = 0;
function indexIterate() {
var newHref = $("#addPancake").attr("href");
var newerHref = newHref.replace(/(?:index=)[0-9]+/i, "index=" + ++currentIndex);
$("#addPancake").attr("href", newerHref);
};
Also notice the @index
set near the top of the view code!
Pancake Controller
Now we add our pancake controller action and View:
public class PancakeController : Controller
{
public ActionResult _CreateFields(Rabbit model, int? index)
{
ViewBag.Index = index ?? 0;
return PartialView(model);
}
}
This returns the partial view:
@model SandboxMvc.Pocos.Rabbit
@{
int i = ViewBag.index ?? 0;
}
<div class="form-group">
@Html.LabelFor(model => model.Pancakes[i].Thickness, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Pancakes[i].Thickness, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Pancakes[i].Thickness, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Pancakes[i].IsCrepe, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.CheckBoxFor(model => model.Pancakes[i].IsCrepe, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Pancakes[i].IsCrepe, "", new { @class = "text-danger" })
</div>
</div>
Compile this and run it and you should be able to create all the pancakes you want on your rabbit.
Model Validation
When you have to use server side validation of your model, your sub items will disappear on validation if the validation fails.
Resolving this is luckily fairly simple. For your Create view for Rabbit add the following code inside your pancake-group
div:
@for (int i = 0; i < index; i++)
{
@Html.Action(actionName: "_CreateFields", controllerName: "Pancake", routeValues: new { index = i })
}
Conclusion
This tutorial showed how to add a list of models to the create for of another model and have that list submitted on post in addition to server side validation.
The full code for the files featured can be found as follows: