Introduction

In WPF, when you navigate to a page, you use a special type of URI known as a pack URI to do so. A pack URI is a URI that uses the pack scheme (for more information on the pack scheme in WPF, see this discussion). Pack URIs can be used to refer to:

· Page, Reference, and Content files (like pages, images, etc) in the local assembly.

· Page and Reference files (like pages, images, etc) in a referenced assembly.

· Unrelated files at the site of origin.

Some of you, though, may not have even realized that you are using a pack URI. For example, consider the following XAML markup with a hyperlink:

<Page

xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation">

...

<!--Navigate to another page that is identified by a pack URI-->

<Hyperlink NavigateUri="APage.xaml">Navigate to A Page</Hyperlink>

...

<Page>

The value of the NavigateUri attribute in this example is a relative pack URI that identifies a file that is compiled into the local assembly. The equivalent absolute version of this pack URI is shown here:

<Hyperlink NavigateUri="pack://application:,,,/APage.xaml">

As you can see, the absolute form is a bit more of a mouthful. As you use pack URIs, you’ll find that you need to remember a variety of URI formats that include the following:

· /Subfolder/APage.xaml

· RefdAssembly;component/APage.xaml

· pack://application:,,,/RefdAssembly;component/APage.xaml

· pack://application:,,,/RefdAssembly;v1.0.0.1;component/APage.xaml

· pack://siteoforigin:,,,/APage.xaml

· pack://siteoforigin:,,,/Subfolder/APage.xaml

It can be tricky remembering these various formats. This post shows how to use custom markup extensions to leave room in your brain to remember stuff like your mother’s birthday, your wedding anniversary, or that you have work today.

Ok ok. It’s probably not *that* hard to remember pack URI formats once you’ve used them enough, but custom markup extensions are fun and can be quite convenient.

Note The link to the accompanying C# sample can be found at the end of this post.

Why a Custom Markup Extension?

When solving a problem like this, your natural tendency might be to write helper code to accept an easy-to-remember description of a pack URI and generate an instance of the System.Uri class with a correctly formatted pack URI. However, this approach suffers from one major problem: it doesn’t help you to create pack URIs in XAML. At this point, you might think about creating a custom hyperlink by deriving from Hyperlink and implement properties to capture pack URI data from XAML. It’s a step in the right direction but isn’t scalable: you would have to derive custom classes for each of the controls that have URI properties.

 

Clearly, we need a class that returns a pack URI based on certain parameters, and that can be used in XAML to specify the value of any attribute that accepts a pack URI. This can be implemented by creating a custom markup extension.

 

Note You can find more detailed information on markup extensions in the SDK, including exposition of those provided by WPF and XAML and how you can use them.

 

In the pack URI case, this could be achieved by creating one custom markup extension that returns URIs like so:

 

<Hyperlink NavigateUri=" {z:PackUri APage.xaml} ">

<Hyperlink NavigateUri=" {z:PackUri AssemblyName=RefdAssembly,Path=APage.xaml} ">

<Hyperlink NavigateUri=" {z:PackUri Location=SiteOfOrigin,Path=Page.xaml} ">

Note You can find more detailed information on markup extensions in the SDK, including exposition of those provided by WPF and XAML and how you can use them.

Deriving from MarkupExtension

A custom markup extension is a class that derives from the abstract MarkupExtension class (from the System.Windows.Markup namespace):

 

using System.Windows.Markup; // MarkupExtension

namespace PackUriHelperLibrary

{

  public class PackUriExtension : MarkupExtension { }

}

 

For reasons that are described later, you should always end the name of your custom markup extensions with the “Extension” suffix.

Overriding MarkupExtension.ProvideValue

When WPF converts an element specified in markup to an instance of an object at run-time, it sets the object’s properties with the equivalent attribute values. When WPF evaluates an attribute whose value is generated by a markup extension, it calls the abstract MarkupExtension.ProvideValue method to return an instance of an object that it sets the property with. For this reason, you must override MarkupExtension.ProvideValue to implement the code that returns an instance of the desired type:

 

