Tuesday, May 5, 2009

Four ASP.NET MVC Rules of Thumb

We've been cranking out a couple of ASP.NET MVC applications for a client in the past few weeks.   I really feel the framework has helped us achieve greater separation of concerns and testability in these line-of-business type apps.  I recently went back to a Web Forms app, and was interested that my MVC work had really helped inform that as well.  I found myself taking better care of my rendered HTML and separating logic much better.  It's been fun to work with, but there was a little learning curve, so below are a few rules of thumb I picked up along the way.  As the title suggests, these are not rules per-se, but things that can tend to help in ASP.NET MVC development.

#1 - Favor smaller controllers over larger ones  - Our first controller attempt lumped several concepts into one class and wound up with about 20 or so action methods on one class- all only tangentially related.  This made the default routes wordy and non-intuitive, and the "one big controller" difficult to debug and read.  In future projects, and when refactoring theses, we broke the app into more narrowly-scoped controllers.  For example prefer an "EmployeeVacationController" over a catch-all "EmployeeController" with action methods for Vacation and other Employee "stuff".  This rule would really apply to most custom-written classes- Keep It Simple and limit classes to a Single Responsibility!

#2 - Consider using ViewModel classes - Most MVC examples show directly using a model class, such as a LINQ-to-SQL or Entity Framework class.  The Visual Studio wiring for MVC even steers you into this concept with it's default "Add View" code-generation, which lets you quickly gen up views based on a single model class.  However, in real-world-apps you often need more than just a single table's data to build out a page.  Some examples get around this by stuffing secondary data into ViewData, but a better way to do this is to create a "roll-up" class to contain properties for _everything_ your view will need.  This has the added benefits of being more strongly-typed, supporting intellisense, being testable, and defining exactly what a view needs.  Here's an example:

public class TerminalIndexViewModel
{
    public Terminal TerminalInfo { get; set; }
 
    public IEnumerable<FuelUsage> LatestUsage { get; set; }
    public IEnumerable<FuelDelivery> LatestDeliveries { get; set; }
    public InventorySummary CurrentInventorySummary { get; set; }
}

This does mean the default code-gen isn't as useful- for example, it wouldn't gen up anything useful for an action that returned the above class.  You can get around this by creating your own code-gen in T4, or temporarily using the "main" model type (ie 'TerminalInfo') to generate the page and tweaking to to work with your ViewModel class.  This is an emerging pattern for Silverlight and WPF as well.  More on ViewModels.

#3 - Consider separating application logic into classes separate from the controller.  As an earlier post of mine demonstrates, it's easy to begin thinking of controllers as just business logic classes.  But they're not.  They are classes whose purpose is to coordinate sending and receiving model data to/from the views to business logic.  For all but the most simple forms-on-data apps, this means the controller should call out to another class to do things like query a data layer or perform calculations.  In these apps, we've had luck using a Visitor pattern.  The controller creates the visitor instance and has it "visit" the model to perform some complex business-specific calculations.  These visitors are extremely testable, since they are not at all concerned with data access or UI wiring.

#4 - Set up client-side 'conventions' using JQuery.  Instead of wiring up individual page elements in each view, establish some 'conventions' for your app's client-side behavior using a little jQuery in script referenced from your site.master.  For example this snippet will set up rules for how we want textboxes, dates, delete buttons, and messageboxes to behave accross the entire app:

$(document).ready(function() {
          $(".initialfocus").focus();
          $(".shortdate").datepicker();
          $(".longdatetime").datepicker();
          $(".print").click(function() { window.print(); });
          $(".delete").click(function() { return confirm("This record will be deleted.  Are you sure you want to continue? Click 'Ok' to delete this record or 'Cancel' to stay on this page."); });
          $(".messagebox")
              .animate({ opacity: 1.0 }, 5000)
              .fadeOut("slow"); 
      });

With the above in site.master, setting the initial focus in any view is just a matter of setting the css tag:

<input type="textbox" class="initialfocus"/>

  I blogged about this previously, and it's made the app's client-side behavior very consistent.

No comments: