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:
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:The base
MarkupExtension
class resides in theSystem.Xaml
assembly. Add a reference to that assembly by right-clicking the References node in the Solution Explorer, and selecting Add Reference…. Scroll down toSystem.Xaml
and select it.Delete the file
Class1.cs
that was created by the wizard.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.Mark the class as public and inherit from
MarkupExtension
.Add a
using
statement toSystem.Windows.Markup
or place the caret somewhere overMarkupExtension
, 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 {}
We need to implement the
ProvideValue
method. The easiest way to get the basic prototype is to place the caret overMarkupExtension
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(); } }
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) { }
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); }
Let's test this. Right-click on the solution node in Solution Explorer and select Add and then New Project….
Create a WPF Application project named
CH01.TestRandom
.Add a reference to the class library just created.
Open
MainWindow.xaml
. We need to map an XML namespace to the namespace and assembly ourRandomExtension
resides in:xmlns:mext="clr-namespace:CH01.CustomMarkupExtension; assembly=CH01.CustomMarkupExtension"
Replace the
Grid
with aStackPanel
and a couple ofTextBlocks
as follows:<StackPanel> <TextBlock FontSize="{mext:Random 10, 100}" Text="Hello" x:Name="text1"/> <TextBlock Text="{Binding FontSize, ElementName=text1}" /> </StackPanel>
he result is a
TextBlock
that uses a random font size between 10 and 100. The secondTextBlock
shows the generated random value.
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.