I’ve been very quiet recently. (I’m trying to not be so loud, Scott. ) You see, I’ve been writing a lot of ASP.Net code for a site I’m working on. And, to be honest, I’ve been having a lot of trouble. The source code for .Net has been very helpful, and I’ve learnt a lot about what’s going on under the covers of ASP.Net because of it.
(Note: I won’t comment here on the quality of the code I’ve found – I’ll leave that up to you to judge. But in any case, I’ve been trying to build on top of it.)
One thing I’ve found to be important is the reliance on Web Controls. (It’s got something to do with javascript libraries, but that’s another story.) Getting away from the "standard" way to do ASP.Net isn’t easy though. Even the ninjas on the ASP.Net MVC team seem to be having trouble. However, with the magic of lambdas and extension methods in C#, I think I might have just about managed to get something usable. I thought I’d publish my work here, and see what comments I got.
I think it’s best to start with what my ASP.Net code looks like once I’ve got everything working. (Notice I still have some Web Controls in there, but that’s because I’ve not worked out how to do sorting of data without web controls yet.)
The inspiration for this was taken from the improvements made to NVelocity by the gurus on the Castle Project. I thought it looked great, and I’d like something similar, but I didn’t really want to learn a whole new scripting language and integrate it into my working environment just for rendering a bit of HTML. So I built some C# classes to do a similar thing for me instead. It’s not as nice as NVelocity, but it’s okay for now.
Warning: The following code may contain statements of a disturbing nature to more sensitive readers. We cannot be held responsible for any confusion, delusion or mental illness caused by this code.
It starts by taking a collection of Task objects, and calling the extension method "ForEach" on them:
<% Tasks.ForEach(sections => { sections.NoData = tasks => { %> <p> Hey, you've got nothing to do.</p> <p> <% }; sections.BeforeAll = tasks => { %> <table class="task-list"> <tr class="task-list-header"> <th> <asp:LinkButton runat="server" CommandName="Sort" CommandArgument="StartDate" Text="Started" /> </th> <th> <asp:LinkButton runat="server" CommandName="Sort" CommandArgument="DueOn" Text="Due" /> </th> <th> <asp:LinkButton runat="server" CommandName="Sort" CommandArgument="Priority" Text="Priority" /> </th> <th> <asp:LinkButton CssClass="task-description" CommandName="Sort" CommandArgument="Title" Text="Description" runat="server" /> </th> <th> </th> </tr> <% }; sections.Before = task => { %> <tr class="<%= this.tableCssClasses.Next() %>"> <% }; sections.Each = task => { %> <td> <div class='calendar calendar-icon-<%= task.StartMonth %>'> <div class="calendar-day"> <%= task.StartDayOfMonth %></div> </div> </td> <td> <div class='calendar calendar-icon-<%= task.DueMonth %>'> <div class="calendar-day"> <%= task.DueDayOfMonth %></div> </div> </td> <td> <%= task.Priority %> </td> <td class="task-title"> <a href='<%= Href.For("~/Tasks/{0}/Show.aspx", task.ID) %>'><%= task.Title %></a> </td> <td> <asp:Button ID="Button1" runat="server" CssClass="button" CommandName="Delete" Text="Mark Done" /> </td> <% }; sections.After = task => { %> </tr> <% }; sections.AfterAll = task => { %> </table> <% }; }); %>
It might take a while to grasp what’s going on here. The code actually starts using an Extension method to IEnumerable that looks like this:
public static void ForEach<T>(this IEnumerable<T> enumerable, ForeachSectionSetter<T> sectionSetter) { if (enumerable != null) { if (sectionSetter != null) { ForeachSections<T> sections = new ForeachSections<T>(); sectionSetter(sections); if (enumerable.Count() == 0) { if (sections.NoData != null) sections.NoData(enumerable); return; } if (sections.BeforeAll != null) sections.BeforeAll(enumerable); int itemIndex = 0; T previousItem = default(T); foreach (T item in enumerable) { if (sections.Before != null) sections.Before(item); if (itemIndex % 2 == 1 && sections.Odd != null) sections.Odd(item); if (itemIndex % 2 == 0 && sections.Even != null) sections.Even(item); if (itemIndex > 0 && sections.Between != null) sections.Between(previousItem, item); if (sections.Each != null) sections.Each(item); if (sections.After != null) sections.After(item); itemIndex++; previousItem = item; } if (sections.AfterAll != null) sections.AfterAll(enumerable); } } }
The delegate ForEachSectionSetter is used by the calling method with a lambda expression. As a parameter it receives an ForeachSections object, which looks like this:
public class ForeachSections<T> { public Action<T> Each { get; set; } public Action<IEnumerable<T>> BeforeAll { get; set; } public Action<T> Before { get; set; } public Action<T,T> Between { get; set; } public Action<T> Odd { get; set; } public Action<T> Even { get; set; } public Action<T> After { get; set; } public Action<IEnumerable<T>> AfterAll { get; set; } public Action<IEnumerable<T>> NoData { get; set; } }
The calling method gets the chance to set the properties of this class before it is returned to the constructor of the ForEach method for processing. And because each property is already preset to a default value (Null in this case), the constructor can use the ForeachSections object just like a set of default or optional parameters. The caller can simply set values to the properties it needs, and ignore the rest.
If I had tried this another way, using overloadable constructors, it would have led to multiple constructors with indistinguishable signatures. If I’d have used property initializers, I wouldn’t have been able to run the whole routine without requiring a second call to the object, which actually wasn’t possible.
Basically, I couldn’t think of another way to do it.
The properties of the ForeachSections object are all delegates too. That means that we can use them with lambdas, which gives us lambdas inside of a lambda. (Hmm, very confusing!)
So what do you think? Could you use something like this? Can you make it simpler? Leave me a comment if you can.