using System; // Uri, IServiceProvider

using System.Windows.Markup; // MarkupExtension

namespace PackUriHelperLibrary

{

  public class PackUriExtension : MarkupExtension

  {

    Location location = Location.Application;

    string assemblyName = null;

    string path = null;

    // Get the file location:

    // * LocalAssembly

    // * ReferencedAssembly

    // * SiteOfOrigin

    public Location Location

    {

      get { return this.location; }

      set { this.location = value; }

    }

    // Get the name of the assembly that the file is in

    // if location is not the site of origin)

    public string AssemblyName

    {

      get { return this.assemblyName; }

      set { this.assemblyName = value; }

    }

    // Get the path to the file

    public string Path

    {

      get { return this.path; }

      set { this.path = value; }

    }

    public override object ProvideValue(IServiceProvider serviceProvider)

    {

      return PackUriHelper.GetPackUri(this.location, this.assemblyName, this.path);

    }

  }

  public static class PackUriHelper

  {

    const string LocOnlyAppPackUriTemplate = "pack://application:,,,/{0}";

    const string LocOrRefAppPackUriTemplate = "pack://application:,,,/{0};component/{1}";

    const string SooPackUriTemplate = "pack://siteoforigin:,,,/{0}";

    public static Uri GetPackUri(Location location, string assemblyName, string path)

    {

      // Return URI specified by parameters, in the format defined

      // by the XxxTemplate parameters above

    }

  }

}

 

To create a pack URI, the ProvideValue override calls the helper that was shown earlier. To collect the values that are needed by the helper to construct the pack URI, PackUriExtension implements public properties. These properties are set from XAML, as you’ll see shortly.

 

Note MarkupExtension.ProvideValue is passed an argument of type IServiceProvider (from the System namespace), which provides context that is interesting in more complex scenarios than this one (see this discussion for more information).

Specifying a Markup Extension Return Type

The return type of the MarkupExtension.ProvideValue method is an object, which allows you to return any type you need. In some cases, however, this may not be explicit enough. For example, PackUriExtension only returns instances of the Uri type and, consequently, can only be used by attributes that are also of the Uri type (or attributes that know how to convert from a Uri object to get their value).

 

You can be explicit about what type your custom markup extension returns by augmenting it with the

MarkupExtensionReturnTypeAttribute (from the System.Windows.Markup namespace). PackUriExtension uses MarkupExtensionReturnTypeAttribute to specify that MarkupExtension.ProvideValue returns a value of type Uri:

 

using System; // Uri, IServiceProvider

using System.Windows.Markup; // MarkupExtension, MarkupExtensionReturnType

namespace PackUriHelperLibrary

{

  [MarkupExtensionReturnType(typeof(Uri))]

  public class PackUriExtension : MarkupExtension { ... }

}

 

Of course, even if you augment your custom markup extension class with MarkupExtensionReturnTypeAttribute, you are not required to only return the type you specify. However, you are signaling your intent and your implementation should match your intention, and the expectations of the users of your custom markup extension. This will help developers avoid using your custom markup extension in the wrong place eg using the PackUriExtension as the value of a FontSize attribute. In this case, an exception of type XamlParseException (from the System.Windows.Markup namespace) is generated.

Distributing a Custom Markup Extension

MarkupExtension.ProvideValue has all the implementation it needs to be used in XAML markup. This means providing an appropriate namespace declaration in the XAML markup that uses PackUriExtension. The namespace declaration will differ depending on how you package a custom markup extension.

 

If your custom markup extension is in the same assembly as the markup that you intend to use it from, you don’t need to do anything other than adding a namespace declaration to the namespace of your local assembly:

 

<Page ... xmlns:local="clr-namespace:PackUriMarkupExtensionClient" >

  ...

</Page>

If your custom markup extension will be reused by multiple applications, you can package it in a library assembly and reference it from your applications. In this case, you use the same syntax as above to refer to the referenced assembly, albeit with different values:

 

