ASP.NET MVC Preview 5: strongly typed HtmlHelpers
ScottGu’s recent post on Form Posting Scenarios in ASP.NET MVC Preview 5 turned on a lot of lights for me regarding recent changes in How a Method Becomes an Action and the new ActionNameAttribute.
Magic data binding…
I was particularly impressed with the round-trip scenario that Scott describes and the clean code in his Edix.aspx view:
1
2
3
4
5
6
7
<tr>
<td>Product Name:</td>
<td>
<%=Html.TextBox("ProductName")%>
<%=Html.ValidationMessage("ProductName", "*")%>
</td>
</tr>
That seems like a very simple way to add/enable server-side validation and I look forward to using it.
###…with strongly typed helpers
In my ongoing quest to eliminate “magic strings” from my code I would much rather write this
1
2
3
4
5
6
7
<tr>
<td>Product Name:</td>
<td>
<%=Html.TextBox<Product>(p => p.ProductName)%>
<%=Html.ValidationMessage<Product>(p => p.ProductName, "*")%>
</td>
</tr>
and lean on the speed and compile-time checks of intellisense and strongly typed functions.
Red - write a failing test
To achieve this syntax, I wrote the following test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Fact]
public void TextBoxOfT_should_take_a_property_access_and_name_the_textbox_with_the_property_name()
{
// Arrange
HtmlHelper Html =
MoqHtmlHelper.CreateMoqHtmlHelper<ProductsController>("~/Products/Edit/10", (NorthwindDataContext)null);
// act
string result = Html.TextBox<Product>(p => p.ProductName);
// assert
Assert.False(String.IsNullOrEmpty(result));
Assert.True(Regex.IsMatch(result, "id=\"ProductName\"", RegexOptions.IgnoreCase), result);
}
Green - make it work
And made it pass with the following code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static string TextBox<T>(this HtmlHelper helper,
Expression<Func<T, object>> memberAccessExpression)
{
MemberExpression memberExpression =
memberAccessExpression.Body as MemberExpression;
// Choice: instead of throwing an exception,
// you could return an empty string
if (memberExpression == null)
{
throw new InvalidExpressionException(
memberAccessExpression
+ " is not a valid propery access");
}
string name memberExpression.Member.Name;
return helper.TextBox(name);
}
Thanks here go to Nigel Sampson for some help with the nuances of using LINQ MemberExpressions to grab property names.
Refactor, refactor, refactor
Finally, I refactored the property name resolution so that I could share it amongst both TextBox
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public static string GetNameOfMember<T>(Expression<Func<T, object>> memberAccessExpression)
{
MemberExpression memberExpression =
memberAccessExpression.Body as MemberExpression;
// Choice: instead of throwing an exception,
// you could return an empty string
if (memberExpression == null)
{
throw new InvalidExpressionException(
memberAccessExpression
+ " is not a valid propery access");
}
return memberExpression.Member.Name;
}
public static string TextBox<T>(this HtmlHelper helper,
Expression<Func<T, object>> memberAccessExpression)
{
string name = GetNameOfMember<T>(memberAccessExpression);
return helper.TextBox(name);
}
public static string ValidationMessage<T>(this HtmlHelper helper, Expression<Func<T, object>> memberAccessExpression)
{
string name = GetNameOfMember<T>(memberAccessExpression);
return helper.ValidationMessage(name);
}
public static string ValidationMessage<T>(this HtmlHelper helper, Expression<Func<T, object>> memberAccessExpression, string validationMessage)
{
string name = GetNameOfMember<T>(memberAccessExpression);
return helper.ValidationMessage(name, validationMessage);
}
For completeness, here is the code
- HtmlHelperExtensions.cs
a complete set of typed extensions on HtmlHelper’s various helper functions - HtmlHelperExtensionsFixture.cs
tests for the important bits using xUnit.NET and Moq - MoqHtmlHelper.cs
a MoqHtmlHelper class that I wrote to mock up HtmlHelper for testing - MvcMockHelpers.cs
The MvcMoqHelper class that Kzu contributed to Scott Hanselman’s blog - mvcApplication49_stronglyTypedHtmlHelpers.zip
A modified version of the sample application ScottGu included in his post that demonstrates usage of this code.