WPF: XAML MarkupExtension as a Macro and Object Factory

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

The MarkupExtension in XAML opens up a world of opportunity to make XAML work the way you want it to. It reminds me somewhat of macros where we think of expanding the macro as substituting the macro for something else. If we consider that the MarkupExtension does nothing more than provide a value that is substituted for the MarkupExtension, it is very much like expanding a Macro.

Parameterised macros are extremely powerful as they do more than just save you typing, they can form a domain specific language. MarkupExtensions are stateful and you can set their state from XAML, leading to the idea that the substitution can be parameterised.

In this article I will demonstrate how to use a custom MarkupExtension as a macro or an object factory. The ideas will be applied to to a simple macro expansion case and the more pertinent case of creating a custom ValueConverter in a manner that is simpler than that provided by raw XAML.

As a simple example of a macro, we might define the following in C++ to add two numbers:

  1. #define SUM( a, b ) (a + b)

And then we could use it in code like this:

  1. int i = SUM( 1, 2 );
Which expands to:
  1. int i = ( 1 + 2 );

We can achieve a similar effect in XAML in the following manner. Here we want to set the contents of a TextBlock to be the sum of two numbers:

  1. <TextBlock Text="{local:Sum Value1=1,Value2=2}" />

Where local:Sum maps to our custom MarkupExtension:

  1. public class SumExtension : MarkupExtension
  2. {
  3. public int Value1 { get; set; }
  4. public int Value2 { get; set; }
  5.  
  6. public override object ProvideValue(IServiceProvider serviceProvider)
  7. {
  8. return Value1 + Value2;
  9. }
  10. }

There is one problem left to solve - the return type of ProvideValue an integer while the TextBlock.Text property is a string. We can use the target property type and System.Convert.ChangeType here to allow conversion of the return type when such conversion is possible. We have to handle the cases where the target property is either a plain-old property or a DependencyProperty.

  1. public class SumExtension : MarkupExtension
  2. {
  3. public int Value1 { get; set; }
  4. public int Value2 { get; set; }
  5.  
  6. private static Type GetPropertyType( object property )
  7. {
  8. if (property is System.Reflection.PropertyInfo)
  9. {
  10. return ((System.Reflection.PropertyInfo)property).PropertyType;
  11. }
  12. else if (property is System.Windows.DependencyProperty)
  13. {
  14. return ((System.Windows.DependencyProperty)property).PropertyType;
  15. }
  16. throw new Exception("Unknown property type");
  17. }
  18.  
  19. public override object ProvideValue(IServiceProvider serviceProvider)
  20. {
  21. IProvideValueTarget target =
  22. (IProvideValueTarget)serviceProvider.GetService(
  23. typeof(IProvideValueTarget));
  24. Type targetPropertyType = GetPropertyType( target.TargetProperty );
  25. if (targetPropertyType != null)
  26. {
  27. return System.Convert.ChangeType(Value1 + Value2, targetPropertyType);
  28. }
  29. return null;
  30. }
  31. }

Since MarkupExtensions nest (just like macro arguments can be macros), there is a lot in inherent power in our MarkupExtension. For example, we can do the following:

  1. <!-- Equivalent to ( 1 + ( 2 + 3 ) ) -->
  2. <TextBlock Text="{local:Sum Value1=1,Value2={local:Sum Value1=2, Value2=3 } }" />

The application of this technique can be extended beyond evaluating and returning simple types. A classic example of where WPF can be a little painful to use is the ValueConverter of the Binding class.

We will be using the following custom ValueConverter. It simply doubles the value when converting forward and halves it in the reverse direction:

  1. public class DoubleValueConverter : IValueConverter
  2. {
  3. public object Convert(object value, Type targetType,
  4. object parameter, System.Globalization.CultureInfo culture)
  5. {
  6. return System.Convert.ToDouble(value) * 2.0;
  7. }
  8.  
  9. public object ConvertBack(object value, Type targetType,
  10. object parameter, System.Globalization.CultureInfo culture)
  11. {
  12. return System.Convert.ToDouble(value) * 0.5;
  13. }
  14. }

To use this value converter, we generally have two options as shown below:

  1. <!-- Option 1: Store the ValueConverter as a resource and refer to
  2. it using the StaticResource MarkupExtension -->
  3.  
  4. <SomeElement.Resources>
  5. &lt;local:DoubleValueConverter x:Key="TheDoubleConverter" />
  6. </SomeElement.Resources>
  7. ...
  8. <TextBox Text="0" Name="Source1" />
  9. <TextBox Text="{Binding ElementName=Source1, Path=Text,
  10. Converter={StaticResource TheDoubleConverter} }" />
  11.  
  12.  
  13. <!-- Option 2: Create the ValueConverter using Element Property syntax -->
  14.  
  15. <TextBox Text="0" Name="Source2" />
  16. <TextBox>
  17. <TextBox.Text>
  18. <Binding ElementName="Source2" Path="Text">
  19. <Binding.Converter>
  20. <local:DoubleValueConverter />
  21. </Binding.Converter>
  22. </Binding>
  23. </TextBox.Text>
  24. </TextBox>

However, there is a more terse and elegant solution using a custom Markup Extension:

  1. public class DoubleValueConverterExtension : MarkupExtension
  2. {
  3. public override object ProvideValue(IServiceProvider serviceProvider)
  4. {
  5. // NOTE: since the DoubleValueConverter converter is stateless, it
  6. // would be more efficient to have a singleton (ie, static) instance
  7. // being returned here. In the general case, a new stateful object
  8. // would be returned as done below.
  9. return new DoubleValueConverter();
  10. }
  11. }

Using the DoubleValueConverterExtension we can create a DoubleValueConverter in the following manner - no need for static resources or overly verbose syntax:

  1. <TextBox Text="0" Name="Source3" />
  2. <TextBox Text="{Binding ElementName=Source3, Path=Text,
  3. Converter={local:DoubleValueConverter} }" />

I have used this technique to make calls to existing object factory frameworks with great success, but there are many other applications of this technique, so keep in mind that MarkupExtensions can be thought of as macros or object factories and you will find interesting and unusual applications for them that can make your WPF life much easier.

Feel free to contact me regarding this post by email - daniel.paull at thinkbottomup.com.au - or leave comments here after registering with this site.

AttachmentSize
Package icon MarkupExtensionAsFactory.zip9.71 KB