We all know DRY is a good principle to follow but it's now always that easy. I will show you technique using templated delegates that keeps your code both flexible and DRY.
The problem
Take a look at this fieldset
<fieldset class="form">
<legend>Elevinformation</legend>
<h2>Elevinformation</h2>
<div class="editor-prop">
<div class="editor-label">
Förnamn
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Dto.FirstName)
@Html.ValidationMessageFor(model => model.Dto.FirstName)
</div>
</div>
<div class="editor-prop">
<div class="editor-label">
Efternamn
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Dto.LastName)
@Html.ValidationMessageFor(model => model.Dto.LastName)
</div>
</div>
<div class="editor-prop">
<div class="editor-label">
Startår
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Dto.StartYear)
@Html.ValidationMessageFor(model => model.Dto.StartYear)
</div>
</div>
<div class="editor-prop">
<div class="editor-label">
Slutår
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Dto.EndYear)
@Html.ValidationMessageFor(model => model.Dto.EndYear)
</div>
</div>
<div class="editor-prop">
<div class="editor-label">
Email
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Dto.Email)
@Html.ValidationMessageFor(model => model.Dto.Email)
</div>
</div>
<p>
<input type="submit" value="Spara" />
</p>
</fieldset>
This produces the following view:
As you can see we repeat parts like this many times.
<div class="editor-prop">
<div class="editor-label">
Email
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Dto.Email)
@Html.ValidationMessageFor(model => model.Dto.Email)
</div>
</div>
There are two things that bugs me a lot with this.
For a single field I have 6 rows of cermony and 3 rows of content. Sure, the rows with content are more complex but it still sucks because I have to type that. I can template or scaffold it but that is just a workaround IMO.
If I decide to change the html structure of my forms. Well.. in the current situation we better not because we have to do the same change across every field in every form of the whole application. That's like cooking spaghetti straw by straw. Don't do it if you want to stay sane!
The problem is that the label is not always a simple string and that I need to be able to specify the editor from time to time. I need control but I want to encapsulate the layout.
What we want/result
This is not the first time I face similar problems so I already knew pretty well how I wanted it to work. I want to inject the label and the field into a method that renders the whole section. I want it to be easy to use in most cases but able to handle more complex situations too.
Simple case example(Will expand to exactly the same html as above):
@Html.EditorField("Email", model => model.Dto.Email)
More advanced scenario:
@Html.EditorField(@<span class="important">Email</span>, @<text>
@Html.TextBoxFor(model => model.Dto.Email, new { @class = "email" })
<span>Place some extra info here for this field</span>
@Html.ValidationMessageFor(model => model.Dto.Email)
</text>)
I'm not sure how that renders so here is a screenshot to show the real power:
Awesome! Real html, no string escaping and stuff. And if you think about you will probably see how flexible that is.
That should give you an idea what the result will be and hopefully why I want this. Here is the whole form using the refactored code.
<fieldset class="form">
<legend>Elevinformation</legend>
<h2>Elevinformation</h2>
@Html.EditorField("Förnamn", model => model.Dto.FirstName)
@Html.EditorField("Efternamn", model => model.Dto.LastName)
@Html.EditorField("Startår", model => model.Dto.StartYear)
@Html.EditorField("Slutår", model => model.Dto.EndYear)
@Html.EditorField("Email", model => model.Dto.Email)
<p>
<input type="submit" value="Spara" />
</p>
</fieldset>
The code
It turns out that there is a little nice feature in razor that let you define templated delegates. Phil Haack wrote about it.
I'm just going to give you the solution here and then I will explain a few things. To start with I left the commented method in there with hopes that someone can tell me how to not get that one and the last method to "collide" when I try to call them.
public static class TemplateDelegates
{
///// <summary>
///// Uses and EditorFor the selected property
///// </summary>
//public static HelperResult EditorField<T, TProp>(this HtmlHelper<T> source, Func<T, HelperResult> label, Expression<Func<T, TProp>> property)
//{
// return source.RenderEditorField(w => label(source.ViewData.Model).WriteTo(w), w =>
// {
// w.WriteLine(source.EditorFor(property));
// w.WriteLine(source.ValidationMessageFor(property));
// });
//}
/// <summary>
/// Uses and EditorFor the selected property
/// </summary>
public static HelperResult EditorField<T, TProp>(this HtmlHelper<T> source, string label, Expression<Func<T, TProp>> property)
{
return source.RenderEditorField(w => w.WriteLine(label), w => {
w.WriteLine(source.EditorFor(property));
w.WriteLine(source.ValidationMessageFor(property));
});
}
/// <summary>
/// Gives you full controll of the html
/// </summary>
public static HelperResult EditorField<T>(this HtmlHelper<T> source, Func<T, HelperResult> label, Func<T, HelperResult> value)
{
return source.RenderEditorField(w => label(source.ViewData.Model).WriteTo(w), w => value(source.ViewData.Model).WriteTo(w));
}
private static HelperResult RenderEditorField<T>(this HtmlHelper<T> source, Action<TextWriter> writeLabel, Action<TextWriter> writeValue)
{
return new HelperResult(writer =>
{
writer.WriteLine("<div class='editor-prop'>");
writer.WriteLine("\t<div class='editor-label'>");
writeLabel(writer);
writer.WriteLine("\t</div>");
writer.WriteLine("\t<div class='editor-field'>");
writeValue(writer);
writer.WriteLine("\t</div>");
writer.WriteLine("</div>");
});
}
}
By taking an Expression<Func<T, TProp>>
property we can simply forward that to the EditorFor and ValidationMessageFor. This is the easiest method to use(The first one I showed).
When we need more controll we as for a HelperResult instead which is where templated delegates come in. Read the post by Haacked if you want to know more about that.
Conclusion
With only a few small methods I can now avoid repeating my layout code all day and focus on actually getting things done. And If I ever decide to change the html structure I'm in a much better position to do so because that is encapsulated into that method. If anyone can solve that overload problem for me I would be even happier:)