Question

I've been banging my head on this issue for several days now and can't figure it out. I need to create a view that allows users to add and remove complex hierarchical objects. Bogus example:

Models

public class ParentModel
{
    public int ParentId { get; set; }
    public string ParentText { get; set; }
    public IList<ChildModel> Children { get; set; } 
}
public class ChildModel
{
    public int ChildId { get; set; }
    public string ChildText { get; set; }
    public IList<GrandChildModel> GrandChildren { get; set; }
}
public class GrandChildModel
{
    public int GrandChildId { get; set; }
    public string GrandChildText { get; set; }
}

Now, assuming I have an action that populates a ParentModel and it's child properties, a simple view could look like this:

@model EditorTemplateCollectiosns.Models.ParentModel
@using (@Html.BeginForm())
{
    <div>
        @Html.TextBoxFor(m => m.ParentId)
        @Html.TextBoxFor(m => m.ParentText)<br/>
        Children:<br/>
        @for (var i = 0; i < Model.Children.Count; i++)
        {
            @Html.TextBoxFor(m => Model.Children[i].ChildId)
            @Html.TextBoxFor(m => Model.Children[i].ChildText)
            <div>
                Grandchildren:<br/>
                <table id="grandChildren">
                    <tbody>
                        @for (var j = 0; j < Model.Children[i].GrandChildren.Count; j++)
                        {
                            <tr>
                                <td>@Html.TextBoxFor(m => @Model.Children[i].GrandChildren[j].GrandChildId)</td>
                                <td>@Html.TextBoxFor(m => @Model.Children[i].GrandChildren[j].GrandChildText)</td>
                            </tr>
                        }
                    </tbody>
                </table>
            </div>
        }
    </div>
    <button type="button" id="addGrandchild">Add Grandchild</button>
    <input type="submit" value="submit"/>
}
<script type="text/javascript">
    $(document).ready(function() {
        $("#addGrandchild").click(function() {
            $("#grandChildren tbody:last").append('<tr><td><input type="text" name="grandChildId" /></td><td><input type="text" name="grandChildText" /></td></tr>');
        });
    });
</script>

This view works fine unless I add a grandchild and submit the form, in which case it does not bind to the list of GrandChildModel. Fiddle of the POST:

Post after adding grandchild

So I tried using an editor template for GrandChildModel, just to see if it would work. This functions nicely for simply displaying the objects, but I have no idea how I would add a grandchild to the list and have it display in the editor template. I also tried nesting partial views loaded with $.ajax calls, but that turned into a logistical nightmare.

In the actual application, the hierarchy is deeper than this example, and each successive child object needs to have this add/remove capability. Is this even possible without a full post of the form every time the user wants to add or remove an object? With the approach in the example above, is it possible to have newly added grandchildren bind to the IList on POST? Alternatively, is there a way I can add an object on the client-side and have it display in an editor template and bind to the IList on POST? If neither option is the right approach, what is?

UPDATE
I've figured out something hacky that might work, but I would still love to know if there's an easier way to do this. I changed the above view code to this:

@model EditorTemplateCollectiosns.Models.ParentModel
@using (@Html.BeginForm())
{
    <div>
        @Html.TextBoxFor(m => m.ParentId)
        @Html.TextBoxFor(m => m.ParentText)<br/>
        Children:<br/>
        <button type="button" id="addChild" onclick="addChildRow()">Add Child</button>
        <table id="children">
            <tbody class="childTableBody">
                @for (var i = 0; i < Model.Children.Count; i++)
                {
                    <tr>
                        <td colspan="3">Child @i</td>
                    </tr>
                    <tr class="childInfoRow">
                        <td>
                            @Html.TextBoxFor(m => Model.Children[i].ChildId)
                        </td>
                        <td>
                            @Html.TextBoxFor(m => Model.Children[i].ChildText)                            
                        </td>
                        <td>
                            <button type="button" id="removeChild@(i)" onclick="removeChildRow(this)">Delete</button>
                        </td>
                    </tr>
                    <tr>
                        <td colspan="3">
                            Grandchildren:<br/>
                            <button type="button" id="addGrandchild@(i)" onclick="addGrandchildRow(this)">Add Grandchild</button>
                            <table id="grandChildren@(i)">
                                <tbody>
                                    @for (var j = 0; j < Model.Children[i].GrandChildren.Count; j++)
                                    {
                                        <tr>
                                            <td>@Html.TextBoxFor(m => @Model.Children[i].GrandChildren[j].GrandChildId)</td>
                                            <td>@Html.TextBoxFor(m => @Model.Children[i].GrandChildren[j].GrandChildText)</td>
                                            <td><button type="button" id="removeGrandchild@(i)_@(j)" onclick="removeGrandchildRow(this)">Delete</button></td>
                                        </tr>
                                    }
                                </tbody>
                            </table>
                        </td>
                    </tr>
                }
            </tbody>
        </table>
    </div>
    <input type="submit" value="submit"/>
}

