A Better Razor Foreach Loop

razor, code, asp.net mvc 0 comments suggest edit

Yesterday, during my ASP.NET MVC 3 talk at Mix 11, I wrote a useful helper method demonstrating an advanced feature of Razor, Razor Templated Delegates.

There are many situations where I want to quickly iterate through a bunch of items in a view, and I prefer using the foreach statement. But sometimes, I need to also know the current index. So I wrote an extension method to IEnumerable<T> that accepts Razor syntax as an argument and calls that template for each item in the enumeration.

public static class HaackHelpers {
  public static HelperResult Each<TItem>(
      this IEnumerable<TItem> items, 
      Func<IndexedItem<TItem>, 
      HelperResult> template) {
    return new HelperResult(writer => {
      int index = 0;

      foreach (var item in items) {
        var result = template(new IndexedItem<TItem>(index++, item));
        result.WriteTo(writer);
      }
    });
  }
}

This method calls the template for each item in the enumeration, but instead of passing in the item itself, we wrap it in a new class, IndexedItem<T>.

public class IndexedItem<TModel> {
  public IndexedItem(int index, TModel item) {
    Index = index;
    Item = item;
  }

  public int Index { get; private set; }
  public TModel Item { get; private set; }
}

And here’s an example of its usage within a view. Notice that we pass in Razor markup as an argument to the method which gets called for each item. We have access to the direct item and the current index.


@model IEnumerable<Question>

<ol>
@Model.Each(@<li>Item @item.Index of @(Model.Count() - 1): @item.Item.Title</li>)
</ol>

If you want to try it out, I put the code in a package in my personal NuGet feed for my code samples. Just connect NuGet to http://nuget.haacked.com/nuget/ and Install-Package RazorForEach. The package installs this code as source files in App_Code.

UPDATE: I updated the code and package to be more efficient (4/16/2011).

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

