4

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?

tereško
  • 56,151
  • 24
  • 92
  • 147
FrankieAvocado
  • 333
  • 1
  • 3
  • 10
  • I don't need one for the javascript approach, since it doesn't ever "submit". – FrankieAvocado Oct 19 '12 at 14:25
  • Why not serialize the form in your ajax post? – Maess Oct 19 '12 at 15:21
  • I'm not sure if I'm following you here @Maess, the AJAX portion works fine here and since I don't have a form I don't need to serialize it, I just build a JSON object and send it to my controller/action. What I'm looking for is a way to do this purely via Razor syntax and object binding. – FrankieAvocado Oct 19 '12 at 15:29
  • If you don't want to post a form, there isn't a pure razor way to do this. You could post a for with ajax from razor, but it will require a form. Otherwise, your solution is fine. – Maess Oct 19 '12 at 15:43
  • Frankie, I'm running into precisely this situation and people keep sending me to the same 4 year old mvc 2 post that talks about something "close"... I posted my question here http://stackoverflow.com/questions/15373158/mvc-client-side-validation-for-a-model-that-has-a-list-of-models if you find a solution I'll be very interested! – Sébastien Richer Mar 13 '13 at 17:23

2 Answers2

3

Read this article:

http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx

Then read this one:

http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/

The articles are for MVC2, but will work in 3 and 4.

danludwig
  • 45,241
  • 21
  • 150
  • 230
  • 2
    The second article was more helpful than the first, but neither are really what I'm looking for. The first article focused on how to submit a static-sized list rather than an arbitrarily sized one. The second article described how to edit a list of items, but not how to create a brand new object that contained a variable sized list of items. I can see, however, how I might be able to hijack the pattern to generate a dynamic input list for submission. I'm giving you a vote-up for now, but I don't think this is a real "solution" to the problem. – FrankieAvocado Oct 19 '12 at 14:23
1

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.

Savantes
  • 83
  • 6