Markup extensions! A very important aspect of XAML - but while you are
using them all the time, you probably pay very little attention to the
fact that they are markup extensions. Every time you are using those
curly braces in XAML, you are dealing with a markup extension. Probably
the most common one is
StaticResource,
but Binding is
probably up there too.
But so what? So Microsoft came up with a funny syntax for enabling
programmers to set complicated values on XML attributes easily. That by
itself isn't particularly interesting. What is interesting, however, is
that you can write and use your own markup extensions. In fact, if you
didn't know you could do this, you should start getting quite excited -
because really this is the best tool at your disposal for extending XAML
to meet your own specialized needs.
We are going to make a pretty simple markup extension here today, but it
should go a long way to showing you what you can do with them. The
purpose of our example extension is to resolve an appropriate image path
given a size and an image name:
public class ImgPathExtension : MarkupExtension
{
public double ImageSize { get; set; }
public string SubPath { get; set; }
public ImgPathExtension() { }
public ImgPathExtension(string path)
{
SubPath = path;
ImageSize = 0;
}
public ImgPathExtension(string path, double size)
{
SubPath = path;
ImageSize = size;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
var target = serviceProvider.GetService(typeof(IProvideValueTarget))
as IProvideValueTarget;
var host = target.TargetObject as FrameworkElement;
if ((ImageSize <= 0 || double.IsNaN(ImageSize)) && host != null)
{ ImageSize = host.Width; }
string folder = ImageSize > 32 ? "256x256" : "32x32";
var source = new BitmapImage(new Uri(
"pack://application:,,,/MarkupExtensionExample;Component/Images/"
+ folder + "/" + SubPath));
var prop = target.TargetProperty as DependencyProperty;
//If we aren't sure they want a ImageSource, lets give a
//full blown image.
if (prop == null || prop.PropertyType != typeof(ImageSource))
{
var img = new Image();
img.Stretch = Stretch.None;
img.Source = source;
return img;
}
return source;
}
}
So the first thing to note is that our markup extension is based on the
class
MarkupExtension.
Doing that is what makes this class a markup extension. There is only
one thing that you are required to do when deriving from that class -
implement the
ProvideValue
however you see fit.
The
ProvideValue
function on a markup extension will be called once
per use of the extension at run time (and it will be a different
instance of the markup extension every time). So this is very much not
like a binding where the value can change over time - a markup extension
only sets the value of a property once, when an element is being loaded.
So let's walk through the
ProvideValue
function of our markup
extension above. The first thing we do is use the
IServiceProvider
passed in to get the
IProvideValueTarget
instance for this use of the markup extension. This interface allows us
to get to all sorts of information about who we are providing the value
for - the
TargetObject
and the
TargetProperty
.
On the very next line of code, we use that
TargetObject
property - we
try and pull out a width. This is because we are trying to be smart, and
if no desired image size was passed into the markup extension, we are
going to use the size of the element we are providing the value for.
Next, we take the image size, and resolve it to one of two available
image sizes (behind the example application here, we have two sets of
the same images - one set 32x32 pixels in size, and the other 256x256).
We then use that size (now a folder name) and the image name to
construct a URI that resolves to the image. Since we are specifying the
URI in code (instead of in XAML), we have to specify the entire ugly
thing (according to the specs of the Pack URIScheme).
The first chuck
("pack://application:,,,/MarkupExtensionExample;Component") just
specifies that the resources are located in the DLL
MarkupExtensionExample (this application), and the rest of the path is
the resource location in that DLL.
OK, now we have a
BitmapImage
that we can return. But say we wanted to
be able to use this extension for more than just the source of an
Image
? That's where the rest of the code comes in. We poke at the
TargetProperty
, to try and find out the type it wants. If it does want
an image source (say, if it is the Source
property of an Image
) then
all is good and we return the BitmapImage
we already made. If not,
however, we make an Image
, set our BitmapImage
as the source, and
return that.
So we have gone through the code behind this markup extension - let's
take a look at how we can use it:
<Window x:Class="MarkupExtensionExample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:m="clr-namespace:MarkupExtensionExample"
Title="Window1" Height="300" Width="300">
<ScrollViewer>
<WrapPanel>
<Image Source="{m:ImgPath AutumnLeaves.jpg}" Width="32" />
<Image Source="{m:ImgPath SubPath=Creek.jpg, ImageSize=256}"
Stretch="None" />
<Image Source="{m:ImgPath Dock.jpg, 256}"
Stretch="None" />
<Image Source="{m:ImgPath DesertLandscape.jpg}"
Stretch="None" />
<Image Source="{m:ImgPath DesertLandscape.jpg, 32}"
Width="100" />
<Image Source="{m:ImgPath Creek.jpg, ImageSize=256}"
Stretch="None" />
<Image Source="{m:ImgPath Dock.jpg, 256}" Stretch="None" />
<Image Source="{m:ImgPath SubPath=AutumnLeaves.jpg}"
Stretch="None" />
<Image Stretch="None">
<Image.Source>
<m:ImgPath SubPath="AutumnLeaves.jpg" />
</Image.Source>
</Image>
<m:ImgPathExtension SubPath="Dock.jpg" />
</WrapPanel>
</ScrollViewer>
</Window>
All these different image instances cover all the different ways you can
declare and use this extension. WPF allows us to leave off the
"Extension" part of the class name (but you can still put it there if
you want to. After saying the type of markup extension to be used, we
get the choice of using the constructors or setting properties
explicitly.
As you can see in the C# code above, our markup extension has two
constructors - one that takes just the image path, and one that takes an
image path and a size. If you are going to use the constructor, you just
declare the values in a comma separated list, like so:
Source="{m:ImgPath DesertLandscape.jpg, 32}"
WPF chooses the constructor to use based on the number of arguments
provided. It does not try and do any type matching or checking - so
don't have two constructors for a markup extension that have the same
number of arguments. The behavior of such a situation is undefined - you
will never know which constructor is going to get called.
If you want to set the properties explicitly, you can use the following
syntax (a comma separated list of property/value pairs):
Source="{m:ImgPath SubPath=Creek.jpg, ImageSize=256}"
Or, if you want, you can mix and match constructor and property setting,
like so:
Source="{m:ImgPath Creek.jpg, ImageSize=256}"
In addition to the curly brace syntax, you can declare markup extensions
like any other element in xaml:
<Image Stretch="None">
<Image.Source>
<m:ImgPath SubPath="AutumnLeaves.jpg" />
</Image.Source>
</Image>
You lose the constructor ability here (you have to set properties, just
like every other XAML element), but
ProvideValue
will get called on
this instance just like the curly brace use.
Finally, if you look at the last use of the markup extension in the XAML
example, you can see where the special case of returning an full
Image
out of ProvideValue
will get hit. The use of the markup extension
there just adds another image to the wrap panel. Pretty cool, eh?
Oh, one quick note - using custom markup extensions can sometimes cause
weird issues with the Visual Studio designer. Your code will compile and
work just fine, but the designer will throw weird errors like "No
constructor for type 'foo' has 2 parameters".
Well, that about wraps it up for this intro to markup extensions. You
can grab the studio project with all the code below.
Source Files:
Comments
Post a Comment