Saturday, November 20, 2010

An extendable MVC2 ModelMetaDataProvider (OCP baby!)

So, last blog I mentioned that my ModelMetaDataProvider smelled like an OCP violation to me.  While technically it's only doing one thing right now, when I read Brad Wilsons blog on what he's doing, I decided I wanted that, but didn't want that big honking class doing all that stuff, so I figured I'd do a little refactoring. I started with this.
   1:  public class MetadataProvider : DataAnnotationsModelMetadataProvider
   2:      {
   3:          protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
   4:          {
   5:              var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
   6:              var linkText = attributes.OfType<LinkText>().FirstOrDefault();
   7:              if(linkText != null)
   8:                  metadata.AdditionalValues.Add("LinkText",linkText);
   9:   
  10:              return metadata;
  11:          }
  12:   
  13:      }

I just wanna have some interface that builds up a ModelMetaData. For simplicity sake, I'll just give it the same signature as DataAnnotationsModelMetadataProvider, but just allow you to send in the ModelMetaData that's already built.


   1:     public interface IModelMetaBuilder
   2:      {
   3:          ModelMetadata BuildUp(ModelMetadata metaData, IEnumerable<Attribute> attributes, Type containerType,
   4:                                Func<object> modelAccessor, Type modelType, string propertyName);
   5:      }


Then I'll make the MetadataProvider just get all instances of those from the container, loop through em all, and build up. That way we can add new behavior without opening it back up (yeah, that's OCP).

First, lets make my LinkText stuff from my last blog and move it into one of these,

   1:  public class LinkTextModelMetadataBuilder : IModelMetaBuilder
   2:      {
   3:          public ModelMetadata BuildUp(ModelMetadata metadata, IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
   4:          {
   5:              var linkText = attributes.OfType<LinkText>().FirstOrDefault();
   6:              if (linkText != null)
   7:                  metadata.AdditionalValues.Add("LinkText", linkText);
   8:              return metadata;
   9:          }
  10:      }

Now... Lets go fix my MetaDataProvider to use my IOC container of choice (StructureMap). To be honest, this is so simple, I'm kinda embarrased that I'm making a blog out of it - but hey, I've got no pride (yeah right).

   1:   public class MetadataProvider : DataAnnotationsModelMetadataProvider
   2:      {
   3:          private readonly IContainer _container;
   4:   
   5:          public MetadataProvider(IContainer container)
   6:          {
   7:              _container = container;
   8:          }
   9:   
  10:          protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
  11:          {
  12:              var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
  13:              var allBuilders = _container.GetAllInstances<IModelMetadataBuilder>();
  14:              foreach (var builder in allBuilders)
  15:              {
  16:                  builder.BuildUp(metadata, attributes, containerType, modelAccessor, modelType, propertyName);
  17:              }
  18:              return metadata;
  19:          }
  20:   
  21:      }

Next up, lets write a cute little registry for StructureMap that'll go get em all. Oh yeah, I renamed that interface to IModelMetadataBuilder. Here it is.

   1:   public class ModelMetadataProviderRegistry : Registry
   2:      {
   3:          public ModelMetadataProviderRegistry()
   4:          {
   5:              Scan(scanner =>
   6:                       {
   7:                           scanner.AssemblyContainingType(GetType());
   8:                           scanner.AddAllTypesOf<IModelMetadataBuilder>();
   9:                       });
  10:          }
  11:      }

And there we go...  Now we just gotta change the App_Start stuff that wired up the ModelMetadataProvider to inject the Container, or better yet, just go get the instance from the container. Like so (well like the last line here - line 13).

   1:   protected void Application_Start()
   2:          {
   3:   
   4:              StructureMapInitilizer.Initilize();
   5:   
   6:              new Bootstrapper(ObjectFactory.Container).BootstrapApplication();
   7:              //TODO: make this stuff bootstrap classes
   8:              AreaRegistration.RegisterAllAreas();
   9:              RegisterGlobalFilters(GlobalFilters.Filters);
  10:              RegisterRoutes(RouteTable.Routes);
  11:              Db = OpenDatabase();
  12:              ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory(ObjectFactory.Container));
  13:              ModelMetadataProviders.Current = ObjectFactory.Container.GetInstance<MetadataProvider>();
  14:   
  15:          }

Cool... and everything is still passing! Next up... I'm gunna go steal all the cool ModelMetaData stuff from everyone, but implement them in their own builders. First up is Brad Wilsons, because it's a whole lot of awesome.

All I need to do is to instead of inheriting the DataAnnotationsModelMetaDataProvider, I'll implement IModelMetadataBuilder. Since the signatures are almost exactly the same this is really easy. Here's what I ended up with.

   1:   public class WilsonModelMetadataBuilder : IModelMetadataBuilder
   2:      {
   3:          public ModelMetadata BuildUp(ModelMetadata metadata,IEnumerable<Attribute> attributes,
   4:                                                          Type containerType,
   5:                                                          Func<object> modelAccessor,
   6:                                                          Type modelType,
   7:                                                          string propertyName)
   8:          {
   9:   
  10:            
  11:   
  12:              // Prefer [Display(Name="")] to [DisplayName]
  13:              DisplayAttribute display = attributes.OfType<DisplayAttribute>().FirstOrDefault();
  14:              if (display != null)
  15:              {
  16:                  string name = display.GetName();
  17:                  if (name != null)
  18:                  {
  19:                      metadata.DisplayName = name;
  20:                  }
  21:   
  22:                  // There was no 3.5 way to set these values
  23:                  metadata.Description = display.GetDescription();
  24:                  metadata.ShortDisplayName = display.GetShortName();
  25:                  metadata.Watermark = display.GetPrompt();
  26:              }
  27:   
  28:              // Prefer [Editable] to [ReadOnly]
  29:              EditableAttribute editable = attributes.OfType<EditableAttribute>().FirstOrDefault();
  30:              if (editable != null)
  31:              {
  32:                  metadata.IsReadOnly = !editable.AllowEdit;
  33:              }
  34:   
  35:              // If [DisplayFormat(HtmlEncode=false)], set a data type name of "Html"
  36:              // (if they didn't already set a data type)
  37:              DisplayFormatAttribute displayFormat = attributes.OfType<DisplayFormatAttribute>().FirstOrDefault();
  38:              if (displayFormat != null
  39:                      && !displayFormat.HtmlEncode
  40:                      && String.IsNullOrWhiteSpace(metadata.DataTypeName))
  41:              {
  42:                  metadata.DataTypeName = DataType.Html.ToString();
  43:              }
  44:   
  45:              return metadata;
  46:          }
  47:      }

There ya go folks... Have fun with it.
If you pull down my code, just look in myDojo.Infrastructure.Web. It's all there.

Peace out!


E

1 comment:

  1. Nice solution.

    Have you ever taken a look at MVC Turbine? It opens most of the MVC framework in a OCP manner (i.e. no big Application_Start with a bunch of unrelated classes grouped together). You could package this solution up into a single DLL and allow others to take advantage of it without copy-pasting code.

    ReplyDelete