Thursday, September 4, 2008

Making the ASP.NET DataGrid Usable

If you have ever used an ASP.NET DataGrid (or GridView without a DataSource control), then you know there is a lot of boiler plate code needed.  The project I am working on right now has a bunch of grids that need to edit, add and page over a certain data type, so I we wanted to avoid duplicating all that code across every page. 
A little research and a prototype later, I came up with what I call the AutoGrid.  For the most part, it just handles the editing, paging and error handling internally.  Arguably more interesting is the fact that it is a generic control; I was under the impression that web controls could not have type parameters.  While this is technically true, there is a cool work around, which is detailed in a blog post by Eilon Lipton.

Basically, you need create a control with generic arguments, and whatever functionality you are after. Then to create another control to be used in asp.net markup. It needs to inherits from the first control and have a string property for each generic argument.  Finally you override the ControlBuilder for your first control and construct your second control by passing the type arguments to a Type  instance of the generic control.  The code for the non-generic markup control is below:
[ControlBuilder(typeof(GenericControlBuilder))]
public class GenericGrid : AutoGrid<Object>
{
  private string _objectType;

  public string ObjectType
  {
      get
      {
          if (_objectType == null)
          {
              return String.Empty;
          }
          return _objectType;
      }
      set
      {
          _objectType = value;
      }
  }
}

As you can see, it doesn't do much by itself.  The ControlBuilder is where the magic happens (this code is straight from Eilon's example):
public class GenericControlBuilder : ControlBuilder
{
  public override void Init(TemplateParser parser, ControlBuilder parentBuilder,
      Type type, string tagName, string id, IDictionary attribs)
  {

      Type newType = type;

      if (attribs.Contains("objecttype"))
      {
          // If objecttype is specified, create a generic type that is bound
          // to that argument and then hide the objecttype attribute.
          Type genericArg = Type.GetType(
              (string)attribs["objecttype"], true, true);
          Type genericType = typeof(AutoGrid<>);
          newType = genericType.MakeGenericType(genericArg);
          attribs.Remove("objecttype");
      }

      base.Init(parser, parentBuilder, newType, tagName, id, attribs);
  }
}

The other thing that makes this work is an IoC container.  In this case I'm using Structure Map, but any container would work.  The idea is that since the AutoGrid knows the type it is editing, it can ask for the appropriate data access class.  Chad Myers helped me get the configuration right, using a custom TypeScanner:
public class RepositoryConventionScanner : ITypeScanner
{
  public void Process(Type type, Registry registry)
  {
      Type repoForType = GetGenericParamFor(type.BaseType, typeof(Repository<>));

      if (repoForType != null)
      {
          var genType = typeof(IRepository<>).MakeGenericType(repoForType);
          registry
              .ForRequestedType(genType)
              .AddInstance(new ConfiguredInstance(type));
      }
  }

  private static Type GetGenericParamFor(Type typeToInspect, Type genericType)
  {
      if (typeToInspect != null
          && typeToInspect.IsGenericType
          && typeToInspect.GetGenericTypeDefinition().Equals(genericType))
      {
          return typeToInspect.GetGenericArguments()[0];
      }

      return null;
  }

Then in your Application_Start event, just tell Structure Map to run this scanner on your assembly:
StructureMapConfiguration
  .ScanAssemblies()
  .IncludeAssemblyContainingType<GenericGrid>()
  .With(new RepositoryConventionScanner());

I wrote this code for a pretty specific situation, your mileage may vary.  I would love to hear thoughts about this, if you care to see the whole solution you can get it here. 

No comments: