Introduction
Three weeks ago, Jeremy Miller and Chad Myers laid down their MVC approach. From the comments it's clear that many of their goals resonated with fellow developers. Since I happened to be starting a large-scale application using MVC, I was very interested in learning more. The videos, from their presentation, detailed their customized MVC stack, but didn't really provide us with anything to hit the ground running (i.e. working code).Chad has stated that he's working on some code, but I figured I'd share my implementation of something he and Jeremy talked about: the TextBoxFor control. Now I don't know exactly how they implemented their version, and mine just provides the necessary basis to get started, but hopefully it'll help people bake some of this goodness within their own code.
Base ViewPage
The first thing to do is create our own base ViewPage which our views will inherit from. Since this code relies on passing strongly-typed objects to our view (remember, "no slinging around HashTable's, IDictionary's, or magic strings"), we inherit from ViewPage<T> instead of ViewPage:
namespace CodeBetter.Web.Components
{
public class ApplicationView<T> : ViewPage<T> where T : class
{
}
}
For those not familiar with generic constraints, the where T : class states that T must be a reference type (this is actually a constraint in the base ViewPage<T> which must be repeated when we inherit from it). This simple code actually has a nasty (and stupid) side effect. To use a generic within the ASP.NET @Page directive we need to use CLR notation (because the ASP.NET directives are language agnostic). In other words, if we want to use a User object in our thunderdome system, we need to declare our view as:
<%@ Page Language="C#" Inherits="CodeBetter.Web.Components.ApplicationView`1[[CodeBetter.User,CodeBetter]]" ... %>
We can't use <XXX>, but instead have to use the CLR `[[XXX,YYY]] notation. This quickly gets out of hand if you're model is a generic type itself. The simplest solution is to use a codebehind (yuck!) file for your view:
login.aspx:
<%@ Page Language="C#" Inherits="CodeBetter.Web.Views.Login" ... %>
login.aspx.cs:
public class Login : ApplicationView<User>
{
}
Expressions
Next we're going to add the method definition for our TextBoxFor method:
protected internal MemberTextBox TextBoxFor(Expression<Func<T, string>> action)
{
}
We're going to implement MemberTextBox shortly (named because it'll server as a TextBox for a class member). First though we need to get over that crazy parameter. Expression<Func<T, string>>, what the func? I've discussed Func<T, X> and their usefulness before (you should read that blog post, good stuff!) it's simply a generic delegate that expects a type T as a parameter and returns type X. So in our example it's expecting type T (which is the type being passed as our ViewData.Model) and returns a string. As for the expressions (and I'm by no means an expert on Expressions, hopefully someone will write a good follow up to explain it better), they are a new features in .NET 3.5 used to implement LINQ (as such you'll find them in the System.Linq.Expressions namespace). When you supply a delegate as an Expression, the code doesn't actually execute. Instead it gets turned into a tree of objects which we can use to dynamically build our textbox. Still confused, look at it this way. If we do this within our view without using an Expression:
<% User user = ViewData.Model %>
...
<%= TextBoxFor(u => u.Name) %>
Guess what'll get passed into our TextBoxFor method? The user's actual name (because the code will be executed as you'd normally expect). We need more than that – we need the expression itself so we can build a consistent naming (name="u.Name") as well as type information. You might be wondering where u comes from? This is the variable that gets passed into our function of type T, which we said would be a user. You need to pass an actual instance, which is why you must render your view via:
return view(new User());
Taking it further
Now that we have that ground-work out of the way, we can focus on actually implementing this thing.
protected internal MemberTextBox TextBoxFor(Expression<Func<T, string>> action)
{
var expression = (MemberExpression)action.Body;
var function = action.Compile();
string value = function(ViewData.Model);
return new MemberTextBox(expression.ToString(), value, expression.Member);
}
This code looks more complicated than it is. First we get the body of our actual expression as a MemberExpression (if you call TextBoxFor and supply a method, like u => u.Save(), you'll get a cast exception). Next we compile the expression (you can more or less think of this as compiling your code within visual studio) which gives us a function. Remember our function definition, Func<T, string>, so we know it expects a T (in this particular case that's a User, whatever it is, ViewData.Model will always be of type T) and returns a string. We execute the actual function and get the result. We now have everything we need to build our textbox.
MemberTextBox
Now we can build an initial version of our MemberTextBox
public class MemberTextBox
{
private readonly Dictionary<string, string> _attributes;
public MemberTextBox(string name, string value, MemberInfo member)
{
_attributes = new Dictionary<string, string>(4);
_attributes["type"] = "text";
var maxlengthAttribute = member.GetCustomAttributes(typeof(MaximumLengthAttribute), true);
if (maxlengthAttribute != null && maxlengthAttribute.Length == 1)
{
_attributes["maxlength"] = ((MaximumLengthAttribute)maxlengthAttribute[0]).Length.ToString();
}
if (!string.IsNullOrEmpty(value))
{
_attributes["value"] = value;
}
}
public MemberTextBox AsPassword()
{
_attributes["type"] = "password";
return this;
}
public MemberTextBox WithClass(string cssClass)
{
_attributes["class"] = cssClass;
return this;
}
public override string ToString()
{
var sb = new StringBuilder(75);
sb.Append("<input ");
foreach(var attribute in _attributes)
{
sb.Append(attribute.Key);
sb.Append("=\"");
sb.Append(HttpUtility.HtmlEncode(attribute.Value));
sb.Append("\" ");
}
return sb.Append("/>").ToString();
}
}
This code is much more straight-forward and really provides everything we need as a basis to move on to a more advanced implementation. We're basically building up a hashtable of names and values which then gets dumped as an HTML tag via the ToString override. Do notice that within the constructor we query our property to see if it has a MaximumLengthAttribute. This is a custom attribute that we use here at work (you should be able to easily find libraries that provide attribute-based validations online). I left this in so that you could see how the TextBoxFor can be used to really write powerful and domain-driven code with minimal effort (you'll need to take it out to get this code to compile, plus fix all the other little mistakes I doubtlessly made!).
Improvement 1
If you've understood everything so far, you might be wondering why I limited my expression to returning a string. I think we can agree that it doesn't make sense to allow any object (how do you display a "UserGroup" within a textbox?), but surely we should, at the very least, also allow numbers. In order to keep things nice and clean, and really show-off the power of this approach, we'll actually create an overloaded TextBoxFor method:
protected internal MemberTextBox TextBoxFor(Expression<Func<T, int>> action)
{
var expression = (MemberExpression)action.Body;
var function = action.Compile();
int value = function(ViewData.Model);
return new MemberTextBox(expression.ToString(), value, expression.Member);
}
Which also requires an overloaded constructor within MemberTextBox (which accepts an int instead of a string):
public MemberTextBox(string name, int value, MemberInfo member) : this(name, value.ToString(), member)
{
_attributes["rel"] = "numeric";
}
As you can see, whenever we create a textbox for an integer, we'll be adding a rel="numeric" attribute to our tag. Why? Now we can actually use a JavaScript library and automatically apply a numeric mask to any inputs with a rel="numeric" (and it just so happens that I wrote an article for DotNetSlackers just the other week that accomplished this using jQuery). So, with very little effort, our code will automatically detect that our type is an int and apply a numeric mask to the appropriate textboxes.
Improvement 2
The last thing we'll look at is cleaning up our code. Specifically, we'll introduce a base MemberControl that'll provide much of the core functionality, allowing us to easily create a CheckBoxFor and so on. First though, let's clean up our ApplicationView a little (and while we're at it, let's add the CheckBoxFor method):
protected internal MemberTextBox TextBoxFor(Expression<Func<T, int>> action)
{
var expression = (MemberExpression)action.Body;
return new MemberTextBox(expression.ToString(), GetValue(action), expression.Member);
}
protected internal MemberTextBox TextBoxFor(Expression<Func<T, string>> action)
{
var expression = (MemberExpression)action.Body;
return new MemberTextBox(expression.ToString(), GetValue(action), expression.Member);
}
protected internal MemberCheckBox CheckBoxFor(Expression<Func<T, bool>> action)
{
var expression = (MemberExpression)action.Body;
return new MemberCheckBox(expression.ToString(), GetValue(action), expression.Member);
}
private V GetValue<V>(Expression<Func<T, V>> action)
{
var function = action.Compile();
return function(ViewData.Model);
}
There are only two interesting things here. First we've introduced a GetValue method to remove some of the repetition found in each of our methods. Secondly, as you probably expect, our CheckBoxFor expects members that return Booleans.
Now we create our base MemberControl class:
public abstract class MemberControl<T>
{
private readonly Dictionary<string, string> _attributes;
private readonly MemberInfo _member;
protected internal abstract string TagName { get; }
protected MemberInfo Member{ get { return _member; } }
protected internal Dictionary<string, string> Attributes
{
get { return _attributes; }
}
protected MemberControl(string name, MemberInfo member)
{
_attributes = new Dictionary<string, string>(4);
_member = member;
Add("name", name);
}
public T WithClass(string className)
{
Add("class", className);
return This();
}
public T WithId(string id)
{
Add("id", id);
return This();
}
protected internal abstract T This();
protected void Add(string key, string value)
{
_attributes[key] = value;
}
public override string ToString()
{
var sb = new StringBuilder(75);
sb.Append("<");
sb.Append(TagName);
sb.Append(" ");
foreach(var attribute in _attributes)
{
sb.Append(attribute.Key);
sb.Append("=\"");
sb.Append(HttpUtility.HtmlEncode(attribute.Value));
sb.Append("\" ");
}
return sb.Append("/>").ToString();
}
}
This is largely what we saw in our previous MemberTextBox code so should be pretty familiar. Now we'll look at our simplified MemberTextBox:
public class MemberTextBox : MemberControl<MemberTextBox>
{
protected internal override string TagName
{
get { return "input"; }
}
public MemberTextBox(string name, int value, MemberInfo member) : this(name, value.ToString(), member)
{
Add("rel", "numeric");
}
public MemberTextBox(string name, string value, MemberInfo member) : base(name, member)
{
Add("type", "text");
var attribute = member.GetCustomAttributes(typeof(MaximumLengthAttribute), true);
if (attribute != null && attribute.Length == 1)
{
Add("maxlength", ((MaximumLengthAttribute)attribute[0]).Length.ToString());
}
if (!string.IsNullOrEmpty(value))
{
Add("value", value);
}
}
protected internal override MemberTextBox This()
{
return this;
}
public MemberTextBox AsPassword()
{
Add("type", "password");
return this;
}
}
And, while we're at it, take a look at the MemberCheckBox class:
public class MemberCheckBox : MemberControl<MemberCheckBox>
{
protected internal override string TagName
{
get { return "input"; }
}
public MemberCheckBox(string name, bool value, MemberInfo member) : base(name, member)
{
Add("type", "checkbox");
if (value)
{
Add("checked", "checked");
}
}
protected internal override MemberCheckBox This()
{
return this;
}
}
Conclusion
So that certainly is a lot of code. And it is relatively complicated. However, I think the numeric textbox example and integration with our validation attributes really showcases how simple it is to write powerful UIs.