Elegant foreach - else construct in Razor
Asked Answered
P

4

8

A lot of templating engines have a special kind of syntax that is a combination of foreach and else. Basically the else clause is executed when the foreach loop doesn't have any iterations. This can be useful if you want to display some kind of no items in the list fallback.

In Twig for example, the for loop can look like this

{% for user in users %}
    <li>{{ user.username|e }}</li>
{% else %}
    <li><em>no user found</em></li>
{% endfor %}

Using the Razor View Engine, the template would like like this, involving an additional check on the number of items in the collection:

@foreach (var user in users) {
    <li>@user.UserName</li>
}
@if (!users.Any()) {
    <li><em>no user found</em></li>
}

So my questions is: can we achieve a similar elegance some way or another using the Razor View Engine.

Polonaise answered 19/10, 2011 at 10:48 Comment(0)
P
9

Consolidating the answers of Jamiec and Martin Booth. I created the following extension method. It takes an IEnumerable as first argument, and then two delegates for rendering the text. In the Razor Views we can pass in Templated Delegates two these parameters. In short this means that you can give in templates. So here is the extension method and how you can call it:

    public static HelperResult Each<TItem>(this IEnumerable<TItem> items, 
        Func<TItem, HelperResult> eachTemplate, 
        Func<dynamic, HelperResult> other)
    {
        return new HelperResult(writer =>
        {
            foreach (var item in items)
            {
                var result = eachTemplate(item);
                result.WriteTo(writer);
            }

            if (!items.Any())
            {
                var otherResult = other(new ExpandoObject());
                // var otherResult = other(default(TItem));
                otherResult.WriteTo(writer);
            }
        });
    }

And in the Razor views:

@Model.Users.Each(
    @<li>@item.Name</li>,
    @<li>
        <b>No Items</b>
     </li>
)

All in all, pretty clean.

UPDATE implementing the suggestions made in the comments. This extension method takes one argument to loop over the items in the collection and returns a custom HelperResult. On that helperresult, one can call the Else method to pass in a template delegate in case no items are found.

public static class HtmlHelpers
{
    public static ElseHelperResult<TItem> Each<TItem>(this IEnumerable<TItem> items, 
        Func<TItem, HelperResult> eachTemplate)
    {
        return ElseHelperResult<TItem>.Create(items, eachTemplate);
    }
}

public class ElseHelperResult<T> : HelperResult
{
    private class Data
    {
        public IEnumerable<T> Items { get; set; }
        public Func<T, HelperResult> EachTemplate { get; set; }
        public Func<dynamic, HelperResult> ElseTemplate { get; set; }

        public Data(IEnumerable<T> items, Func<T, HelperResult> eachTemplate)
        {
            Items = items;
            EachTemplate = eachTemplate;
        }

        public void Render(TextWriter writer)
        {
            foreach (var item in Items)
            {
                var result = EachTemplate(item);
                result.WriteTo(writer);
            }

            if (!Items.Any() && ElseTemplate != null)
            {
                var otherResult = ElseTemplate(new ExpandoObject());
                // var otherResult = other(default(TItem));
                otherResult.WriteTo(writer);
            }
        }
    }

    public ElseHelperResult<T> Else(Func<dynamic, HelperResult> elseTemplate)
    {
        RenderingData.ElseTemplate = elseTemplate;
        return this;
    }

    public static ElseHelperResult<T> Create(IEnumerable<T> items, Func<T, HelperResult> eachTemplate)
    {
        var data = new Data(items, eachTemplate);
        return new ElseHelperResult<T>(data);
    }

    private ElseHelperResult(Data data)
        : base(data.Render)
    {
        RenderingData = data;
    }

    private Data RenderingData { get; set; }
}

This can then be called as follows:

@(Model.Users
   .Each(@<li>@item.Name</li>)
   .Else(
        @<li>
            <b>No Users</b>
         </li>
        )
)
Polonaise answered 19/10, 2011 at 11:42 Comment(6)
The comma is near-invisible. :PPlanetoid
I agree, but there's no real way we can circumvent that, can we? :)Polonaise
If you use a named parameter for the else clause it is probably even more readable and you can make it optionalGrandniece
I think you could derive either from HelperResult or from IHtmlString, and create an .Else() method in similar way. So you'd write: @Models.Users.Each(@<li>@item.name</li>).Else(@<li><b>No items</b></li>)Planetoid
Better still, return a custom class from the for each extention method which implements ihtmlstring but this means you can make the else method only apply to this custom class (ie not any ienumerable)Grandniece
+1 - Great answer. I knew there was a way to build on what i'd started, I just didnt know about Templated Delegates for Razor.Harmony
H
2

The only way I could think to achieve something like this is with a couple of extensions to IEnumerable<T>:

public static class IEnumerableExtensions
{
    public static IEnumerable<T> ForEach<T>(this IEnumerable<T> enumerable, Action<T> action)
    {
       foreach(T item in enumerable)
           action(item);

        return enumerable;
    }

    public static IEnumerable<T> WhenEmpty<T>(this IEnumerable<T> enumerable, Action action)
    {
       if(!enumerable.Any())
           action();
        return enumerable;
    }
}

This enables you to chain 2 calls onto each other as demonstarted by this live example: http://rextester.com/runcode?code=AEBQ75190 which uses the following code:

var listWithItems = new int[] {1,2,3};
var emptyList = new int[]{};

listWithItems.ForEach(i => Console.WriteLine(i))
    .WhenEmpty( () => Console.WriteLine("This should never display"));

emptyList.ForEach(i => Console.WriteLine(i))
    .WhenEmpty( () => Console.WriteLine("This list was empty"));

Quite how this would fit in with a Razor template im still unsure of.... but maybe this gives you something to go on.

Harmony answered 19/10, 2011 at 11:10 Comment(0)
G
1

Nothing built in afaik, but you could probably extend this to suit your needs:

http://haacked.com/archive/2011/04/14/a-better-razor-foreach-loop.aspx

I might be able to help later when I'm not using my phone if you still don't have an answer

Grandniece answered 19/10, 2011 at 11:15 Comment(1)
I think if you combine this with jamiec's answer you will have a solution!Grandniece
U
0

Maybe this wasn't possible when the question was posed, but I've just achieved this like this:

@if (Model.EmailAddress.Count() > 0)
{
    foreach (var emailAddress in Model.EmailAddress)
    {
        <div>@emailAddress.EmailAddress</div>
    }
} else { <span>No email addresses to display</span>  }
Usable answered 18/1, 2018 at 15:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.