Introducing the Smart Grid for ASP.NET MVC

Edit: Some documentation for the Grid is now available at the MvcContrib Wiki.

I’ve recently been working on an equivalent of the ASP.NET GridView for use with ASP.NET MVC.

This is partially inspired by the SmartGrid component which is part of the Castle Contrib project, but uses lambdas in order to build up a set of columns that can be automatically turned into an HTML table. For example, I might have this in an ASP.NET MVC Controller:

public class HomeController {
	private UserRepository repository = new UserRepository();

	public ActionResult Index() {
		ViewData["users"] = repository.FindAll();
		return RenderView();
	}
}

Then in my View I could have this:

<%
Html.Grid<Person>(
	"people",
	column => {
		column.For(p => p.Id);
		column.For(p => p.Name);
		column.For(p => p.Gender);
		column.For(p => p.RoleId);
	}
);
%>

Which would create something like this:

grid1

Note how the column names are automatically generated from the lambda expressions. However, you can override a column heading:

column.For(p => p.Id, "ID Number");

You can also create custom columns using lambda statements…

column.For("Custom Column").Do(p => { %>
	<td>A custom column...</td>
<% });

…and columns can have formatting applied to them:

column.For(p => p.DateOfBirth).Formatted("{0:d}");

Pagination is also fully supported by using the AsPagination extension method which works on any IEnumerable<T> or IQueryable<T>

public ActionResult Index(int? page) {
	ViewData["users"] = repository.FindAll().AsPagination(page ?? 1);
	return RenderView();
}

Which would produce something like this:

grid2

The source code is available in the mvccontrib trunk.

ASP.NET MVC Controllers, Windsor and IDisposable

I recently upgraded one of my larger intranet applications to the latest ASP.NET MVC release and after doing so, I noticed that the memory usage on our webserver would gradually go up and up. After 4-5 hours all the memory on the server was in use (2gb) and the only solution was to restart the application.

After doing some investigation I realised what the problem was: System.Web.Mvc.Controller implements IDisposable and the Windsor IoC container will keep a reference to all transient objects that it creates if they implement IDisposable. So not only was every controller being kept alive, but also everything stored in the ViewData dictionary.

Explicitly calling container.Release() inside the controller factory’s DisposeController method fixed the problem.

Testing Action Results with ASP.NET MVC

The latest preview of ASP.NET MVC supports returning ActionResult objects from controller actions.

This makes it very easy to test the results of an action (for example, redirecting to another controller) which was a much more involved process in previous releases.

Imagine the following controller:

public class HomeController : Controller {
	public ActionResult Index() {
		return RedirectToAction(new { controller = "Home", action = "about" });
	}
 
	public ActionResult About() {
		return RenderView();
	}
}

While previously you’d have to mock the Response.Redirect method, now you can do this:

var controller = new HomeController();
ActionRedirectResult result = controller.Index() as ActionRedirectResult;
 
if(result == null) {
	Assert.Fail("Expected an ActionRedirectResult");
}
else {
	Assert.That(result.Values["controller"], Is.EqualTo("Home"));
	Assert.That(result.Values["action"], Is.EqualTo("about"));
}

While this is certainly easier, it is still a little cumbersome. To help with this, I’ve added some extension methods to MvcContrib’s ‘TestHelper’ project. So now I can write:

var controller = new HomeController();
controller.Index().AssertIsActionRedirect().ToController("Home").ToAction("about");

Much better!

Helpers for the MS MVC Framework

Go to the bottom of this post for the download.

There’s a discussion going on at the MvcContrib site about including UI Helpers in the MvcContrib project. The discussion seems to be centralising around the issue of whether helpers should be implemented through extension methods (the approach the MVCToolkit takes), regular static methods or instance methods.

My preference is for instance methods on dedicated helper classes. This approach is the most flexible as it allows you to:

  • Replace an entire helper with a custom implementation
  • Easily share services between all helpers

I decided to put together a sample application that uses an IoC container (Windsor) for creating helpers. The sample project includes:

  • FormHelper and UrlHelper implementations that are created using the MvcContrib DependencyResolver.
  • Services for generating URLs (IUrlBuilder) and for automatic data binding (IDataBinder)
  • Delegate-based rendering (see this post on Adam Tybor’s blog for more info).
  • Using IDictionaries for specifying HTML attributes
  • Methods for generating text fields, text areas, hidden fields, forms and hyperlinks

Some sample code:

<% Form.For("person", new Hash(action => "Save"), form => { %>
	Surname: <%= form.TextField("Surname", new Hash(style => "width: 200px")) %>
	<br />
	Forename: <%= form.TextField("Forename", new Hash(style => "width: 200px"))%>
	<br /><br />
	<%= form.HiddenField("id") %>
	<%= form.Submit() %>
<% }); %>

Which generates…

<form method="post" action="/Home/Save">
	Surname: <input type="text" id="person_Surname" name="person.Surname" value="Smith" style="width: 200px" />
	<br />
	Forename: <input type="text" id="person_Forename" name="person.Forename" value="Jane" style="width: 200px" />
	<br /><br />
	<input type="hidden" id="person_id" name="person.id" value="2" />
	<input type="submit" value="Submit" />
</form>

Note: To run the sample you will need the .NET 3.5 Extensions CTP installed.

Disclaimers:

  • The DefaultDataBinder implementation uses code from Castle’s MonoRail
  • The DefaultUrlBuilder uses code from Rob Conery’s MvcToolkit
  • The Hash class is inspired by this post on Bill Pierce’s blog.

The download can be found here.

I think my code has Rails Envy

Using Brail with MS MVC can lead to almost Rails-like views…

brail1.gif