Thursday, May 15, 2008

MVC Framework Scaffolding

Scaffolding is an idea from Ruby on Rails. It's a way of providing a simple default implementation of common views of data in a web application. After coding a couple of Controllers for the simple administration of lookup data in Suteki Shop, I got really bored of the repetitive coding and decided to write a simple scaffolding controller. Using an IoC Container and the IRepository pattern makes this really easy. To create a new scaffolding controller simply write:

public class CountryController : ScaffoldController<Country>
{
}

And here's a screen-shot of the two country views, Index and Edit.

sutekishopCountries

sutekishopCountriesEdit

It's clever enough to work out which properties are foreign keys and get the correct lookup table to populate the 'Post Zone' combo box. It also uses my generic ordering implementation to allow you to reorder the items (that's the little green up and down arrows in the list of countries). Unfortunately, I haven't got around to auto generating the views yet, so you still have to write those.

All the code can be found in Suteki Shop as usual. But just to show how it's really quite simple, here's the full code of the ScaffoldController:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Suteki.Shop.Repositories;
using Suteki.Shop.Services;
using Suteki.Shop.ViewData;
using Suteki.Shop.Validation;
using Suteki.Shop.Extensions;
using Castle.MicroKernel;
using System.Reflection;

namespace Suteki.Shop.Controllers
{
   public class ScaffoldController<T> : ControllerBase where T : class, IOrderable, new()
   {
       public IKernel Kernel { get; set; }
       public IRepository<T> Repository { get; set; }
       public IOrderableService<T> OrderableService { get; set; }

       public virtual ActionResult Index()
       {
           return RenderIndexView();
       }

       private ActionResult RenderIndexView()
       {
           var items = Repository.GetAll().InOrder();
           return RenderView("Index", Scaffold.Data<T>().With(items));
       }

       public virtual ActionResult New()
       {
           T item = new T
           {
               Position = OrderableService.NextPosition
           };
           return RenderView("Edit", BuildEditViewData().With(item));
       }

       [NonAction]
       public virtual ScaffoldViewData<T> BuildEditViewData()
       {
           ScaffoldViewData<T> viewData = Scaffold.Data<T>();
           AppendLookupLists(viewData);
           return viewData;
       }

       public virtual ActionResult Edit(int id)
       {
           T item = Repository.GetById(id);
           return RenderView("Edit", BuildEditViewData().With(item));
       }

       public virtual ActionResult Update()
       {
           int id = int.Parse(this.ReadFromRequest(typeof(T).GetPrimaryKey().Name));
           T item = null;

           if (id == 0)
           {
               item = new T();
           }
           else
           {
               item = Repository.GetById(id);
           }

           try
           {
               ValidatingBinder.UpdateFrom(item, Request.Form);
               if (id == 0)
               {
                   Repository.InsertOnSubmit(item);
               }
               Repository.SubmitChanges();

               return RenderIndexView();
           }
           catch (ValidationException validationException)
           {
               return RenderView("Edit", BuildEditViewData().With(item)
                   .WithErrorMessage(validationException.Message));
           }
       }

       public virtual ActionResult MoveUp(int id)
       {
           OrderableService.MoveItemAtPosition(id).UpOne();
           return RenderIndexView();
       }

       public virtual ActionResult MoveDown(int id)
       {
           OrderableService.MoveItemAtPosition(id).DownOne();
           return RenderIndexView();
       }

       /// <summary>
       /// Appends any lookup lists T might need for editing
       /// </summary>
       /// <param name="viewData"></param>
       [NonAction]
       public virtual void AppendLookupLists(ScaffoldViewData<T> viewData)
       {
           // find any properties that are attributed as a linq entity
           foreach (PropertyInfo property in typeof(T).GetProperties())
           {
               if (property.PropertyType.IsLinqEntity())
               {
                   AppendLookupList(viewData, property);
               }
           }
       }

       private void AppendLookupList(ScaffoldViewData<T> viewData, PropertyInfo property)
       {
           if (Kernel == null)
           {
               throw new ApplicationException("The Kernel property must be set before AppendLookupLists is called");
           }

           // get the repository for this Entity
           Type repositoryType = typeof(IRepository<>).MakeGenericType(new Type[] { property.PropertyType });

           object repository = Kernel.Resolve(repositoryType);
           if (repository == null)
           {
               throw new ApplicationException("Could not find IRepository<{0}> in kernel".With(property.PropertyType));
           }

           MethodInfo getAllMethod = repositoryType.GetMethod("GetAll");

           // get the items
           object items = getAllMethod.Invoke(repository, new object[] { });

           // add the items to the viewData
           viewData.WithLookupList(property.PropertyType, items);
       }
   }
}

1 comment:

Anonymous said...

I was looking into ways of integrating dynamic data with mvc and came across to your blog.
very interesting post.