Why All The Lambdas?

Tuesday, November 27, 2012

"Why All The Lambdas?" is a question that comes up with ASP.NET MVC.

@Html.TextBoxFor(model => model.Rating)

Instead of lambdas, why don't we just pass a property value directly?

@Html.TextBoxFor(Model.Rating)

Aren't we simply trying to give a text input the value of the Rating property?

image

There is more going on here than meets the eye, so let's …

Start From The Top

If all we need is a text box with a value set to the Rating property, then all we need to do is write the following:

<input type="text" value="@Model.Rating" />

But, the input is missing an important attribute, the name attribute, which allows the browser to associate the value of the input with a name when it sends information back to the server for processing. In ASP.NET MVC we typically want the name of an input to match the name of a property or action parameter so model binding will work (we want the name Rating, in this case). The name is easy enough to add, but we need to remember to keep the name in synch if the property name ever changes.

<input type="text" value="@Model.Rating" name="Rating"/>

Creating the input manually isn't difficult, but there are still some open issues:

- If Model is null, the code will fail with an exception.

- We aren't providing any client side validation support.

- We have no ability to display an alternate value (like an erroneous value a user entered on a previous form submission – we want to redisplay the value, show an error message, and allow the user to fix simple typographical errors).

To address these scenarios we'd need to write some more code and use if statements each time we displayed a text input. Since we want to keep our views simple,we'll package up the code inside an HTML helper.

Without Lambdas

Custom HTML helpers are easy to create.  Let's start with a naïve helper to create a text input.

// this code demonstrates a point, don't use it
public static IHtmlString MyTextBox<T>(
this HtmlHelper<T> helper, object value, string name)
{
var builder = new TagBuilder("input");
builder.MergeAttribute("type", "text");
builder.MergeAttribute("name", name);
builder.MergeAttribute("value", value.ToString());

return new HtmlString(
builder.ToString(TagRenderMode.SelfClosing)
);
}

The way you'd call the helper is to give it a value and a name:

@Html.MyTextBox(Model.Rating, "Rating")

There isn't much going on inside the helper, and the helper doesn't address any of the scenarios we thought about earlier. We'll still get an exception in the view if Model is null, so instead of forcing the view to give the helper a value (using Model.Rating), we'll instead have the view pass the name of the model property to use.

@Html.MyTextBox("Rating")

Now the helper itself can check for null models, so we don't need branching logic in the view.

// this code demonstrates a point, don't use it for real work
public static IHtmlString MyTextBox<T>(
this HtmlHelper<T> helper, string propertyName)
{
var builder = new TagBuilder("input");
builder.MergeAttribute("type", "text");
builder.MergeAttribute("name", propertyName);

var model = helper.ViewData.Model;
var value = "";

if(model != null)
{
var modelType = typeof(T);
var propertyInfo = modelType.GetProperty(propertyName);
var propertyValue = propertyInfo.GetValue(model);
value = propertyValue.ToString();
}

builder.MergeAttribute("value", value);

return new HtmlString(
builder.ToString(TagRenderMode.SelfClosing)
);
}

The above helper is not only using the propertyName parameter to set the name of the input, but it also uses propertyName to dig a value out of the model using reflection. We can build robust and useful HTML helpers just by passing property names as strings (in fact many of the built-in helpers, like TextBox, accept string parameters to specify the property name). Giving the helper a piece of data (the property name) instead of giving the helper a raw value grants the helper more flexibility.

The problem is the string literal "Rating". Many people prefer strong typing, and this is where lambda expressions can help.

With Lambdas

Here is a new version of the helper using a lambda expression.

// this code demonstrates a point, don't use it for real work
public static IHtmlString MyTextBox<T>(
this HtmlHelper<T> helper,
Func<T, object> propertyGetter,
string propertyName)
{
var builder = new TagBuilder("input");
builder.MergeAttribute("type", "text");
builder.MergeAttribute("name", propertyName);

var value = "";
var model = helper.ViewData.Model;

if(model != null)
{
value = propertyGetter(model).ToString();
}

builder.MergeAttribute("value", value);

return new HtmlString(
builder.ToString(TagRenderMode.SelfClosing)
);
}

Notice the reflection code is gone, because we can use the lambda expression to retrieve the property value at the appropriate time.  But look at how we use the helper in a view, and we'll see it is a step backwards.


@Html.MyTextBox(m => m.Rating, "Rating")

Yes, we have some strong typing, but we have to specify the property name as a string. Although the helper can use the lambda expression to retrieve a property value, the lambda doesn't give the helper any data to work with – just executable code. The helper doesn't know the name of the property the code is using. This is the point where Expression<T> is useful.

With Expressions

This version of the helper will wrap the incoming lambda with Expression<T>. The Expression<T> data type is magical. Instead of giving the helper executable code, an expression will force the C# compiler to give the helper a data structure that describes the code (my article on C# and LINQ describes this in more detail).

