user2014503
user2014503

Reputation: 21

MVC4 Not Posting Object With Nested Recursive Lists

I'm done tearing my hair out, so I'm asking for help. I've build a short recursive HTML Helper to display a list of lists. I want to return any edits back to the controller. The child of a child keeps coming back null. When I remove the recursion and just force through a 2nd layer of a "for loop", it seems to work. Any clue? I'm about to throw in the towel on RAZOR and just learn to do all this in Jquery...

Based on my code, the problem resides here when the model is posted back (you'll see this in the comments in the post ActionResult method):

Node.Name is ok

Node.Children[0].Name is ok

Node.Children[1].Name is ok

Node.Children[1].Children = null (HERE LIES THE PROBLEM!)

CONTROLLER CODE

       public ActionResult Y()
    {
        Node driverTree = new Node() { Name="Level1", Children = new List<Node>() };
        driverTree.Children.Add(new Node() { Children = null, Name = "Level2A" });
        driverTree.Children.Add(new Node() {Name ="Level2B", Children = new List<Node> {new Node{Name="Level3A", Children=null},
                                                                                 new Node{Name="Level3B", Children=null}}
        });
        return View(driverTree);
    }
    [HttpPost]
    public ActionResult Y(Node Node)
    {
        //Keeps returning:
            // Node.Name is ok
            // Node.Children[0].Name is ok
            // Node.Children[1].Name is ok
            // Node.Children[1].Children = null (HERE LIES THE PROBLEM!)
        int x = 5; // 
        return View();
    }
}

public class Node
{
    public string Name { get; set; }
    public List<Node> Children { get; set; }
    public bool IsChecked { get; set; }
}

VIEW CODE

@model JqueryUITests.Controllers.Node
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
<script src="../../Scripts/jquery-ui-1.10.3.min.js" type="text/javascript"></script>
<script src="../../Scripts/jquery-ui.unobtrusive-2.1.0.min.js" type="text/javascript"></script>
}

@helper ShowTree(List<JqueryUITests.Controllers.Node> children)
{
<ul>
    @for (int i = 0; i < children.Count;i++ )
    {
        <li>
            @Html.EditorFor(x => children[i].Name)
            @if (children[i].Children != null)
            {
                @ShowTree(children[i].Children)
            }
        </li>
    }
</ul>
}
@using (Html.BeginForm())
{
<ul id="tree">
    <li>
        @Html.EditorFor(x=>Model.Name)
        @ShowTree(Model.Children)
    </li>
</ul>
<p>
    <input type="submit" value="Create" />
</p>

}

HTML CODE

<form action="/X/Y" method="post">    <ul id="tree">
    <li>
        <input class="text-box single-line" id="Name" name="Name" type="text" value="Level1" />
            <ul>
        <li>
            <input class="text-box single-line" id="children_0__Name" name="children[0].Name" type="text" value="Level2A" />
        </li>
        <li>
            <input class="text-box single-line" id="children_1__Name" name="children[1].Name" type="text" value="Level2B" />
<ul>
        <li>
            <input class="text-box single-line" id="children_0__Name" name="children[0].Name" type="text" value="Level3A" />
        </li>
        <li>
            <input class="text-box single-line" id="children_1__Name" name="children[1].Name" type="text" value="Level3B" />
        </li>
</ul>
        </li>
</ul>

    </li>
</ul>
<p>
    <input type="submit" value="Create" />
</p>

Upvotes: 0

Views: 2279

Answers (3)

cadrell0
cadrell0

Reputation: 17327

Expanding on dav83's remark about a display template. It should look something like this.

File

Views\DisplayTemplates\Node.cshtml

@model JqueryUITests.Controllers.Node

<li>
    @Html.EditorFor(x => x.Name)
    @if (Model.Children != null)
    {
        <ul>
            @Html.Editor(x => x.Children)
        </ul>
    }
</li>

Now in your view put.

@model JqueryUITests.Controllers.Node
@section Scripts 
{
    @Scripts.Render("~/bundles/jqueryval")
    <script src="../../Scripts/jquery-ui-1.10.3.min.js" type="text/javascript"></script>
    <script src="../../Scripts/jquery-ui.unobtrusive-2.1.0.min.js" type="text/javascript"></script>
}

@using (Html.BeginForm())
{
    <ul id="tree">
        @Html.EditorForModel()
    </ul>
    <p>
        <input type="submit" value="Create" />
    </p>
}

MVC will render the HTML using the Node DisplayTemplate for your model. Additionally, when you call @Html.EditorFor(x => x.Children), it will use the display template to render each item in the collection. This is where you get your recursion to build the entire tree.

Additionally, it will keep track of where it is in the tree and name the HTML elements correctly, allowing your tree to post as you would expect.

Upvotes: 0

dav83
dav83

Reputation: 322

The list is rendering an item for each node, but it is not rendering the name attribute for each node properly.

Razor handles lists by using the name attribute to create arrays when the inputs are posted back and the model re-created. Eg

List<Foo> AllMyFoos { new Foo { Id = 1 }, new Foo { Id = 2 }, new Foo { Id = 3 }}

Would be rendered (as per your example above) like this:

<input type="text" name="AllMyFoos[0].Id" />
<input type="text" name="AllMyFoos[1].Id" />
<input type="text" name="AllMyFoos[2].Id" />

Under the hood the model binding in Razor and MVC then recreates the list of Foo objects and passes it to your controller method.

The issue with your recursive method is that when you call it with the list of child nodes, the index of each child node is being rendered and you are losing the info that is defining it as a child of another node.

Does that make sense?

Have a look at this SO question and read the answer about display templates for collection types - it's a better way of achieving what you're after.

SO Display Templates Answer

Upvotes: 2

Nenad
Nenad

Reputation: 26687

Problem is clear:

@Html.EditorFor(x => children[i].Name)

Normally x represents model and then properties are of format x.children[i].Name. Next level would be x.children[i].children[j].Name. EditorFor derives id and name of <input /> field from that expression. In your case expression is always children[i].Name, so it breaks mapping of id/name relative to your root Model.

I'm not sure if there is a good way to make recursive rendering like you want. Perhaps using non-lambda version Html.Editor(string expression), where you would construct expression as a string, taking care of nesting level in your code (ex: @Html.Editor("Children[1].Children[2].Name")).

You would be responsible to generate proper value "Children[1].Children[2].Name".

Upvotes: 1

Related Questions