Reputation: 343
Hello out there in internet land, I have an interesting conundrum for you:
Is it possible to bind a view for creating an object, if that object contains a list of other objects purely using MVC views/partial views?
Man, that came out all complicated like...let me give you a quick code example of what I mean:
Models:
public class ComplexObject
{
public string title { get; set; }
public List<ContainedObject> contents { get; set; }
}
public class ContainedObject
{
public string name { get; set; }
public string data { get; set; }
}
Nice and simple right? Okay, so a strongly typed view for creating one of these is really simple for the "title" property:
something like:
@Html.TextBoxFor(x => x.title)
but I can't figure out a good way to bind a list of "ContainedObjects" using MVC. The closest that I got was to create a strongly-typed IEnumerable partial view with the "List" scaffold template and include that on the page.
Without adding styling etc, the default look of that partial view is:
@model IEnumerable<MVCComplexObjects.Models.ContainedObject>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table>
<tr>
<th>
@Html.DisplayNameFor(model => model.name)
</th>
<th>
@Html.DisplayNameFor(model => model.data)
</th>
<th></th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.name)
</td>
<td>
@Html.DisplayFor(modelItem => item.data)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { /* id=item.PrimaryKey */ }) |
@Html.ActionLink("Details", "Details", new { /* id=item.PrimaryKey */ }) |
@Html.ActionLink("Delete", "Delete", new { /* id=item.PrimaryKey */ })
</td>
</tr>
}
</table>
But frankly, I can't figure out how to include that as bound to the creation of a new ComplexObject. In other words, I can show a list of already existing ContainedObjects by binding as so: @Html.Partial("PartialCreate", Model.contents)
But what I really want I guess, is something like:
@Html.PartialFor("PartialCreate", x => x.contents)
I should note that I didn't have too much trouble coding around this with Javascript (I'll include the code below) but I'd really like to know if there's a way to do this purely with MVC. I'm a recent convert from WebForms (where I'd pretty much just replaced all of my postbacks with AJAX calls anyway) and this sort of thing comes up a lot in projects that I work on.
Anyway, here's how I currently do it:
Html -
Name: <input type="text" id="enterName" />
Data: <input type="text" id="enterData" />
<a id="addItem">Add Item</a>
<ul id="addedItems">
</ul>
<a id="saveAll">Save Complex Object</a>
Javascript -
<script>
var contents = [];
$(document).ready(function () {
$('#addItem').click(function () {
var newItem = { name: $('#enterName').val(), data: $('#enterData').val() };
contents.push(newItem);
$('#addedItems').html('');
for (var i = 0; i < contents.length; i++) {
$('#addedItems').append(
"<li>" + contents[i].name + ", " + contents[i].data + "</li>"
);
}
});
$('#saveAll').click(function () {
var toPost = { title: "someTitle", contents: contents };
$.ajax({
url: '/Home/SaveNew',
type: 'POST',
data: JSON.stringify(toPost),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
success: function (data, textStatus, jqXHR) {
alert("win");
},
error: function (objAJAXRequest, strError) {
alert("fail");
}
});
});
});
</script>
And that's not a terrible solution or anything, I just don't want to have to implement Javascript calls everytime I want to save a new object, but use standard Razr code everywhere else. I'd like to be reasonably consistent across the board.
Has anyone else run into this issue and found a solution?
Upvotes: 4
Views: 13038
Reputation: 103
I recently found myself needing to accomplish the same task and, like you, not wanting to add a bunch of javascript. I'm using MVC4 and, as best I can tell, there doesn't appear to be an out-of-the-box way to bind enumerable properties of a model to a view. :(
However, as you demonstrated in your question, it is possible to retrieve enumerable properties from the model in a view. The trick is just getting the updates back to the controller. Going off of your example models, your view could look like this (you don't need to make a partial):
@model MVCComplexObjects.Models.ComplexObject
<p>
@Html.ActionLink("Create New", "Create")
</p>
@using (Html.BeginForm("SaveNew", "Home", FormMethod.Post))
{
<table>
<tr>
<th>
@Html.DisplayNameFor(model => model.contents[0].name)
</th>
<th>
@Html.DisplayNameFor(model => model.contents[0].data)
</th>
<th></th>
</tr>
@for (int i = 0; i < Model.contents.Count; i++)
{
<tr>
<td>
@Html.TextBox("updatedContents["+i+"].name", Model.contents[i].name)
</td>
<td>
@Html.TextBox("updatedContents["+i+"].data", Model.contents[i].data)
</td>
<td>
@* Got rid of the edit and detail links here because this form can now act as both *@
@Html.ActionLink("Delete", "Delete", new { /* id=item.PrimaryKey */ })
</td>
</tr>
}
</table>
<input type="submit" value="Save" />
}
And your controller action would look like this:
[HttpPost]
public ActionResult SaveNew(ICollection<ContainedObject> updatedContents)
{
foreach (var co in updatedContents)
{
//Update the contained object...
}
return RedirectToAction("Index");
}
Basically, we are defining a new collection object in the view for MVC to pass to your action method upon form submission. The new object ("updatedContents" in this example) is basically the same as the list property ("contents", in this example) that was defined and populated in the ComplexObject model.
This is a bit more work, but does accomplish the goal of not needing any javascript for the post back. Everything can be done with standard MVC.
Upvotes: 1