C# WPF Tutorial - Fun With Markup Extensions [Beginner]


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