So I’ve begun work on a new application, using ASP.Net MVC 3 (with Razor) and Entity Framework 4.1 (Code First). So far it looks like a good combo of technology, some head-aches and obstacles, but with the help of Google I’ve found my way through them all.
Earlier today I needed to do an “if check” before printing some data on a page, so I did something like this:
@if(!String.IsNullOrEmpty(item.Compliant))
@Html.DisplayFor(modelItem => item.Deadline, Model)
}
And then on the next line I did the exact same, but for a different property (ie. not “Deadline”). I felt a slight pain as I repeated myself (ie. the if test) and started to look at how to avoid that. I tried a number of different things, all documented below.
1. Custom code to do check and return value
My first attempt was to just take the code out of the view and put it in a static class. The code looked a little like this:
public static string DisplayBasedOnCompliantStatus(SomeClass eval, string displayText)
{
if (!String.IsNullOrWhiteSpace(eval.Compliant) && eval.Compliant.Equals(“Yes”, StringComparison.InvariantCultureIgnoreCase))
return displayText;
return String.Empty;
}
This code worked as expected, but the return value was a string, so I lost the dataformatting of the input value (DateTime). So i figured there had to be a better way…
2. Reuse of “DisplayFor” extension methods
For my next attempt I tried to take as input whatever I needed to use to call the “Html.DisplayFor” method I used in the view code. One of my attempts looked a little like this:
public static MvcHtmlString DisplayOnlyIfCompliant<TValue>(HtmlHelper<TModel> html, TModel model, Expression<Func<TModel, string>> compliantExpression, Expression<Func<TModel, TValue>> expression)
{
Func<TModel, string> deleg = compliantExpression.Compile();
var compliant = deleg(model);
if (!String.IsNullOrWhiteSpace(compliant) && compliant.Equals(“Yes”, StringComparison.InvariantCultureIgnoreCase))
{
return html.DisplayFor(expression);
}
return new MvcHtmlString(null);
}
This I could never make work, not even compile, but I felt as if I was on the right track.
3. Create another extension method
For my third attempt I had a very close look at the orignal method (“Html.DisplayFor”) and suddenly realized that it was an extension method and I could probably create another extension method myself! So the third attempt looks like this:
public static MvcHtmlString DisplayFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, SomeClass model)
{
if (!String.IsNullOrWhiteSpace(model.Compliant) && model.Compliant.Equals(“Yes”, StringComparison.InvariantCultureIgnoreCase))
{
return html.DisplayFor<TModel, TValue>(expression);
}
return new MvcHtmlString(null);
}
And what-do-you-know? It works! To use it all I had to do was this:
@Html.DisplayFor(modelItem => item.Deadline, Model)
But when looking at the code I can see that it’s a little less flexible than it could have been. What if I need the same functionality for another property beside “Compliant”? Then I have to create another method, rename the original (it already has a bad name!) and then I’m back to repeating myself! So a fourth attempt should be tried!
4. generic Extension method
My final requirement is that the extension method can handle any model to check for null-string. This meant that I had to take the model specific code out of my extension method and add another parameter to be able to define the property to check for null-string. The code looks like this:
public static MvcHtmlString DisplayFor<TModel, TValue>(
this HtmlHelper<TModel> html,
Expression<Func<TModel, TValue>> expression,
Expression<Func<TModel, string>> compliantExpression,
TModel model)
{
var compliant = compliantExpression
.Compile()
.Invoke(model);
if (!String.IsNullOrWhiteSpace(compliant) && compliant.Equals(“Yes”, StringComparison.InvariantCultureIgnoreCase))
{
return html.DisplayFor<TModel, TValue>(expression);
}
return new MvcHtmlString(null);
}
}
And in use:
@Html.DisplayFor(m => item.PropertyToDisplay, c => item.PropertyToCheck, Model)
I guess it would be possible to do a more generic “null check”, so let’s make a fifth attempt…
5. 100% Pure Generic Extension Method
The final step in making the extension method 100% pure from any “non-generic noise” is to remove the “string” part in the “compliantExpression” input definition. There is really just one obstacle to make that happen; I need a generic null check. I tried using “IComparable” and requiring the input type to inherit from it, but this caused some issues when it was null (NullReferenceException). So I had to do a som Googling and found a great question on Stackoverflow that helped me out! Turns out I can write the method like this:
public static MvcHtmlString DisplayFor<TModel, TValue, TCheck>(
this HtmlHelper<TModel> html,
Expression<Func<TModel, TValue>> expression,
Expression<Func<TModel, TCheck>> compliantExpression,
TModel model)
{
TCheck compliant = compliantExpression
.Compile()
.Invoke(model);
if (!Equals(compliant, default(TCheck)))
{
return html.DisplayFor<TModel, TValue>(expression);
}
return new MvcHtmlString(null);
}
}
And of course no need to adjust how we call the method, since types are inferred when using expressions (I guess?).
There is however a serious issue with the extension method now; I’ m only testing for “null”, not for an actual value compare, which of course I need… So, enter attempt number six.
6. 100% Pure Generic Extenstion Method with compare function
I had to make it possible to compare the actual value with a provided value, such as “Yes” or “1”. To do this I needed to add a parameter to the method enabling the caller to input a function to compare the value of “valueExpression” with a caller defined value. It now looks like this:
public static MvcHtmlString DisplayForIfEqual<TModel, TValue, TCheck>(
this HtmlHelper<TModel> html,
Expression<Func<TModel, TValue>> valueExpression,
Expression<Func<TModel, TCheck>> checkExpression,
Func<TCheck, bool> compareFunction,
TModel model)
{
TCheck checkValue = checkExpression
.Compile()
.Invoke(model);
if (!Equals(checkValue, default(TCheck)) && compareFunction(checkValue))
return html.DisplayFor<TModel, TValue>(valueExpression);
return new MvcHtmlString(null);
}
To call it:
@Html.DisplayForIfEqual(
m => item.SomeDisplayProperty,
c => item.SomeCheckProperty,
m => m.Equals(“No”, StringComparison.InvariantCultureIgnoreCase),
Model)
A curiosity is that the “m” variable does not give you IntelliSense in Visual Studio today. I was puzzled by this, but no big deal really.
There might be some issues with the “Equals” method in the null-check (like boxing), but for now I’ll be using this method.