Meet my new HtmlHelper extensions friends, ForEach and If
Posted by Kenny Eliasson | Posted in C# , Extension Methods , Html , MVC | 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.