Windows Presentation Foundation 4.5 Cookbook
上QQ阅读APP看书,第一时间看更新

Creating a custom markup extension

Markup extensions are used to extend the capabilities of XAML, by providing declarative operations that need more than just setting some properties. These can be used to do pretty much anything, so caution is advised – these extensions must preserve the declarative nature of XAML, so that non-declarative operations are avoided; these should be handled by normal C# code.

Getting ready

Make sure Visual Studio is up and running.

How to do it...

We'll create a new markup extension that would provide random numbers and use it within a simple application:

  1. First, we'll create a class library with the markup extension implementation and then test it in a normal WPF application. Create a new Class Library project named CH01.CustomMarkupExtension. Make sure the checkbox Create directory for solution is checked, and click on OK:
    How to do it...
  2. The base MarkupExtension class resides in the System.Xaml assembly. Add a reference to that assembly by right-clicking the References node in the Solution Explorer, and selecting Add Reference…. Scroll down to System.Xaml and select it.
  3. Delete the file Class1.cs that was created by the wizard.
  4. Right-click the project node, and select Add Class…. Name the class RandomExtension and click on Add. This markup extension will generate a random number in a given range.
  5. Mark the class as public and inherit from MarkupExtension.
  6. Add a using statement to System.Windows.Markup or place the caret somewhere over MarkupExtension, click on the smart tag (or press Ctrl + . (dot), and allow the smart tag to add the using statement for you. This is how the class should look right now:
       public class RandomExtension : MarkupExtension {}
  7. We need to implement the ProvideValue method. The easiest way to get the basic prototype is to place the caret over MarkupExtension and use the smart tag again, this time selecting Implement abstract class. This is the result:
    public class RandomExtension : MarkupExtension {
       public override object ProvideValue(IServiceProvider sp) {
          throw new NotImplementedException();
       }
    }
  8. Before we create the actual implementation, let's add some fields and constructors:
    readonly int _from, _to; 
    public RandomExtension(int from, int to) {
       _from = from; _to = to;
    } 
    public RandomExtension(int to)
       : this(0, to) {
    }
  9. Now we must implement ProvideValue. This should be the return value of the markup extension – a random number in the range provided by the constructors. Let's create a simple implementation:
    static readonly Random _rnd = new Random();
    public override object ProvideValue(IServiceProvider sp) {
       return (double)_rnd.Next(_from, _to);
    }
  10. Let's test this. Right-click on the solution node in Solution Explorer and select Add and then New Project….
    How to do it...
  11. Create a WPF Application project named CH01.TestRandom.
  12. Add a reference to the class library just created.
  13. Open MainWindow.xaml. We need to map an XML namespace to the namespace and assembly our RandomExtension resides in:
    xmlns:mext="clr-namespace:CH01.CustomMarkupExtension;
    assembly=CH01.CustomMarkupExtension"
  14. Replace the Grid with a StackPanel and a couple of TextBlocks as follows:
    <StackPanel>
       <TextBlock FontSize="{mext:Random 10, 100}" Text="Hello"
                  x:Name="text1"/>
       <TextBlock Text="{Binding FontSize, ElementName=text1}" />
    </StackPanel>
  15. he result is a TextBlock that uses a random font size between 10 and 100. The second TextBlock shows the generated random value.
    How to do it...

How it works...

A markup extension is a class inheriting from MarkupExtension, providing some service that cannot be done with a simple property setter. Such a class needs to implement one method: ProvideValue. Whatever is returned provides the value for the property. ProvideValue accepts an IServiceProvider interface that allows getting some "context" around the markup extension execution. In our simple example, it wasn't used.

Any required arguments are passed via constructor(s). Any optional arguments can be passed by using public properties (as the next section demonstrates).

Let's try using our markup extension on a different property:

 <TextBlock Text="{mext:Random 1000}" />

We hit an exception. The reason is that our ProvideValue returns a double, but the Text property expects a string. We need to make it a bit more flexible. We can query for the expected type and act accordingly. This is one such service provided through IServiceProvider:

public override object ProvideValue(IServiceProvider sp) {
   int value = _rnd.Next(_from, _to);
   Type targetType = null;
   if(sp != null) {
      var target = sp.GetService(typeof(IProvideValueTarget)) 
         as IProvideValueTarget;
      if(target != null) {
         var clrProp = target.TargetProperty as PropertyInfo;
         if(clrProp != null)
            targetType = clrProp.PropertyType;
         if(targetType == null) {
            var dp = target.TargetProperty 
               as DependencyProperty;
            if(dp != null)
               targetType = dp.PropertyType;
         }
      }
   }
   return targetType != null ?
      Convert.ChangeType(value, targetType) :
      value.ToString();
}

You'll need to add a reference for the WindowsBase assembly (where DependencyProperty is defined). IServiceProvider is a standard .NET interface that is a kind of "gateway" to other interfaces. Here we're using IProvideValueTarget, which enables discovering what property type is expected, with the TargetProperty property. This is either a PropertyInfo (for a regular CLR property) or a DependencyProperty, so appropriate checks must be made before the final target type is ascertained. Once we know the type, we'll try to convert to it automatically using the Convert class, or return it as a string if that's not possible.

For more information on other interfaces that can be obtained from this IServiceProvider, check this page on the MSDN documentation: http://msdn.microsoft.com/en-us/library/B4DAD00F-03DA-4579-A4E9-D8D72D2CCBCE(v=vs.100,d=loband).aspx.

There's more...

Constructors are one way to get parameters for a markup extension. Properties are another, allowing optional values to be used if necessary. For example, let's extend our random extension, so that it is able to provide fractional values and not just integral ones. This option would be set using a simple public property:

   public bool UseFractions { get; set; }

The implementation of ProvideValue should change slightly; specifically, calculation of the value variable:

   double value = UseFractions ? 
      _rnd.NextDouble() * (_to - _from) + _from : 
      (double)_rnd.Next(_from, _to);

To use it, we set the property after the mandatory arguments to the constructor:

 <TextBlock Text="{mext:Random 1000, UseFractions=true}" />

Don't go overboard

Markup extensions are powerful. They allow arbitrary code to run in the midst of XAML processing. We just need to remember that XAML is, and should remain, declarative. It's pretty easy to go overboard, crossing that fine line. Here's an example: let's extend our RandomExtension to allow modifying the property value at a regular interval. First, a property to expose the capability:

   public TimeSpan UpdateInterval { get; set; }

Now, some modifications to the ProvideValue implementation:

if(UpdateInterval != TimeSpan.Zero) {
   // setup timer...
   var timer = new DispatcherTimer();
   timer.Interval = UpdateInterval;
   timer.Tick += (sender, e) => {
      value = UseFractions ?
         _rnd.NextDouble() * (_to - _from) + _from :
         (double)_rnd.Next(_from, _to);
      finalValue = targetType != null ?
         Convert.ChangeType(value, targetType) :
         value.ToString();
      if(dp != null)
         ((DependencyObject)targetObject).SetValue(
            dp, finalValue);
      else if(pi != null)
         pi.SetValue(targetObject, value, null);
   };
   timer.Start();
}

targetObject is obtained by calling IProvideValueTarget.TargetObject. This is the actual object on which the property is to be set.

And the markup:

<TextBlock Text="This is funny" 
FontSize="{mext:Random 10, 50, UpdateInterval=0:0:1}" />

This is certainly possible (and maybe fun), but it's probably crossing the line.