In my last post I explored the possibility of block components in ASP.NET MVC. Actually, these posts should be titled "Block components in ASP.NET MVC / WebFormViewEngine". But WebFormViewEngine is the default view engine (a strong default) so whenever I say ASP.NET MVC it's WebFormViewEngine. Of course we could replace it and implement block components any way we want, but today we'll stick to defaults as much as we can, trying to keep it simple.
So far, I have only considered a single block. Let's now see what happens when trying to introduce several named blocks. We want to map the name of the block (a string) to the block itself (an Action). So... IDictionary<string, Action>:
box.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IDictionary<string, Action>>" %> <div class="box"> <div class="top"></div> <div class="title"><% Model["title"](); %></div> <div class="content"> <% Model["content"](); %> </div> <div class="bottom"></div> </div>
OK, that wasn't so bad... now let's see how we define the sections.
First attempt
It's a dictionary, so we could just give it a dictionary:
<% Html.RenderPartial("box", new Dictionary<string, Action> { {"title", () => {%> <h1>the title</h1> <%}}, {"content", () => { %> <div>some content</div> <%}}}); %>
This doesn't require any extra code but it's very ugly... we could try defining a small fluent interface for this:
Second attempt
<% Html.Partial("box") .WithSection("title", () => {%> <h1>the title</h1> <% }) .WithSection("content", () => {%> <div>some content</div> <% }) .Render(); %>Implementation:
public static class HtmlHelperExtensions { public static PartialWithSectionsRendering Partial(this HtmlHelper html, string viewName) { return new PartialWithSectionsRendering(html, viewName); } public class PartialWithSectionsRendering { private readonly HtmlHelper html; private readonly string viewName; private readonly IDictionary<string, Action> sections = new Dictionary<string, Action>(); public PartialWithSectionsRendering(HtmlHelper html, string viewName) { this.html = html; this.viewName = viewName; } public PartialWithSectionsRendering WithSection(string section, Action render) { sections[section] = render; return this; } public void Render() { html.RenderPartial(viewName, sections); } } }
Still too awkward... plus we could easily forget to call the final Render().
Third attempt
<% Html.RenderPartialWithSections("box", section => { section("title", () => { %> <h1>the title</h1> <% }); section("content", () => { %> <div>some content</div> <% }); }); %>
This certainly looks better... the intention is expressed quite clearly, and the implementation is very simple:
public static class HtmlHelperExtensions { public static void RenderPartialWithSections(this HtmlHelper html, string viewName, Action<Action<string, Action>> sections) { var dict = new Dictionary<string, Action>(); sections.Invoke((section, render) => dict[section] = render); html.RenderPartial(viewName, dict); } }
Let's try this one more time:
Fourth attempt
<% Html.RenderPartialWithSections2("box", title => {%> <h1>the title</h1> <% }, content => {%> <div>some content</div> <% }); %>
This is shorter that the last attempt, but what's really that title
variable? What would happen if we use it in the Action block? Let's see the implementation first:
public static class HtmlHelperExtensions { public static void RenderPartialWithSections2(this HtmlHelper html, string viewName, params Action<object>[] sections) { var dict = new Dictionary<string, Action>(); foreach (var a in sections) { var name = a.Method.GetParameters()[0].Name; var render = a; // copied to avoid access to modified closure dict[name] = () => render.Invoke(null); } html.RenderPartial(viewName, dict); } }
So if you use the title
variable in your title Action block, Bad Things Will Happen. Namely, a NullReferenceException since it's just a dummy null object!
Conclusion
I'll stop there for now, but I have to confess that I don't really like any of these solutions :-) They work, but this problem can be expressed more elegantly in Brail. DSLs win here... unless I'm missing something!