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?

5
  • I don't need one for the javascript approach, since it doesn't ever "submit". Commented Oct 19, 2012 at 14:25
  • Why not serialize the form in your ajax post? Commented Oct 19, 2012 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. Commented Oct 19, 2012 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. Commented Oct 19, 2012 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 stackoverflow.com/questions/15373158/… if you find a solution I'll be very interested! Commented Mar 13, 2013 at 17:23

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

1 Comment

This is a super sneaky way of doing it...and I love it.

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.