WPF currently, and Silverlight
in v5, enables you to create your own custom markup extensions.
Markup extensions are those little strings in XAML that are
typically (but not always) enclosed in {curly braces} which, as the
name implies, add new functionality to XAML. The most commonly used
markup extensions are the {StaticResource}, {TemplateBinding}, and
{Binding} extensions.
Creating your own markup extensions is a nice way to integrate
your view of the world into the toolset. They're particularly
popular with MVVM and similar toolkits.
This article is, in part, an excerpt from early work in
Silverlight 5 in Action, the revised edition of Silverlight 4 in Action. SL5
in Action is due out by the end of 2011, and will have a MEAP
(early access bits) around the time of the first public developer
release of Silverlight 5.
Creating a Simple Markup Extension
There's not much ceremony to creating a markup extension. The
amount of effort required is directly proportional to the
complexity of what you're trying to do. For example:
public class HelloExtension : MarkupExtension
{
public HelloExtension() { }
public override object ProvideValue(
IServiceProvider serviceProvider)
{
return "Hello";
}
}
As shown in this example, to create a markup extension, you need
only inherit from MarkupExtension and override the ProvideValue
method. The empty constructor is necessary for use in XAML,
specifically when you include additional parameterized
constructors.
When you use the markup extension, you must declare the
namespace as explained in section 2.1.2 (of Silverlight 4/5 in
Action), and use the class name in your XAML. For example, the
following XAML uses the HelloExtension markup extension we just
created.
<Grid xmlns:ext="clr-namespace:CustomMarkupExtensions">
<TextBlock Text="{ext:Hello}" />
</Grid>
Because we followed the convention of naming our markup
extension with the suffix "Extension", we refer to it simply as
"Hello" not "HelloExtension". The convention is helpful, but not
required. When run, the result
One interesting thing is that markup extensions can be blown out
using property element syntax. So, you could use the curly braces
as shown above, or you could get the same effect by writing it this
way:
<Grid xmlns:ext="clr-namespace:CustomMarkupExtensions">
<TextBlock>
<TextBlock.Text>
<ext:Hello />
</TextBlock.Text>
</TextBlock>
</Grid>
This can be important for some scenarios where you need finer
control over what you pass in, as we'll see later.
That was a super-simple example, but shows how little you need
to do to create a markup extension. Next, we'll look at how to make
an extension that takes in a parameter or two.
Creating a Markup Extension with Support for Parameters
Typically, markup extensions will be more complex and include
the ability to accept parameters. As we previously saw, the
built-in examples such as Binding, take in a number of discrete
parameters which are used to provide key values as well as to alter
their behavior.
This example shows how to create an interesting markup extension
which will always return the maximum of two provided values.
public class MaxValueExtension : MarkupExtension
{
public MaxValueExtension() { }
public MaxValueExtension(object value1, object value2)
{
Value1 = value1;
Value2 = value2;
}
[ConstructorArgument("value1")]
public object Value1 { get; set; }
[ConstructorArgument("value2")]
public object Value2 { get; set; }
public override object ProvideValue(
IServiceProvider serviceProvider)
{
if (Value1 is IComparable && Value2 is IComparable)
{
IComparable val1 = Value1 as IComparable;
IComparable val2 = Value2 as IComparable;
if (val1.CompareTo(val2) >= 0)
return Value1.ToString();
else
return Value2.ToString();
}
else
{
return string.Empty;
}
}
}
Compared to the previous example, this adds a few new elements.
First, we have a constructor accepting two parameters. Those two
parameters are also provided as discrete properties. Note that the
properties have been marked up so they map to the parameters - this
is used by XAML serialization and, while not required, is a good
practice.
Arguably, there's at least one cheat in this code: I coerce the
final returned value to a string. That limits its usefulness for
anything other than a Text or Content property. However, it keeps
this example simple.
To use the markup extension, the syntax is similar. There's a
subtle bug in this approach, however, which I'll explain after the
example.
<Grid xmlns:ext="clr-namespace:CustomMarkupExtensions">
<TextBlock Text="{ext:MaxValue Value1=200,Value2=25}" />
</Grid>
In this example, you'd expect the TextBlock to display 200, but
it displays 25. This is because the values, in the absence of any
other type cues, are treated as strings. One way to force them to
be treated as numbers is to break the statement out using the
property element syntax described earlier. The next example shows
the verbose approach to using the markup extension
<Grid xmlns:ext="clr-namespace:CustomMarkupExtensions"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<TextBlock>
<TextBlock.Text>
<ext:MaxValue>
<ext:MaxValueExtension.Value1>
<sys:Int32>200</sys:Int32>
</ext:MaxValueExtension.Value1>
<ext:MaxValueExtension.Value2>
<sys:Int32>25</sys:Int32>
</ext:MaxValueExtension.Value2>
</ext:MaxValue>
</TextBlock.Text>
</TextBlock>
</Grid>
This markup shows the expanded approach. While this is
significantly longer than the previous example, it does provide
complete control over the parameters. Note also that the MaxValue
markup extension uses its full name MaxValueExtension when
referencing its own parameters.
With the types correctly specified, it works as expected. Of
course, you could build intelligence into your markup extension or
use other ways of forcing the types. You could even create a markup
extension that always returns an integer and use that as a
parameter to your MaxValue extension.
Let's go the approach of adding some brains to our markup
extension.
Creating a Markup Extension with Type Casting Support
So far, we've done nothing with the IServiceProvider parameter
to the ProvideValue function. That object provides some interesting
information about the target that this extension is going to
populate. The IServiceProvider interface includes a GetService
function which can return an IProvideValueTarget. Using that
interface, you can access the target object and property, get
property/type information from them, and set the property value
directly.
In our case, we want to make sure that if we pass in two ints,
but plan to use the result in a TextBlock, that the resulting int
is converted to a string. If you leave the conversion out, you will
get an error at runtime when you specify the types using, for
example, this syntax (same approach as the earlier example):
<Grid xmlns:ext="clr-namespace:CustomMarkupExtensions"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<TextBlock>
<TextBlock.Text>
<ext:MaxValue>
<ext:MaxValueExtension.Value1>
<sys:Int32>100</sys:Int32>
</ext:MaxValueExtension.Value1>
<ext:MaxValueExtension.Value2>
<sys:Int32>90</sys:Int32>
</ext:MaxValueExtension.Value2>
</ext:MaxValue>
</TextBlock.Text>
</TextBlock>
</Grid>
As mentioned previously, you normally get strings as parameters
unless you break it out and get explicit with what you're passing
in. Once you do that, you have to build conversion smarts into your
markup extension. Here's the full markup extension with the type
conversion logic built in.
using System;
using System.Windows.Markup;
using System.Windows;
using System.Reflection;
namespace CustomMarkupExtensions
{
// totally optional. I have the return type here just so you know
// it's possible. With a return type of object, you wouldn't normally
// include the return type attribute
[MarkupExtensionReturnType(typeof(object))]
public class MaxValueExtension : MarkupExtension
{
public MaxValueExtension() { }
public MaxValueExtension(object value1, object value2)
{
Value1 = value1;
Value2 = value2;
}
[ConstructorArgument("value1")]
public object Value1 { get; set; }
[ConstructorArgument("value2")]
public object Value2 { get; set; }
private object GetMaxValue()
{
if (Value1 is IComparable && Value1 is IComparable)
{
if (((IComparable)Value1).CompareTo(Value2) >= 0)
return Value1;
else
return Value2;
}
else
{
// use val1
return Value1;
}
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (serviceProvider == null)
return null;
// get the target of the extension from the IServiceProvider interface
IProvideValueTarget ipvt =
(IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
DependencyObject targetObject = ipvt.TargetObject as DependencyObject;
object val = GetMaxValue();
if (ipvt.TargetProperty is DependencyProperty)
{
// target property is a DP
DependencyProperty dp = ipvt.TargetProperty as DependencyProperty;
if (val is IConvertible)
{
val = Convert.ChangeType(val, dp.PropertyType);
}
}
else
{
// target property is not a DP, it's PropertyInfo instead.
PropertyInfo info = ipvt.TargetProperty as PropertyInfo;
if (val is IConvertible)
{
val = Convert.ChangeType(val, info.PropertyType);
}
}
return val;
}
}
}
This example is more complex, but if you break down the code,
there's nothing magical going on here. Inside ProvideValue, I check
to see if we have a DependencyProperty or a regular property.
Based on the type of property we're accessing, I get the
PropertyType and then, if the type is convertible, call
Convert.ChangeType to handle the type conversion.
MarkupExtensions can provide some interesting functionality to
your XAML. Of course, you'll want to temper this with testability
concerns (it's harder to test this than logic in your viewmodel),
but it can get you out of some scrapes, and otherwise clean up
nasty glue/utility code you may need to write in your
code-behind.
This was written against and tested on WPF4. I haven't
tested this on Silverlight 5, so if you're reading this in the
Silverlight 5 timeframe, your mileage may vary.