WPF: Adding Metadata to objects in XAML using a MarkupExtension

A Visual Studio 2008 Solution with the complete code listing for this posting is attached.

It is quite common to augment data with metadata. An example is the set of tags on this post. How would we represent such metadata in a XAML document?

XAML provides a method for augmenting our objects - the Attached Property. It would be very simple to have a "Metadata" Attached Property whose type is a collection of metadata objects. When this attached property is set, the metadata is stored in the appropriate place and cross referenced with the data object to which the metadata refers. Simple! Well, not really.

The Attached Property method is all well and good so long as the data object is a DependencyObject because Attached Properties can only be attached to DependencyObjects. It is possible, however, to use a MarkupExtension to do a lot of what an Attached Property does.

In this article I demonstrate how to use a custom MarkupExtension to register metadata for non-DependencyObject types.

Consider the XAML below. A DateTime object is created as a resource of the window and then displayed in a TextBlock:

  1. <Window x:Class="ObjectMetadata.Window1"
  2. xmlns=... >
  3.  
  4. <Window.Resources>
  5. <system:DateTime x:Key="TheObject">
  6. 1/26/1788
  7. </system:DateTime>
  8. </Window.Resources>
  9.  
  10. <StackPanel>
  11. <TextBlock Margin="5">The object is:</TextBlock>
  12. <TextBlock Margin="5" Text="{Binding Source={StaticResource TheObject}}" />
  13. </StackPanel>
  14. </Window>

I would like to attach metadata to the DateTime to indicate the significance of the date. Snaps are awarded to those who know the significance of the above date (26 Jan 1788) without reading on.

The first thing we need is a simple metadata framework. It doesn't get any simpler than a static mapping from the data object to a list of metadata objects:

  1. // A dumb registry for metadata. A mapping from the object to it's metadata.
  2. public static class MetadataRegistry
  3. {
  4. public static Dictionary<object, List<Object>> Metadata =
  5. new Dictionary<object, List<object>>();
  6. }

Now we write a custom MarkupExtension that holds onto both the data and metadata objects. The ProvideValue method returns the data object, in effect substituting the MarkupExtension with the data object. The trick here is that the MarkupExtension can also register the metadata objects in the ProvideValue method.

  1. [ContentProperty("MetadataTarget")]
  2. public class MetadataExtension : MarkupExtension
  3. {
  4. public Object MetadataTarget { get; set; }
  5. public List<Object> Metadata { get; set; }
  6.  
  7. public MetadataExtension()
  8. {
  9. Metadata = new List<object>();
  10. }
  11.  
  12. public override object ProvideValue(IServiceProvider serviceProvider)
  13. {
  14. // register the metadata
  15. MetadataRegistry.Metadata[MetadataTarget] = Metadata;
  16.  
  17. // return the child that we decorated
  18. return MetadataTarget;
  19. }
  20. }

We can now decorate the DateTime object using our MarkupExtension. Here we add in some strings as metadata, revealing the significance of the date:

  1. <local:Metadata x:Key="TaggedObject">
  2.  
  3. <!-- The metadata objects -->
  4. <local:Metadata.Metadata>
  5. <system:String>Australia Day</system:String>
  6. <system:String>Anniversary Day</system:String>
  7. <system:String>Foundation Day</system:String>
  8. <system:String><a href="http://en.wikipedia.org/wiki/Australia_Day</system:String>
  9. ">http://en.wikipedia.org/wiki/Australia_Day</system:String>
  10. </a> </local:Metadata.Metadata>
  11.  
  12. <!-- The data to which the metadata refers --->
  13. <system:DateTime>1/26/1788</system:DateTime>
  14.  
  15. </local:Metadata>

Another simple MarkupExtension can retrieve the metadata for a given object:

  1. public class GetMetadataExtension : MarkupExtension
  2. {
  3. public Object Target { get; set; }
  4.  
  5. public override object ProvideValue(IServiceProvider serviceProvider)
  6. {
  7. // return the metadata for the target object
  8. return MetadataRegistry.Metadata[Target];
  9. }
  10. }

Using the GetMetadataExtension, we can easily display the metadata in a ListBox:

  1. <!-- NOTE: we should be able to write the following, but it causes a
  2. parsing error in the XAML parser. Luckily it works properly when we
  3. use Property Element syntax.
  4.  
  5. <ListBox Margin="5" ItemsSource="{local:GetMetadata Target={StaticResource TaggedObject} }">
  6. -->
  7. <ListBox Margin="5">
  8. <ListBox.ItemsSource>
  9. <local:GetMetadata Target="{StaticResource TaggedObject}" />
  10. </ListBox.ItemsSource>
  11. </ListBox>

Putting it all together we get the following XAML file:

  1. <Window x:Class="ObjectMetadata.Window1"
  2. xmlns=...>
  3.  
  4. <Window.Resources>
  5. <local:Metadata x:Key="TaggedObject">
  6. <local:Metadata.Metadata>
  7. <system:String>Australia Day</system:String>
  8. <system:String>Anniversary Day</system:String>
  9. <system:String>Foundation Day</system:String>
  10. <system:String><a href="http://en.wikipedia.org/wiki/Australia_Day</system:String>
  11. ">http://en.wikipedia.org/wiki/Australia_Day</system:String>
  12. </a> </local:Metadata.Metadata>
  13. <system:DateTime>1/26/1788</system:DateTime>
  14. </local:Metadata>
  15. </Window.Resources>
  16.  
  17. <StackPanel>
  18. <TextBlock Margin="5">The object is:</TextBlock>
  19. <TextBlock Margin="5" Text="{Binding Source={StaticResource TaggedObject}}" />
  20.  
  21. <TextBlock Margin="5,15,5,5">The tags on the object are:</TextBlock>
  22.  
  23. <ListBox Margin="5">
  24. <ListBox.ItemsSource>
  25. <local:GetMetadata Target="{StaticResource TaggedObject}" />
  26. </ListBox.ItemsSource>
  27. </ListBox>
  28. </StackPanel>
  29. </Window>

Which produces the following window:

So there we have it - data augmentation at XAML parse time using a MarkupExtension to do the work that would normally be the domain of the Attached Property!

AttachmentSize
Package icon ObjectMetadata.zip8.97 KB