0

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>

4
  • Can you post rendered html markup? Commented Jul 1, 2013 at 20:34
  • Yours: @ShowTree(children[i].Children) is never rendered. Are you sure it's not null during view rendering? Commented Jul 1, 2013 at 20:50
  • I see it on the screen, so it is being rendered. THere's a mess of "li" and "ul" at the bottom of the html markup I pasted (sorry about that). But, visually, it all looks in order. Commented Jul 1, 2013 at 21:10
  • Unrelated to the question. Can you tell me what jquery-ui.unobtrusive-2.1.0.js is used for? I found it in my project. I can't find a clear explanation or website about it online. Commented Sep 6, 2013 at 20:23

3 Answers 3

2

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

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

5 Comments

Thanks. I figured it had to be some Razor thing since when I replaced the recursion with a 2nd for loop, it all looked in order. Would you suggest trying this in jquery to get the markup all situated is a better path? Many thanks.
If you're happier using jQuery then go for it. You need to be able to maintain it (presumably) so whatever you're more comfortable with. I would always have a go at learning something new though, especially in a big framework like MVC. In your position, the questions I would be asking myself are: 'How would I do this in jQuery?' and then 'Why can't I do that in Razor?'.
@dav83 - this is not "some Razor thing", it's an HTTP thing. You're sending duplicate named objects in the post. It really has nothing to do with razor, it's because you aren't using the Html Helpers correctly (which are part of MVC, not Razor).
Thanks, Mystere Man (your comment presumably was directed to me). I can only guess the @EditorFor helper is what I'm not using correctly. Can you suggest what html <input> tag would need to get there to work? I'm certainly not married to html helpers...
I never said it was 'some Razor thing'.
1

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".

2 Comments

There is a good way (or there should be) as I've implemented editors with MVC3 that not only handled complex models but allowed for JavaScript templates and trivial adding/deletion of any model within the complex model without any postback to load the editors... it did take some finagling, though.
It's partially matter of taste. Editing (and posting back) full recursive model in one go, like in this question, doesn't seem to be too user friendly to me. It doesn't even cover adding/removing child nodes.
0

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.

Comments

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.