<Page ... xmlns:local="clr-namespace:PackUriHelperLibrary" >

  ...

</Page>

 

As a fun trick, you can be consistent with the way that WPF and XAML namespaces can be specified eg:

 

<Page

xmlns = "https://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x = "https://schemas.microsoft.com/winfx/2006/xaml"

  ...

</Page>

 

If this is something you’d like, particularly if you have several different assemblies that you’d like to logically group under one banner, you can augment your assembly with XmlnsDefinitionAttribute (from the System.Windows.Markup namespace). You would do this from your AssemblyInfo.cs file:

 

using System.Windows.Markup; // XmlnsDefinitionAttribute

...

[assembly: XmlnsDefinition(

  "https://www.microsoft.com/windowssdk/2007/xaml",

  "PackUriHelperLibrary")]

 

Then, after you’ve referenced an assembly with this attribute, you can use the following namespace declaration instead:

 

<Page ... xmlns:z="https://www.microsoft.com/windowssdk/2007/xaml" >

  ...

</Page>

Using the Custom Markup Extension

Once your XAML markup has access to the custom markup extension, you can use it just like any markup extension. To set the properties on the markup extension, you provide one or more name/value pairs, separated by commas, where the name is the name of the property. The following example shows how to use PackUriExtension into XAML:

 

<Page

  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

xmlns:z = "https://www.microsoft.com/windowssdk/2007/xaml"

  x:Class="PackUriMarkupExtensionClient.HomePage">

  ...

  <!-- Navigate to a page in the local assembly -->

  <Hyperlink NavigateUri = "{z:PackUriExtension Path=APage.xaml} ">

  ...

  </Hyperlink>

  ...

</Page>

Simplifying Custom Markup Extension Usage

Markup extension usage in markup can become wordy fairly easily, especially if they use several parameters. Fortunately, there are a few tricks you can use to reduce how much you need to type to markup extensions.

 

First, if you were good and added the “Extension” suffix to the name of your custom markup extension class, you can eschew the suffix in XAML, eg:

 

<Page

  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

xmlns:z = "https://www.microsoft.com/windowssdk/2007/xaml"

  x:Class="PackUriMarkupExtensionClient.HomePage">

  ...

  <!-- Navigate to a page in the local assembly -->

  <Hyperlink NavigateUri = "{z:PackUri Path=APage.xaml} ">

  ...

  </Hyperlink>

  ...

</Page>

 

This trick takes advantage of the fact that WPF has explicit knowledge of markup extensions so, when it sees XAML that looks like a markup extension (XAML between the left and right curly braces) it will add “Extension” to the name of the specified markup extension.

 

Second, you can alleviate the need to provide parameter name values in order to write XAML like the following:

 

<Page

  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

xmlns:z = "https://www.microsoft.com/windowssdk/2007/xaml"

  x:Class="PackUriMarkupExtensionClient.HomePage">

  ...

  <!-- Navigate to a page in the local assembly -->

  <Hyperlink NavigateUri = "{z:PackUriExtension APage.xaml} ">

  ...

  </Hyperlink>

  ...

</Page>

 

You enable this by implementing overloads of your custom markup extension’s constructor to accept arguments that equate to the parameters:

 

using System; // Uri, IServiceProvider

using System.Windows.Markup; // MarkupExtension, MarkupExtensionReturnType

namespace PackUriHelperLibrary

{

  [MarkupExtensionReturnType(typeof(Uri))]

  public class PackUriExtension : MarkupExtension

  {

    ...

    public PackUriExtension() { }

    public PackUriExtension(string path)

    {

      this.path = path;

    }

    ...

  }

}

The order of the constructor’s arguments must reflect the order in which you’ll type the name values in XAML.

 

