Monday, June 11, 2012

Dynamic Data and Custom Metadata Providers

In my previous post on Dynamic Data, I mentioned that you can use the MetadataType attribute to point Dynamic Data at class that contains additional metadata for your model.  This additional metadata will give you more control over how your UI elements render.  If you don't want a column to display in your GridView, want to change the column header text from EmployeeID to Employee ID or want the cell values formatted a little differently this metadata class is where this information gets specified.  The code snippet below shows how this class can be used for customization.

[Update: 9/21/2008]: Added link to live demo

Download | Live Demo

   1: //  Attach the Employee Metadata to the Employee 
   2: //  entity that the LINQ to SQL designer generates
   3: [MetadataType(typeof(EmployeeMetadata))]
   4: public partial class Employee
   5: {
   6: }
   7:  
   8: //  Attach some additional metadata
   9: public class EmployeeMetadata
  10: {
  11:     //  Rename the EmployeeID column to Employee ID
  12:     [DisplayName("Employee ID")]
  13:     public object EmployeeID { get; set; }
  14:  
  15:     //  Format the Hire Date
  16:     [DisplayFormat(DataFormatString = "{0:d}")]
  17:     public object HireDate { get; set; }
  18:  
  19:     //  Hide the HomePhone column
  20:     [ScaffoldColumn(false)]
  21:     public object HomePhone { get; set; }
  22: }

 

That is pretty cool.  And what's even better is that if you don't like storing this information as attributes, you can swap out the default implementation and replace it with a solution that better fits your needs.  Stuff your metadata in an XML file, flat file, in-memory, or database - it is pretty much up to you.  All you need to do is write the TypeDescriptor logic that rebuilds the metadata from where ever it is you have placed it.

Below shows three different ways of specifying the a MetadataProviderFactory.  Internally, ContextConfiguration uses the AssociatedMetadataTypeTypeDescriptionProvider if a custom factory is not provided so the first two calls to RegisterContext do exactly the same thing.  In the third example I have provided my own custom provider, XmlMetadataDescriptionProvider, that reads the metadata from an xml file.

   1: //  Example 1:
   2: //  just use the default metadata provider
   3: model.RegisterContext(typeof(NorthwindDataContext), new ContextConfiguration()
   4: {
   5:     ScaffoldAllTables = true
   6: });
   7:  
   8: //  Example 2:
   9: //  this is exactly the same as above        
  10: model.RegisterContext(typeof(NorthwindDataContext), new ContextConfiguration()
  11: {
  12:     ScaffoldAllTables = true,
  13:     MetadataProviderFactory = (type => new AssociatedMetadataTypeTypeDescriptionProvider(type))
  14: });                
  15:  
  16: //  Example 3:
  17: //  here I am using a custom provider that reads the metadata from
  18: //  an xml file
  19: model.RegisterContext(typeof(NorthwindDataContext), new ContextConfiguration()
  20: {
  21:     ScaffoldAllTables = true,
  22:     MetadataProviderFactory = (type => new XmlMetadataDescriptionProvider(type, "metadata.xml"))
  23: });  

 

Implementing a Custom Metadata Provider

While building my Simple 5 Table Dynamic Data Northwind example last week, I found myself typing in the same type of metadata information for each of the properties I was showing on my screens.  I was adding attributes for things like ...

  • Add spaces into the column headers.  So ShippedDate would become Shipped Date.
  • Stripping the time component from my DateTime properties
  • Formatting my decimal properties as currency

So I created a new TypeDescriptionProvider that I have configured to supplement the AssociatedMetadataTypeTypeDescriptionProvider with additional metadata that is generated by a handful of rules.  Stuff like ...

  • DateTime properties should have a default format of {0:d}
  • decimal properties should have a default format of {0:c}
  • Split the property name into its word components and use that as its display name