Then, I added the following javascript. I can't use a class and jQuery click event handler as re-binding the handler after each add/remove doesn't seem to work right:

<script type="text/javascript">
    function addChildRow() {
        var newRowIndex = $("#children tr.childInfoRow").length;
        var newRow = '<tr><td colspan="3">Child ' + newRowIndex + '</td></tr><tr class="childInfoRow"><td><input type="text" name="Children[' + newRowIndex + '].ChildId" /></td>' +
            '<td><input type="text" name="Children[' + newRowIndex + '].ChildText" /></td>' +
            '<td><button type="button" id="removeChild' + newRowIndex + '" onclick="removeChildRow(this)">Delete</button></td></tr>' +
            '<td colspan="3">Grandchildren: <br/><button type="button" id="addGrandchild' + newRowIndex + '" onclick="addGrandchildRow(this)">Add Grandchild</button>' +
            '<table id="grandChildren' + newRowIndex + '"><tbody></tbody></table></td></tr>';
        $("#children tbody.childTableBody:last").append(newRow);
    }
    function addGrandchildRow(elem) {
        var childIndex = $(elem).attr('id').replace('addGrandchild', '');
        var newRowIndex = $("#grandChildren" + childIndex + " tr").length;
        var newRow = '<tr><td><input type="text" name="Children[' + childIndex + '].GrandChildren[' + newRowIndex + '].GrandChildId" /></td>' +
            '<td><input type="text" name="Children[' + childIndex + '].GrandChildren[' + newRowIndex + '].GrandChildText" /></td>' +
            '<td><button type="button" id="removeGrandchild' + childIndex + '_' + newRowIndex + '" onclick="removeGrandchildRow(this)">Delete</button></td></tr>';
        $("#grandChildren" + childIndex + " tbody:last").append(newRow);
    }
    function removeChildRow(elem) {
        $(elem).closest('tr').prev().remove();
        $(elem).closest('tr').next().remove();
        $(elem).closest('tr').remove();
    }
    function removeGrandchildRow(elem) {
        $(elem).closest('tr').remove();
    }
</script>

So far, this is working, and the array values seem to be binding correctly. However, I'm not a huge fan of the huge pile of stuff required to add a row to either table. Does anyone have a better idea?

Was it helpful?

Solution 2

I ended up using the solution in the edit to my original post. It's a ton of javascript and took a long time to test, but it works and is the only way I could find. If anyone is interested in the full solution, comment here and I will post it.

OTHER TIPS

This is easier to accomplish if you stick with just one model. Instead of having ParentModel, ChildModel and GrandChildModel just have a HierarchichalPersonModel.

That way you would just need one view and one custom editor template. Ajax would be a little harder, but not the logistical nightmare you described.

Update: Added Examples

Person Model:

public class Person
{
    public int Id { get; set; }
    public string Text { get; set; }
    public List<Person> Children { get; set; }
}

Fake Repository:

public static List<Person> PersonCollection { get; set; }

public static Person FindById(int id, List<Person> list = null)
{
    if (list == null)
        list = PersonCollection;

    foreach (var person in list)
    {
        if (person.Id == id)
            return person;

        if (person.Children != null)
            return FindById(id, person.Children);
    }

    return null;
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top