24 responses

  1. Avatar for Andrew Robinson
    Andrew Robinson April 14th, 2011

    Maytbe a better approach? Sorry don't have MVC installed on this machine but I think you get the idea.
    class Program
    {
    static void Main(string[] args)
    {
    var fred = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, };
    foreach (var item in fred.ItemIndex())
    Console.WriteLine(item.Index.ToString() + ":" + item.Item.ToString());
    }
    }
    static class Ext
    {
    public static IEnumerable<ItemIndex<T>> ItemIndex<T>(this IEnumerable<T> source)
    {
    return source.Select((item, index) => new ItemIndex<T> { Item = item, Index = index, });
    }
    }
    public class ItemIndex<T>
    {
    public T Item { get; internal set; }
    public int Index { get; internal set; }
    }

  2. Avatar for Alexander Zubkov
    Alexander Zubkov April 14th, 2011

    There is a best enumeration :)
    foreach (SmartEnumerable<string>.Entry entry in
    new SmartEnumerable<string>(list))
    {
    Console.WriteLine ("{0,-7} {1} ({2}) {3}",
    entry.IsLast ? "Last ->" : "",
    entry.Value,
    entry.Index,
    entry.IsFirst ? "<- First" : "");
    }

    More details here http://www.yoda.arachsys.com/csharp/miscutil/usage/smartenumerable.html

  3. Avatar for Magnus M&#229;rtensson
    Magnus M&#229;rtensson April 14th, 2011

    Personal NuGet stream? Please share on how to get one. Me wants one for my own... my precious! ;~)
    Cheers,
    M.

  4. Avatar for Steve Fenton
    Steve Fenton April 14th, 2011

    There are some examples of setting up your own NuGet repository on the NuGet page on Codeplex...
    http://nuget.codeplex.com/
    Not just good for sharing with other people... Also a better alternative to SVN externals!

  5. Avatar for лз
    лз April 14th, 2011

    items.ElementAt(i) is not the best choice. Depending on implementation it might reiterate the whole thing from the start over and over again.
    I think it's better to use foreach and maintain the current position. Like
    int i = 0;
    foreach (var item in items){
    var result = template(new IndexedItem<TItem>(i++, item));
    result.WriteTo(writer);
    }

  6. Avatar for Suhas
    Suhas April 14th, 2011

    How should I write the web forms view engine equivalent of this?

  7. Avatar for Anonymous
    Anonymous April 14th, 2011

    Isnt this going to be an exponential iteration if the enumerable. Count() will need to enumerate all items, then ElementAt will probably also need to so the same thing for every single element.

  8. Avatar for ruhul
    ruhul April 14th, 2011

    I was looking for this solution and posted a question in stackoverflow.. Then I read an article on your site about delegate usage in razor. That article gave me enough hints to find a solution and posted the answer in stackoverflow.

  9. Avatar for Sergey
    Sergey April 14th, 2011

    Why should I use a proxy class? Isnt it better to just put an indexer to Func? Like this:
    public static void ForEach<T>(this IEnumerable<T> items, Action<int, T> exp)
    {
    var index = 0;
    foreach (var item in items)
    {
    exp(index++, item);
    }
    }

  10. Avatar for Erik Zettersten
    Erik Zettersten April 14th, 2011

    I tried this a while back - in the earlier stages of WebMatrix and Razor.
    I always felt like I was doing it wrong... Long story short... This looks/works great!

  11. Avatar for Alberto Chvaicer
    Alberto Chvaicer April 14th, 2011

    Set items.Count() into a variable.
    It's a tiny thing, but why not reduce the number of calls of something that never changes?

  12. Avatar for Ryan
    Ryan April 15th, 2011

    Kinda messy. I find it odd that Razor doesn't have an index overload of Each like LINQ does for Select. I wrote a full response here with a comparison to Spark: http://pastie.org/1798675

  13. Avatar for Jeff Owen
    Jeff Owen April 15th, 2011

    Razor doesn't seem to allow nested inline markupblock.
    "(@

    Contenxt</p) cannot be nested. Only one level of inline markup is allowed." as the error message says.
    So it's not possible to use it on a list of items that themselves contain collections.

  14. Avatar for Roberto Hernandez
    Roberto Hernandez April 15th, 2011

    I also got the Razor extension bug.
    See http://blog.overridethis.com/blog/post/2011/04/15/Learning-Razore28093Writing-a-Once-Extension.aspx
    I love the code in your post, but I had been using this for looping.
    http://nuget.org/List/Packages/WebMatrixLoopHelper
    Regards,
    Roberto.-

  15. Avatar for Luk&#225;š Novotn&#253;
    Luk&#225;š Novotn&#253; April 15th, 2011

    I came up with this. It is not as simple, but it enumerates any collection only once and doesn't wrap items to another class.

  16. Avatar for Ian
    Ian April 15th, 2011

    foreach (var item in Model.Select((value,i) => new {i, value}))
    This gets you the item (item.value) and its index (item.i).

  17. Avatar for naraga
    naraga April 15th, 2011

    This is exactly what i needed one day ago to support editable list in grid.
    i have ended up with with very similar solution...

  18. Avatar for haacked
    haacked April 15th, 2011

    Thanks for the suggestions. I'll look to improve the code when I get a moment.
    To host your own feed, check out my blog post on Hosting a Simple NuGet Server.

  19. Avatar for haacked
    haacked April 15th, 2011

    I updated the package today to be more efficient.

  20. Avatar for Thanigainathan
    Thanigainathan April 16th, 2011

    Hi,
    This is a very powerful feature of Razor. Thanks for sharing this knowledge. I was creating a separate variable each time to get the index of the looped item to show zebra coloring. Now this will be useful for me.

  21. Avatar for Paul
    Paul April 16th, 2011

    Does this solution preform better then just doing:
    <ul>
    @foreach(var item in Model) {
    <li>@Model.IndexOf(item) index of @(Model.Count() - 1) : @item.title</li>
    }
    </ul>
    Thanks.

  22. Avatar for bibianaaklo
    bibianaaklo June 1st, 2011

    I thought he was right on and not backing up but doing world news what he does...Correct the media and rightwing lies!

  23. Avatar for Tim Osborn
    Tim Osborn June 22nd, 2014

    phew.. thanks.. dropin solution for a .net'less frontend guy

  24. Avatar for John angular
    John angular August 29th, 2016

    Very useful foreach loop