It turns out there isn't a whole lot to my custom provider (download the code and take a peek).  I just run a piece of code that checks to see if the property already has the DisplayName and DisplayFormat attributes defined.  If so its a no-op.  If not, I use some simple rules to generate these attributes and add them to the PropertyDescriptor.  Below is the core logic.  A few things to note ...

  • Line 12: I first check to see if the property already has the DisplayNameAttribute defined.  If it does I don't do anything.  But if it doesn't have this attribute defined, I use the properties name to generate the friendly display name using the ToHumanFromPascal function (which I stole from here).
  • Line 24: I do the same here.  If the property doesn't have the DisplayFormatAttribute I get the default display format for the property type and apply that.
   1: public override PropertyDescriptorCollection GetProperties()
   2: {
   3:     List<PropertyDescriptor> propertyDescriptors = new List<PropertyDescriptor>();
   4:  
   5:     foreach (PropertyDescriptor propDescriptor in base.GetProperties())
   6:     {
   7:         List<Attribute> newAttributes = new List<Attribute>();
   8:  
   9:         //  Display Name Rules ...
  10:         //  If the property doesn't already have a DisplayNameAttribute defined
  11:         //  go ahead and auto-generate one based on the property name
  12:         if (!HasAttribute<DisplayNameAttribute>(propDescriptor))
  13:         {
  14:             //  generate the display name
  15:             string friendlyDisplayName = ToHumanFromPascal(propDescriptor.Name);
  16:  
  17:             //  add it to the list
  18:             newAttributes.Add(new DisplayNameAttribute(friendlyDisplayName));
  19:         }
  20:  
  21:         //  Display Format Rules ...
  22:         //  If the property doesn't already have a DisplayFormatAttribute defined
  23:         //  go ahead and auto-generate one based on the property type
  24:         if (!HasAttribute<DisplayFormatAttribute>(propDescriptor))
  25:         {
  26:             //  get the default format for the property type
  27:             string displayFormat = GetDisplayFormat(propDescriptor.PropertyType);
  28:  
  29:             //  add it to the list
  30:             newAttributes.Add(new DisplayFormatAttribute() { DataFormatString = displayFormat });
  31:         }
  32:  
  33:         propertyDescriptors.Add(new WrappedPropertyDescriptor(propDescriptor, newAttributes.ToArray()));
  34:     }
  35:  
  36:     //  return the descriptor collection
  37:     return new PropertyDescriptorCollection(propertyDescriptors.ToArray(), true);
  38: }

 

So what does all of this produce?  Well, with this metadata ...

   1: //  Attach the OrderMetadata to the Order class
   2: [MetadataType(typeof(OrderMetadata))]
   3: public partial class Order {}
   4:  
   5: [TableName("My Orders")]
   6: public class OrderMetadata
   7: {
   8:     //  Columns I want hidden
   9:     [ScaffoldColumn(false)]
  10:     public object RequiredDate { get; set; }
  11:     [ScaffoldColumn(false)]
  12:     public object ShipVia { get; set; }
  13:     [ScaffoldColumn(false)]
  14:     public object Freight { get; set; }
  15:     [ScaffoldColumn(false)]
  16:     public object ShipName { get; set; }
  17:     [ScaffoldColumn(false)]
  18:     public object ShipPostalCode { get; set; }
  19:     [ScaffoldColumn(false)]
  20:     public object ShipCountry { get; set; }
  21: }

 

and this configuration ...

   1: model.RegisterContext(typeof(NorthwindDataContext), new ContextConfiguration() { 
   2:     ScaffoldAllTables = true
   3: });

 

the orders grid looks like this.  Notice the concatenated column headers and the OrderDate and ShippedDate cell values ...

 

but with the same metadata and my custom metadata provider ...

   1: model.RegisterContext(typeof(NorthwindDataContext), new ContextConfiguration()
   2: {
   3:     ScaffoldAllTables = true,
   4:     MetadataProviderFactory = (type => new DefaultTypeDescriptionProvider(type, new AssociatedMetadataTypeTypeDescriptionProvider(type)))
   5: }); 

 

it looks like this ...

 

Conclusion

I am sure a few people are wincing that I am applying these rules at run-time when they are statically known.  No problem, move these rules from the TypeDescriptor and into your build process and auto-generate the metadata class or move the stuff to an xml file and write your own custom provider.  Or you can even use a hybrid approach like I have done here that supplements the default attribute implementation with a few basic rules which are evaluated at run-time.  The cool thing here is that you can choose what best fits your needs.

 

No comments:

Post a Comment