Update: This project has been open-sourced. See details in this post.
Another Update: This article was written using asp.net MVC preview 4. The better option now is to use MVCContrib’s validators or the MVCContrib fluent html project in general with IDataErrorInfo. I leave this post here as a reference. Also, if you wish to have a more Django style forms framework, this would be a great starting point. The code is freely available for anyone who wishes to use it.
One thing that is notably missing from ASP.NET MVC is a good way to handle forms and their validation. To resolve this issue, I started on a simple forms framework this weekend.
The end goal
I don’t particularly like the action filter to handle the insertion of the model as a parameter. I would prefer it be done via windsor and interceptors; however, for the first go round I have decided to keep the castle stack out of this. The technique could easily be adapted to use MVC Contrib’s WindsorControllerFactory and interceptors so that attributes do not have to be on every action you wish to use a form helper with. More on that later.
- Field – A field is the smallest unit of input and validation in a form.
- Widget – A widget is an abstraction of HTML template text for input.
- FormBase – Base class for form helpers.
- ModelForm – A form auto created from a POCO model.
A widget has a name, a value, and some attributes. The name and value properties are by default shortcuts to the name and value attributes of the widget’s attribute collection. However, this behavior is overridable in subclasses. A widget also has a way of rendering itself as XHTML.
A field has a name, a value, and a widget. By default a field is required, and it has a publicly exposed validate method so that it can be asked to validate its value. It also has the ability to output itself as XHTML.
FormBase, the base class for forms contains a collection of fields, a method to validate the fields, and a method to load the values of fields from a name value collection. The latter facilitates the loading of data from a browser request.
The first concrete implementation of FormBase I created was ModelForm. A ModelForm accepts a generic type argument and in its constructor takes an object of that type. It uses this object to generate fields for the form. The generation logic is implemented using the strategy pattern so that it is easily customizable.
Here is the strategy interface
The ModelForm registers default strategies
The constructor allows you to pass in your own strategies
With that, we have everything we need to make a form from a POCO.
Model Action Filter
One of the neatest things ASP.NET MVC does is allow you to make controllers with parameters that will be filled in from the request.
I wanted to be able to do this with my form classes as well.
For the first go round, I decided not to use what I would prefer: Windsor and Interceptors. Instead I integrated the MVC way by using their action filters.
The filters give us everything we need to set the values for a parameter. We have access to the parameters through the ActionMethod.GetParmaeters() method and we can set the parameters via the ActionParameters dictionary.
Here is the action filter
FormFactory is a simple helper class that uses a strategy pattern to create forms based upon the type passed in and a NameValueCollection.
Now, this action will work!
Rendering the Form in a View
I provided three canonical methods for rendering the form:
AsDiv – renders the form with each field wrapped in a div
AsTable – render the form with each field as a table row
AsList – render the form with each field as a list item of an unordered list
I also provided a AsCustom method that allows you to specify an XElement to wrap the form fields inside of and an XElement to use as the parent for the children generated by the field instances.
If you want even more flexibility, the rendering is completely overridable by subclassing.
Rendering the form is as simple as passing it to a view and calling AsDiv() or your preferred alternative.
All output is valid XHTML. Invalid fields receive an error class.