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 ...

image

 

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 ...

image

 

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.

 

That's it.  Enjoy!


TrackBack

TrackBack URL for this entry:
http://mattberseth.com/blog-mt/mt-tb.fcgi/148

Comments


Hi Matt,

Where did you got the VS template for the dynamic data website?

I already downloaded the vs template to create dynamic data websites but the template does not work and vs 2008 does not give me any option to create dynamic data websites.

Posted by: OldBoy on August 25, 2008 09:19 AM

Another great post.

I can't see the DD approach generating a huge following in Enterprise Level design, but interesting to see Microsoft's response to the Ruby on Rails popularity in RAD circles.

Keep up the good work!

If you can live with mixing implementation patterns, this stuff is great for the ole' Admin module!!

Excuse me, but are you bugging my cube or something? Your post here just saved me hours - thanks Matt :)

@Mohammad -
I installed VS 2008 SP1 - nothing else.

@OldBoy -
Exactly - perfect for admin screens. And I think we will also use it as a first version of our new screens. If we get feedback that requires a standalone page we will create one, but until then we will just rely on DD.

@Joel -
No problem.

Posted by: Alex Coulson on August 26, 2008 08:30 AM

How did you create your XmlMetadataDescriptionProvider? Does it support any attibute?

Not fair just mentioning it on the fly! I want one.

@Alex -
Its vaporware - I just made it up, but I would imagine someone has already started building one.

Also, here is a link that I forgot to mention in my original post. It discusses how you could build an in-memory metadata provider.

Posted by: Mark Beachill on August 27, 2008 06:05 PM

Great post - this is of course how almost every database is written, it is rare that people put spaces into field names.

For the underscore freaks I would have added:
if (c == '_')
{
sb.Append(' ');
}
{
sb.Append(c);
}

But one question that's hurting my head a little -
why have you set scaffolding to false on all the fields?

And at what point are they re-scaffolded?

Mark

Posted by: Jeremy Sharp on August 30, 2008 10:48 AM

Very nice. I look forward to working with this tool and your post have set me up for success.

thx,
J

Hi Matt, great article. I was wondering why the TypeDescriptionProvider that you have implemented does not affect the Table DisplayName? Your implementation does not seem majorly different from Marcins InMemoryMetadataTypeDescriptionProvider from DD Futures OR is there something I'm missing.
Steve

Hi Matt, I've added the bit I said was missing and posted it to my blog linking back to your post as the author.
Hope you don't mind :D

Here's the link to my post: http://csharpbits.notaclue.net/2008/10/dynamic-data-custom-metadata-providers.html

Regards
Steve

Posted by: Ed on October 24, 2008 12:33 PM

Well the SWFUpload rocks I can say. Use this multi file upload control if you want to select and upload more then one file at a time.

Post a comment

(If you haven't left a comment here before, you may need to be approved by the site owner before your comment will appear. Until then, it won't appear on the entry. Thanks for waiting.)

Consulting Services

Yep - I also offer consulting services. And heck, I'll do just about anything. If you enjoy my blog just drop me an email describing the work you need done.

Recent Comments

  • Ed wrote: Well the SWFUpload rocks I can say. Use this multi file upload control if you want to select and upl...
  • Steve Naughton wrote: Hi Matt, I've added the bit I said was missing and posted it to my blog linking back to your post as...
  • Steve Naughton wrote: Hi Matt, great article. I was wondering why the TypeDescriptionProvider that you have implemented do...
  • Jeremy Sharp wrote: Very nice. I look forward to working with this tool and your post have set me up for success. thx,...
  • Mark Beachill wrote: Great post - this is of course how almost every database is written, it is rare that people put spac...
  • Matt Berseth wrote: @Alex - Its vaporware - I just made it up, but I would imagine someone has already started b...
  • Alex Coulson wrote: How did you create your XmlMetadataDescriptionProvider? Does it support any attibute? Not fair ju...
  • Matt Berseth wrote: @Mohammad - I installed VS 2008 SP1 - nothing else. @OldBoy - Exactly - perfect fo...