The HTML helper can use the data structure to find all sorts of interesting things, like the property name, and given the name it can get the value in different ways.

// this code demonstrates a point, don't use it for real work
public static IHtmlString MyTextBox<T, TResult>(
this HtmlHelper<T> helper,
Expression<Func<T, TResult>> expression)
{
var builder = new TagBuilder("input");
builder.MergeAttribute("type", "text");

// note – not always this simple
var body = expression.Body as MemberExpression;
var propertyName = body.Member.Name;
builder.MergeAttribute("name", propertyName);

var value = "";
var model = helper.ViewData.Model;
if (model != null)
{
var modelType = typeof(T);
var propertyInfo = modelType.GetProperty(propertyName);
var propertyValue = propertyInfo.GetValue(model);
value = propertyValue.ToString();
}

builder.MergeAttribute("value", value);

return new HtmlString(
builder.ToString(TagRenderMode.SelfClosing)
);
}

The end result being the HTML helper gets enough information from the expression that all we need to do is pass a lambda to the helper:


@Html.MyTextBox(m => m.Rating)

Summary

The lambda expressions (of type Expression<T>) allow a view author to use strongly typed code, while giving an HTML helper all the data it needs to do the job.

Starting with the data inside the expression, an HTML helper can also check model state to redisplay invalid values, add attributes for client-side validation, and change the type of input (from text to number, for example) based on the property type and data annotations.

That's why all the lambdas.


Comments
gravatar Tuesday, November 27, 2012
Remarkable blog post. I really like when people start explaining things with the situation which forces us to find alternative ways. And this blog post is giving a really nice walk through to the best solution.
gravatar Wednesday, November 28, 2012
Thank you for explaining the little details.
gravatar Wednesday, November 28, 2012
Instead of the reflection code in the last version (under "With Expressions"), wouldn't you be able to compile the Expression to a lambda var propertyGetter = expression.compile(); and then use value = propertyGetter(model).ToString(); i.e. as in the example under "With Lambdas"?
gravatar Wednesday, November 28, 2012
Oops, formatting is messed up in my previous comment. Newline between "lambda" and "var" and between "use" and "value"
gravatar Wednesday, November 28, 2012
Excellent article that explains the rationale behind "all the lambdas" in an intelliglbe, easy-to-follow manner.
gravatar Wednesday, November 28, 2012
Re: Using Compile() Yes, that would be an improvement in a way, because the current helper only works against the Model object, but you can (with the real helpers) write an expression that gets data from another object (i.e. Html.TextBox(model => item.Name) is a common one you'll see in a loop). The downside is potentially 1000s of compilations across a site, which caching amortize.
gravatar Wednesday, November 28, 2012
This is terrific article. Funcs and Expressions have confused me endlessly. It's also good to see this logical step by step approach to how we got what we have right now. Thanks!
gravatar Wednesday, November 28, 2012
Enlightening! Simple and very well explained.
gravatar Wednesday, November 28, 2012
Exciting way to have machines do the work, but what would be performance of that code compared to straight ASP.NET?
gravatar Scott Wednesday, November 28, 2012
Re: performance - I haven't measured the difference.
gravatar Georgi Hadzhigeorgiev Thursday, November 29, 2012
Another great post! Well done Scott!
gravatar rtpHarry Thursday, November 29, 2012
Nice article, great length, easy to follow. A++++ would buy again.
gravatar AdamL Thursday, November 29, 2012
This is a really good explanation of the evolution of an API using features of the C# language that may be a little too 'magical' to grasp how they can be leveraged. Great stuff.
gravatar Mark Thursday, November 29, 2012
Very well done, I had looked at Razor recently and this is a great addition!
gravatar Alex Friday, November 30, 2012
to compile the expression takes a lot of time. afaik the ExpressionHelper in ASP.NET MVC is caching the compiled expression.
Pauix Saturday, December 1, 2012
Great post!
gravatar Robert Zych Sunday, December 2, 2012
Expressions are great, but can feel awkward for developers unfamiliar with lambdas. I think this is where the main question is coming from. Shouldn't MVC have an alternative way of doing this? Something to the effect of @Model.Rating.TextBoxFor() would be nice to have.
gravatar gby Sunday, December 2, 2012
Pretty good explanation, thanks!
gravatar Nilesh Sunday, December 2, 2012
another nice and palatable explaination !! Thanks
gravatar Arunav Monday, December 3, 2012
Very nice post, explains it very clearly. Thanks
gravatar Avi Jassra Monday, December 3, 2012
Nice ....Thanks
Malka Tuesday, December 4, 2012
Really nice post...
gravatar aylmer carson Tuesday, December 4, 2012
great post. very informative and well structured. thanks very much :)
gravatar Jalpesh Vadgama Tuesday, December 18, 2012
Really Nice work Scott. I always prefer lambads instead of lenghy for loop. Regards, Jalpesh (www.dotnetjalps.com)
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!