Finally, you can determine whether your custom markup extension is trying to solve too many problems. One indicator is that your custom markup extension returns more than one unique type of data. For example, PackUriExtension can return three types of URI – one for the local assembly, one for the referenced assembly, and one for the site of origin. One side effect of this is that the user of the PackUriExtension must remember when to use which properties. In the case of PackUriExtension, this problem can be mitigated by dividing PackUriExtension into three custom markup extensions, each of which targets a specific location:

 

using System; // Uri, IServiceProvider

using System.Windows.Markup; // MarkupExtension, MarkupExtensionReturnType

namespace PackUriHelperLibrary

{

  // For pack URIs for the local assembly

  [MarkupExtensionReturnType(typeof(Uri))]

  public class LocPackUriExtension : MarkupExtension { ... }

  // For pack URIs for the referenced assembly

  [MarkupExtensionReturnType(typeof(Uri))]

  public class RefPackUriExtension : MarkupExtension { ... }

  // For pack URIs at the site of origin

  [MarkupExtensionReturnType(typeof(Uri))]

  public class SooPackUriExtension : MarkupExtension { ... }

  }

}

 

Thus, we end up with the following, even more simplified XAML markup:

 

<Page ... xmlns:z="https://www.microsoft.com/windowssdk/2007/xaml">

  ...

  <Hyperlink NavigateUri=" {z:LocPackUri APage.xaml} ">

    ...

  </Hyperlink>

  ...

  <Hyperlink NavigateUri=" {z:RefPackUri RefdAssembly,APage.xaml} ">

    ...

  </Hyperlink>

  ...

  <Hyperlink NavigateUri=" {z:SooPackUri APage.xaml} ">

    ...

  </Hyperlink>

  ...

</Page>

XxxPackUriExtension in Action

The sample that accompanies this post demonstrates the use of the various custom markup extensions that were discussed in this post. The following screenshot of the application shows them in action.

 

 

 

Each hyperlink’s text content is the value of the NavigateUri property, which is generated by the custom markup extensions.

Considerations

There is one important timing consideration when using custom markup extensions. Consider Image.ImageSource property, which you can set in XAML like so:

<Page ... xmlns:z="https://www.microsoft.com/windowssdk/2007/xaml">

...

<!-- Valid -->

<Image Source="pack://application:,,,/Figure.gif" />

...

</Page>

Image.Source is of type ImageSource although the value is a pack URI. When the XAML is parsed, a converter is used to turn the pack URI into an ImageSource with an image that comes from the location specified by the pack URI. As such, you might think that this is the perfect scenario for our custom markup extensions:

<Page ... xmlns:z="https://www.microsoft.com/windowssdk/2007/xaml">

...

<!-- Invalid -->

<Image Source=" {z:LocPackUri Figure.gif} " />

...

</Page>

This will cause a XamlParseException to be thrown because, by the time the custom markup extension is employed, the XAML parsing process ignores type converters even if they are specified.

Consequently, the Uri object that is returned by LocPackUriExtension cannot be converted into the ImageSource object that Image.Source expects. You might be tempted to try and updated the LocPackUriExtension to also return an ImageSource object but there are two good reasons not to.

First, markup extensions *should only* return objects of the type specified by MarkupExtensionReturnTypeAttribute. If you arrive at a situation where you do need to return two different types, you can create another custom markup extension that consumes the original custom markup extension to return the desired type.

Second, check the element you are using for alternative ways to use the custom markup extension. For example, you can use the following alternative syntax for Image:

<Page ... xmlns:z="https://www.microsoft.com/windowssdk/2007/xaml">

...

<Image>

< Image.Source >

< BitmapImageUriSource = "{z:LocPackUri Figure_Resource.gif}"/>

</ Image.Source >

</Image>

...

</Page>

Where are We?

This post discussed a scenario that was perfectly suited to markup extensions, although required the creation of a custom markup extension. Then, the key aspects of implementing, packaging, and using custom markup extensions were shown, before exploring techniques for simplifying their usage. Finally, custom markup extension processing constraints were covered.

PackUriMarkupExtensionSample.zip