Meet my new HtmlHelper extensions friends, ForEach and If

Posted by Kenny Eliasson | Posted in , , , | Posted on 13:15

How many times have you written code like this in your MVC views?

<%
 int currentIndex = 0;
 foreach(var item in Model.Items) {
%>
    <% if(currentIndex == 0) {%>
       <div class="item first"><%= item.Name %></div>
    <% } else if(currentIndex == Items.Count()-1) { %>
       <div class="item last"><%= currentIndex + 1 %> <%= item.Name %></div>
    <% } else { %>
       <div class="item"><%= item.Name %></div>
    <% } %>
<%
 currentIndex++;
 }
%>

This is pure ugliness :(

What I missed was something like django for variables. They include both a Last and First variable on the loop.

After reading Phil Haacked's blog about "A code based repeater for .NET MVC" I decided to shamelessy take some parts of his code and write my own extension methods.

The first extension method I wrote was the Html.ForEach which gets me a index counter.

public static void ForEach<T>(this HtmlHelper html, IEnumerable<T> items, Action<T, int> render)
{
    if (items == null)
        return;

    int i = 0;
    items.ForEach(item => render(item, i++));
}

And to use it

<% Html.ForEach(Model.Items, (item, index) => { %>
    <div>#<%= index + 1 %> <%= item.Name %>
<% }); %>

Pretty slick and I dont need to have the counter variable in the view code. It uses lambda in way I didn't thought was possible before I read Haacked blog post.

I then decided to see if I could manage to check if I was on the first or last item in the loop.

I came up with this

public static void ForEachExtra<T>(this HtmlHelper html, IEnumerable<T> items, Action<T, ForLoop<T>> render)
{
    if (items == null)
       return;

    var loop = new ForLoop<T>(items);

    int i = 0;
    items.ForEach(item => { render(item, loop.Update(i)); i++; });
}

public class ForLoop<T>
{
    private readonly int _itemCount;

    public ForLoop(IEnumerable<T> items)
    {
        _itemCount = items.Count();
    }

    public int Counter { get; set; }
    public int Counter0 { get; set; }
    public bool First { get; set; }
    public bool Last { get; set; }

    public ForLoop<T> Update(int i)
    {
        Counter = i + 1;
        Counter0 = i;
        First = i == 0;
        Last = i == _itemCount-1;
        return this;
    }
 }


And to use this you will write

<% Html.ForEachExtra(Model.Templates, (template, loop) => {%>
   <% if(loop.First) { %> First! <% }%>">
   <div>#<%= loop.Counter %> - <%= template.Name %></div>
   <% if(loop.Last) { %> Last! <% }%>
<% }); %>


Starting to look impressive here! :) But I still think I can get it better, so onto my other new extension method, If()!

What I want is to have code that looks something like this

<% Html.ForEachExtra(Model.Items, (item, loop) => { %>
   <div class="item <%= Html.If(loop.First).Write("first").ElseIf(loop.Last).Write("last") %>
       #<%=loop.counter%> <%= item.Name %>
   </div>
<% }); %>

That would be awesome so off I went to write it.

public interface IConditionWrite
{
    IElseIfConditionBuilder Write(string output);
}

public interface IElseIfConditionBuilder
{
    IConditionWrite ElseIf(bool condition);
}

public class ConditionBuilder : IConditionWrite, IElseIfConditionBuilder
{
    private readonly IList<FluentHtmlCondition> _conditions;
    private FluentHtmlCondition _lastAddedCondition;

    public ConditionBuilder()
    {
        _conditions = new List<FluentHtmlCondition>();
    }

    public IConditionWrite AddCondition(FluentHtmlCondition condition)
    {
       _lastAddedCondition = condition;
       _conditions.Add(condition);
       return this;
    }
        
    public override string ToString()
    {
        foreach (var condition in _conditions) {
            if (condition.Fulfilled)
                return condition.Output;
        }

        return "";
    }

    public IConditionWrite ElseIf(bool condition)
    {
        AddCondition(new FluentHtmlCondition(condition));
        return this;
    }

    public IElseIfConditionBuilder Write(string output)
    {
        _lastAddedCondition.Output = output;
        return this;
    }
}

public class FluentHtmlCondition
{
    public readonly bool Fulfilled;
    public string Output;

    public FluentHtmlCondition(bool fulfilled)
    {
        Fulfilled = fulfilled;
    }

    public void Write(string output)
    {
        Output = output;
    }
 }

And in my Html-helper extension class i add

public static IConditionWrite If(this HtmlHelper html, bool condition)
{
    return new ConditionBuilder().AddCondition(new FluentHtmlCondition(condition));
}

And there you have it all, hope someone can have some use with it.

Comments (2)

  1. Yes me likey, but some things that can be debated :D

    First:

    The null check is this something for the view to do or the controller?

    Second:

    The condition logic, should you build something that is supported in the language? ( condition ? something : somethingelse )

    See nothing really wrong with your implementation just want to know what you think :)

  2. Good post! keep it up...

Skicka en kommentar