views:

1540

answers:

4

I have a nested data object for a set of items within categories. Each category can contain sub categories and there is no set limit to the depth of sub categories. (A file system would have a similar structure.) It looks something like this:

class category
{
    public int id;
    public string name;
    public IQueryable<category> categories;
    public IQueryable<item> items;
}
class item
{
    public int id;
    public string name;
}

I am passing a list of categories to my view as IQueryable<category>. I want to output the categories as a set of nested unordered list (<ul>) blocks. I could nest foreach loops, but then the depth of sub categories would be limited by the number of nested foreach blocks. In WinForms, I have done similar processing using recursion to populate a TreeView, but I haven't seen any examples of using recursion within an ASPX MVC view.

Can recursion be done within an ASPX view? Are there other view engines that include recursion for view output?

+9  A: 

You could easily do it by having each <ul> list in a PartialView, and for each new list you need to start you just call Html.RenderPartial("myPartialName");.

So the Category PartialView could look like this:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<Category>>" %>
<% foreach(Category cat in ViewData.Model) { %>
     <li><p><%= cat.name %></p>
        <% if (cat.categories.Count > 0) {
                Html.RenderPartial("Category", cat.Categories);
           } %></li>
<% } %>

In your View, you simply send the "root" collection as the model for the partial view:

<% Html.RenderPartial("Category", ViewData.Model) %>

EDIT:

  • I had forgotten the second parameter to the Html.RenderPartial() call - of course the category has to be passed as the model.
  • Of course you are right about the DRY mistake I made - I have updated my code accordingly.
Tomas Lycken
Inside the partial view, how is it setting cat.categories as the model for the sub category partial view?
Dennis Palmer
The RenderPartial method can take a second parameter to be used as it's model. Personally I wouldn't do the loop on your page, simply pass it your category collection and start the looping there - it's more DRY that way.
Charlino
+4  A: 

Create your own HtmlHelper extension method like so:

namespace System.Web.Mvc
{
    public static class HtmlHelperExtensions
    {
        public static string CategoryTree(this HtmlHelper html, IEnumerable<Category> categories)
        {
            string htmlOutput = string.Empty;

            if (categories.Count() > 0)
            {
                htmlOutput += "<ul>";
                foreach (Category category in Categories)
                {
                    htmlOutput += "<li>"
                    htmlOutput += category.Name;
                    htmlOutput += html.CategoryTree(category.Categories);
                    htmlOutput += "</li>"
                }
                htmlOutput += "</ul>";
            }

            return htmlOutput;
        }
    }
}

Funny you should ask because I actually created one of these just yesterday.

HTHs, Charles

Charlino
I like this code. Seems strange to write such a specialized helper function, but would this perform better than the recursive partial view in Tomas' answer?
Dennis Palmer
I'm 99% certain the performance would be better here. 'RenderPartial' has some overhead where as a simple helper method has as little overhead as you could have.
Charlino
This is also a good solution - however, I'd use the TagBuilder class to generate the HTML. Or at the very least, a StringBuilder, instead of just concatenating strings... ;)
Tomas Lycken
The problem with this approach is that all html is on one line. And if you try to have multiple rows the identation gets wrong. So a PartialView is better for this amount of data. I think HtmlHelper extension methods should only be used for single tags like they are used in the framework.
ericmj
What's wrong with having all the html on one line? Browsers don't care about indentation. Aside from maybe IE6, where if you know anything about IE6 + LI tags + CSS it's better to have them all bunched up. BUT if you're that worried about it, throw in some line breaks etc. You may want to read Rob Conery's 'Avoiding Tag Soup' blog post, he lives by the rule "If there's an IF, make it a helper". Also, the MSDN article "Guiding Principles For Your ASP.NET MVC Applications" as it has a good section on HTML Helpers.
Charlino
A: 

I am a little unsure of how to use the HtmlHelperExtensions above - can some kind person give me a very quick example?

greg
Hi greg, welcome to StackOverflow! It would probably work better for you to post this as it's own question and maybe include a link to this question for reference.
Dennis Palmer
+2  A: 

You can reuse html parts with lambdas

Example


public class Category
    {
        public int id;
        public string name;
        public IEnumerable categories;
    }
 <%
        Action<IEnumerable<Category>> categoriesMacros = null;
        categoriesMacros = categories => { %>
        <ul>
            <% foreach(var c in categories) { %>
                <li> <%= Html.Encode(c.name)%> </li>
                <% if (c.categories != null && c.categories.Count() > 0) categoriesMacros(c.categories);  %>
            <% } %>
        </ul>
        <% }; %>

    <% var categpries = (IEnumerable<Category>)ViewData["categories"]; %>
    <% categoriesMacros(categpries); %>
Achmedzhanov Nail