Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Elegant foreach - else construct in Razor

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.

like image 637
Thomas Avatar asked Oct 19 '11 10:10

Thomas


3 Answers

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>
        )
)
like image 152
Thomas Avatar answered Oct 19 '22 02:10

Thomas


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.

like image 40
Jamiec Avatar answered Oct 19 '22 03:10

Jamiec


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

like image 27
Martin Booth Avatar answered Oct 19 '22 03:10

Martin Booth