A Better Razor Foreach Loop

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

What others have said

Requesting Gravatar... Andrew Robinson Apr 14, 2011 7:54 AM
# re: A Better Razor Foreach Loop
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; }
}
Requesting Gravatar... Alexander Zubkov Apr 14, 2011 8:11 AM
# re: A Better Razor Foreach Loop
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
Requesting Gravatar... Magnus Mårtensson Apr 14, 2011 3:01 PM
# re: A Better Razor Foreach Loop
Personal NuGet stream? Please share on how to get one. Me wants one for my own... my precious! ;~)

Cheers,

M.
Requesting Gravatar... Steve Fenton Apr 14, 2011 4:08 PM
# re: A Better Razor Foreach Loop
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!
Requesting Gravatar... лз Apr 14, 2011 5:36 PM
# re: A Better Razor Foreach Loop
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);
}
Requesting Gravatar... Suhas Apr 14, 2011 6:52 PM
# re: A Better Razor Foreach Loop
How should I write the web forms view engine equivalent of this?
Requesting Gravatar... Anonymous Apr 14, 2011 8:14 PM
# re: A Better Razor Foreach Loop
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.
Requesting Gravatar... ruhul Apr 14, 2011 8:47 PM
# re: A Better Razor Foreach Loop
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.
Requesting Gravatar... Sergey Apr 14, 2011 9:13 PM
# re: A Better Razor Foreach Loop
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);
}
}
Requesting Gravatar... Erik Zettersten Apr 14, 2011 10:59 PM
# re: A Better Razor Foreach Loop
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!
Requesting Gravatar... Alberto Chvaicer Apr 15, 2011 12:00 AM
# re: A Better Razor Foreach Loop
Set items.Count() into a variable.
It's a tiny thing, but why not reduce the number of calls of something that never changes?
Requesting Gravatar... Ryan Apr 15, 2011 5:42 AM
# re: A Better Razor Foreach Loop
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
Requesting Gravatar... Jeff Owen Apr 15, 2011 6:31 AM
# re: A Better Razor Foreach Loop
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.

Requesting Gravatar... Roberto Hernandez Apr 15, 2011 6:37 AM
# re: A Better Razor Foreach Loop
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.-
Requesting Gravatar... Lukáš Novotný Apr 15, 2011 8:52 AM
# re: A Better Razor Foreach Loop
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.
Requesting Gravatar... Ian Apr 15, 2011 10:38 AM
# re: A Better Razor Foreach Loop
foreach (var item in Model.Select((value,i) => new {i, value}))

This gets you the item (item.value) and its index (item.i).
Requesting Gravatar... naraga Apr 15, 2011 11:36 PM
# re: A Better Razor Foreach Loop
This is exactly what i needed one day ago to support editable list in grid.

i have ended up with with very similar solution...
Requesting Gravatar... haacked Apr 16, 2011 2:17 AM
# re: A Better Razor Foreach Loop
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.
Requesting Gravatar... haacked Apr 16, 2011 2:33 AM
# re: A Better Razor Foreach Loop
I updated the package today to be more efficient.
Requesting Gravatar... Thanigainathan Apr 17, 2011 12:48 AM
# re: A Better Razor Foreach Loop
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.
Requesting Gravatar... Paul Apr 17, 2011 2:16 AM
# re: A Better Razor Foreach Loop
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.
Requesting Gravatar... bibianaaklo Jun 02, 2011 1:18 AM
# Mr
I thought he was right on and not backing up but doing world news what he does...Correct the media and rightwing lies!

What do you have to say?

(will show your gravatar)
Please add 1 and 5 and type the answer here: