Why XAML?
Let’s first look at the advantage of using XAML (or
simply XML) to describe your objects. You can create objects directly
in code, can’t you? By defining your objects as XAML, it is a
lot easier to build tools that can transform this XAML, for example
custom editors can be built that can edit, change, etc… your
objects. The best example I know is the Microsoft Interactive Designer
for building user interfaces. This tool can be used by designers (yes,
people with talent for building nice user interfaces ? and developers
to build state of the art user interfaces, with animation, data binding
and such, without writing a single line of code. That is the power of
XAML!
The Maze object with XAML
Instead of using Windows Presentation Foundation to explain
XAML (I might start to explain more on WPF than on XAML), we are going
to build a custom object hierarchy that we are going to use with XAML.
This way we can look at the details of building XAML enabled objects,
how you can take advantage of the advanced features of this language
and how you can extend it for your own use. We are going to build a
little maze (hence A MAZING XAML ? with rooms and items, so
let’s start by creating a new library project which we will
call XAML.Mazes. Rename the class1.cs file to Maze.cs and build the
following class:
namespace XAML.Maze {
public class Maze {
private string _name;
public string Name {
get { return _name; }
set { _name = value; }
}
}
}
Build this project. We will later use this object in XAML, but
first let’s build an application to load our maze into
memory.
Loading XAML into your application
Add a console application to the solution and call it
XAML.TheMaze. Add the following references to the project: WindowsBase,
PresentationCore, PresentationFramework, XAML.Mazes, and add a couple
of using statements:
using System.Windows;
using System.Windows.Markup;
using XAML.Mazes;
using System.IO;
Implement your Main method as follows:
private const string myMaze = "../../myMaze.xaml";
static public Maze LoadMaze( string fromFile ) {
FileStream xamlFile = new FileStream ( fromFile, FileMode.Open, FileAccess.Read );
Maze theMaze = (Maze) XamlReader.Load ( xamlFile );
return theMaze;
}
static void Main( ) {
Maze myMaze = Program.LoadMaze ( Program.myMaze );
Console.ReadLine ( );
}
This code used the System.Windows.Markup.XamlReader class to
load a XAML file called myMaze.xaml from disk. So let’s build
a XAML file for our Maze object. Add a new XML file to your console
project called myMaze.xaml (adding a new xaml file in visual studio
will invoke the Cider built-in XAML editor, which will take a while, so
be patient):
<u:Maze
xmlns:u="clr-namespace:XAML.Mazes;assembly=XAML.Mazes" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="The Amazing XAML Maze"
>
</u:Maze>
XAML namespaces
Time to explain this XAML file a little; first of all we use a
root object called Maze. But because XAML doesn’t know our
Maze class we have to tell the XamlReader where it can find the class
definition. We do this by declaring a xml-namespace:
xmlns:u="clr-namespace:XAML.Mazes;assembly=XAML.Mazes"
This is a special syntax used in XAML to map the u prefix to
the XAML.Mazes CLR namespace and the XAML.Mazes assembly. The other
namespace declaration points to a number of specific XAML extensions
which we will discuss later… Because the Maze element uses
the u prefix, the XamlReader knows now that is needs to load the
XAML.Mazes assembly and look for the XAML.Mazes.Maze class. It creates
a new instance of our Maze object and sets the Name property to the
“The Amazing XAML Maze” string.
Stepping through this application with the Visual Studio
Debugger will show you that indeed a Maze object gets created with the
Name property set.
Object properties
XAML creates your objects by calling the default constructor,
and then uses each xml attribute to set the property on the object with
the same name. That is why our Maze instance has the Name property
value correctly set. Actually, the XAML that we are using is equivalent
to this code:
Maze myMaze = new Maze ( );
myMaze.Name = "The Amazing XAML Maze";
Adding Rooms to the Maze
Every maze needs a couple of rooms so you at least can get
lost. So let’s add a new Room class to the XAML.Mazes
project:
public class Room
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
private string _description;
public string Description
{
get { return _description; }
set { _description = value; }
}
}
Make sure the Room class is public.
Now let’s add a couple of rooms to the XAML maze
file:
<u:Maze
xmlns:u="clr-namespace:XAML.Mazes;assembly=XAML.Mazes" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="The Amazing XAML Maze"
>
<u:Room Name="room_1" Description="The first room" />
<u:Room Name="room_2" Description="The second room" />
</u:Maze>
Running our application will however result in an
“Cannot add content to an object of type
'XAML.Mazes.Maze'.” exception. That is because XAML
doesn’t know where to add our room objects to the maze
object.
Property-Element syntax
Could we have used another attribute on the Maze class to have
XAML add the room objects to the maze? Of course not, first of all
because a room is itself a complex object, and also because we have
multiple rooms. That is why XAML supports the property-element syntax,
where we can use a <Class.Property>kind of notation. But
first let’s add a Rooms property to the Maze:
public RoomCollection _rooms
= new RoomCollection();
public RoomCollection Rooms
{
get { return _rooms; }
}
For this we also need a collection of Rooms, so add a new
RoomCollection class to the XAML.Mazes project:
public class RoomCollection : List<Room>
{
}
Now we change our XAML file to look like this:
<u:Maze
xmlns:u="clr-namespace:XAML.Mazes;assembly=XAML.Mazes" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="The Amazing XAML Maze"
>
<u:Maze.Rooms>
<u:Room Name="room_1" Description="The first room" />
<u:Room Name="room_2" Description="The second room" />
</u:Maze.Rooms>
</u:Maze>
This works because the XamlReader recognizes the
<u:Maze.Rooms> syntax, and our RoomCollection class
implements the ICollection interface. Our Room objects get built as
normal, and are then added to the Rooms property by calling the
ICollection.Add method. XAML allows for another technique, which we
will elaborate later on in this article.
Our XAML file does the same as this code fragment:
Maze myMaze = new Maze ( );
myMaze.Name = "The Amazing XAML Maze";
ICollection rooms = myMaze.Rooms;
Room room1 = new Room();
room1.Name = "room_1";
room1.Description = "The first room";
Rooms.Add( room1 );
Room room2 = new Room();
room2.Name = "room_2";
room2.Description = "The second room";
Rooms.Add( room2 );
Using Multiple Namespaces in XAML
Let’s clean up our solution for a moment. First of
all I want my rooms to be in a different namespace than the Maze class
itself (who know how many different room classes we might end up at the
end of the project?). So let’s move the Room and Rooms
classes to the XAML.Mazes.Rooms namespace:
namespace XAML.Mazes.Rooms
and add a new using statement to the Maze class because we use
the RoomCollection class there:
using XAML.Mazes.Rooms;
Running the application now results in a “The tag
'Room' does not exist in XML namespace
'clr-namespace:XAML.Mazes;assembly=XAML.Mazes'” exception.
How can we solve this?
XAML actually allows you to use multiple CLR namespaces mapped
to the same XML namespace. This requires a couple of steps. First add
the following CLR attributes to the XAML.Mazes project, for example in
the Mazes class (you will need to reference the WindowsBase,
PresentationCore, and PresentationFramework assemblies again for this
to compile):
...
using System.Windows.Markup;
[assembly: XmlnsDefinition("http://www.u2u.be/amazingxaml", "XAML.Mazes")]
[assembly: XmlnsDefinition("http://www.u2u.be/amazingxaml", "XAML.Mazes.Rooms")]
[assembly: XmlnsPrefix("http://www.u2u.be/amazingxaml", "u")]
namespace XAML.Mazes
{
...
We also need to specify a mapping in the XAML file:
<?xml version="1.0" encoding="utf-8" ?>
<?Mapping XmlNamespace="http://www.u2u.be/amazingxaml" ClrNamespace="XAML.Mazes" AssemblyName="XAML.Mazes"?>
<u:Maze
xmlns:u="http://www.u2u.be/amazingxaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="The Amazing XAML Maze"
>
<u:Maze.Rooms>
<u:Room Name="room_1" Description="The first room" />
<u:Room Name="room_2" Description="The second room" />
</u:Maze.Rooms>
</u:Maze>
This mapping tells the XAML parser to load our XAML.Mazes
assembly. The XmlnsDefinition attributes then are used to identify all
classes from the XAML.Mazes and XAML.Mazes.Rooms assemblies, so now we
can use them in our XAML file.
Actually, we can now lose the u: prefixes in our XAML file:
<?xml version="1.0" encoding="utf-8" ?>
<?Mapping XmlNamespace="http://www.u2u.be/amazingxaml" ClrNamespace="XAML.Mazes" AssemblyName="XAML.Mazes"?>
<Maze
xmlns="http://www.u2u.be/amazingxaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="The Amazing XAML Maze"
>
<Maze.Rooms>
<Room Name="room_1" Description="The first room" />
<Room Name="room_2" Description="The second room" />
</Maze.Rooms>
</Maze>
Resources
Every maze has a couple of items you need to open doors, solve
puzzles, etc... So let’s add a couple of items to the maze.
Because these items move around, and might not even be available right
away, we need another place to store them. Instead of using a normal
collection of items, we will use the standard XAML resources for
storing the items. WPF used resources to supply its objects with an
easy way for finding commonly reused objects, such as styles and
data-objects. Resources are stored as a key-value collection using the
ResourceDictionary class, so let’s add this to our Maze class
as a public property:
private ResourceDictionary _resources
= new ResourceDictionary();
public ResourceDictionary Resources
{
get { return _resources; }
set { _resources = value; }
}
Of course we also need an item class, to create a new Item
class in the XAML.Mazes.Items namespace:
namespace XAML.Mazes.Items
{
public class Item
{
private string _description;
public string Description
{
get { return _description; }
set { _description = value; }
}
private double _weight;
public double Weight
{
get { return _weight; }
set { _weight = value; }
}
}
}
And we need to add an extra XmlnsDefinition attribute to the
beginning of the Item class:
[assembly: XmlnsDefinition("http://www.u2u.be/amazingxaml", "XAML.Mazes.Items")]
So now we are ready to add an item to our maze using XAML:
<Maze
xmlns="http://www.u2u.be/amazingxaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="The Amazing XAML Maze"
>
<Maze.Resources>
<Item x:Key="theKey" Description="A long, rusty key" Weight="55"/>
</Maze.Resources>
...
</Maze>
A couple of thing to note about this. First of all, resources
are stores as key-value pairs, with the value being the object. But
where do we get the key? This is what the
x:Key=”…” is used for. The x:Key is a
standard feature of XAML (hence the x: prefix) and is used to send the
key value to the XAML parser so it can add it to the ResourceDictionary
instance.
Type Converters
The Weight property is a double, but in XAML the value is a
string. How does XAML convert the string to a double? Well, it looks if
the target type (or target property) has a TypeConverter.
TypeConverters are a standard way for converting one type to another,
and the .NET runtime has supplied us with TypeConverters for standard
built-in types. That is why XAML can convert strings to integers,
doubles, etc…
We can use this system to our advantage. Some rooms have items
already in them, and we’re going to use a TypeConverter so we
can easily list the items in the room. Start by adding a new ItemNames
property to the Room class:
using System.ComponentModel;
using XAML.Mazes;
... private static string[] noItemNames = new string[] { };
private string[] _itemNames = null;
[TypeConverter ( typeof ( StringArrayConverter ) )]
public string[] ItemNames {
get {
if( _itemNames == null )
return noItemNames;
else
return _itemNames;
}
set { _itemNames = value; }
}
Please note that the ItemNames property is an array of
strings, and we’re going to use a type converter to convert a
comma-separated list of item-names into an array of strings.
We’re using the TypeConverterAttribute to specify the
TypeConverter XAML needs to call to do the conversion. Add a
StringArrayConverter class to your XAML.Mazes project:
using System.ComponentModel;
using System.Globalization;
namespace XAML.Mazes
{
public class StringArrayConverter : TypeConverter
{
public override bool CanConvertFrom(
ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
return true;
else
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(
ITypeDescriptorContext context, CultureInfo culture, object value)
{
return (value as string).Split(',');
}
}
}
(TypeConverters are nicely documented in the .NET
documentation, so we’re not going to discuss this.)
Now we can add some items to our rooms:
<Maze
xmlns="http://www.u2u.be/amazingxaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="The Amazing XAML Maze"
>
<Maze.Resources>
<Item x:Key="theKey" Description="A long, rusty key" Weight="55"/>
<Item x:Key="oldKnife" Description="An old, antique knife" Weight="65"/>
<Item x:Key="oldFork" Description="An old, antique fork" Weight="65"/>
</Maze.Resources>
<Maze.Rooms>
<Room Name="room_1" Description="The first room" ItemNames="theKey"/>
<Room Name="room_2" Description="The second room" ItemNames="oldKnife, oldFork"/>
</Maze.Rooms>
</Maze>
To actually use the items in the room we need to add an Items
property that will lookup the items using the maze’s
resources (add this code to the Room class):
using XAML.Mazes.Items;
using System.Diagnostics;
...
private Item[] _items = null;
public Item[] Items {
get {
if( _items == null ) {
_items = LookupItemNames ( );
}
return _items;
}
}
private Item[] LookupItemNames( ) {
List<Item> items = new List<Item>( );
foreach( string itemName in ItemNames ) {
Item it = this.Maze.Resources[itemName.Trim()] as Item;
if( it != null )
items.Add ( it );
else
Debug.WriteLine ( "Item with name {0} was not found", itemName );
}
return items.ToArray ( );
}
This code does require access to the room’s maze, so
let’s add support for that (add code to the Maze class):
static Maze _current;
public static Maze Current { get { return _current; } }
public Maze( ) {
_current = this;
}
Markup Extensions
Now let’s add exits to our rooms, start by adding a
new Exit class to the XAML.Mazes project:
using XAML.Mazes;
using XAML.Mazes.Rooms;
using System.Windows.Markup;
[assembly: XmlnsDefinition("http://www.u2u.be/amazingxaml", "XAML.Mazes.Exits")]
namespace XAML.Mazes.Exits
{
public enum Direction
{
North,
East,
West,
South
};
public class Exit
{
private Direction _direction;
public Direction Direction
{
get { return _direction; }
set { _direction = value; }
}
private Room _room;
public Room Room
{
get { return _room; }
set { _room = value; }
}
}
}
And of course an ExitCollection class:
public class ExitCollection : List<Exit>{
}
Now let’s add an Exits property to the Room class:
using XAML.Mazes.Exits;
private ExitCollection _exits = new ExitCollection();
public ExitCollection Exits
{
get { return _exits; }
}
And now add the Exits to the XAML file:
<Room Name="room_1" Description="The first room" ItemNames="theKey">
<Room.Exits>
<Exit Direction="North" Room="???" />
<Exit Direction="East" Room="???" />
</Room.Exits>
</Room>
Specifying the direction is easy, XAML will automatically
convert the string to our enumeration, however to specify the room we
need something special. We cannot use a room element because this might
result in an endless XML loop; look at room1, the north exits actually
ends up in the same room!
We are going to use a Markup Extension, this is a special
class that will get called by XAML during parsing, asking the extension
to return the value by calling its ProvideValue method. Using a markup
extension is easy in XAML, simple specify the name of the extension
between curly braces ({ <Extension> …}. WPF
uses this for example to lookup a resource using the {x:Resource}
syntax. We’re going to build our own extension, so
let’s start by adding a new GoTo class to our XAML.Mazes
project (who would have thought you would ever use a GoTo in XAML ? ) :
using System.Windows.Markup;
using XAML.Mazes.Rooms;
...
[MarkupExtensionReturnType(typeof(Room))]
public class GoTo : MarkupExtension
{
internal class DelayRoomLookup : Room
{
public DelayRoomLookup(string name) :
base( name ) { }
}
private string _roomName;
private string RoomName { get { return _roomName; } }
public GoTo(string roomName)
{
_roomName = roomName;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
Room otherRoom = Maze.Current[RoomName];
if (otherRoom != null)
return otherRoom;
else
return new DelayRoomLookup(RoomName);
}
}
All markup extensions need to derive from the MarkupExtension
base class, and override the ProvideValue method. Initializing a markup
extension is normally done with the constructor, which can take a
string argument. We are also being friendly to XAML by explaining that
our extension returns a Room object from the ProvideValue method using
the MarkupExtensionReturnTypeAttribute. The ProvideValue method returns
the room to go to by doing a lookup by room name.
To complete this extension we need a read-only indexer on the
Maze class for retrieving rooms by name:
public Room this[string roomName]
{
get
{
return Rooms.Find(delegate(Room room)
{
return room.Name == roomName;
});
}
}
And of course we have to add a little support for the
DelayRoomLookup class in the Exit class, so change the Exit’s
Room property:
public Room Room
{
get {
if (_room is GoTo.DelayRoomLookup)
{
GoTo.DelayRoomLookup lookup = _room as GoTo.DelayRoomLookup;
_room = Maze.Current[lookup.Name];
}
return _room;
}
set { _room = value; }
}
And to complete this functionality add a GoTo method in the
Room class:
public Room GoTo(Direction direction)
{
Exit exit = Exits.Find(delegate(Exit e) { return (e.Direction == direction); });
if (exit != null)
return exit.Room;
else
return this;
}
You can actually use more than one string to initialize the
GoTo extension. Let’s say we want some of our exits to be
available only when the player carries an item:
public GoTo(string roomName, string itemName)
{
RoomName = roomName;
RequiredItemName = itemName;
}
You use it in the XAML file as follows:
<Exit Direction="North" Room="{GoTo room_1, theKey}" />
Or you use named properties:
private string _roomName;
public string RoomName
{
get { return _roomName; }
protected set { _roomName = value; }
}
private string _requiredItedName;
public string RequiredItemName
{
get { return _requiredItedName; }
protected set { _requiredItedName = value; }
}
Which you then set in the XAML file:
<Exit Direction="North" Room="{GoTo RoomName=room_1, RequiredItemName=theKey}" />
Using ContentPropertyAttribute
Some of our items have very long descriptions, and in this
case the XAML element for an item becomes very long and difficult to
read. Can we solve this? Yes, by applying the ContentPropertyAttribute
to our class we can use the inner text of the element to use as our
description:
[ContentProperty("Description")]
public class Item
{ ... }
Our XAML item now looks like this (add it to the item
collection):
<Item x:Key="mustyBook" Weight="100">
An old musty book called -- Programming Windows 3.1 --
</Item>
We can actually use this same technique to get rid of the
<Maze.Rooms>property-element!
[ContentProperty("Rooms")]
public class Maze
{ ... }
Remove the <Maze.Rooms>and
</Maze.Rooms>tags from the XAML file:
<Maze
xmlns="http://www.u2u.be/amazingxaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="The Amazing XAML Maze"
>
<Maze.Resources>
...
</Maze.Resources>
<Room Name="room_1" Description="The first room" ItemNames="theKey">
<Room.Exits>
<Exit Direction="North" Room="{GoTo RoomName=room_1, RequiredItemName=theKey}" />
<Exit Direction="East" Room="{GoTo room_2}" />
</Room.Exits>
</Room>
<Room Name="room_2" Description="The second room" ItemNames="oldKnife, oldFork"/>
</Maze>
You might want to try the same for the Room.Exits!
Attached Properties
This leaves one more thing we need to discuss about XAML. In
WPF there are all kinds of containers for your controls, for example
there is a Canvas control that allows you to position child controls
anywhere you want. There is also a Grid control that will do the layout
for you. The problem is each control needs to specify where it wants to
be put in the containing control. Of course we don’t want to
have to add properties to our controls for each possible container.
XAML solves this using attached properties. These allow a control to
tell any containing control where it wants to position itself. For
example:
<Window x:Class="AttachedPropertySample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="AttachedPropertySample" Height="300" Width="300"
>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Grid.Row="1" Grid.Column="1">Click me!</Button>
</Grid>
</Window>
This example will place the button in the lower right corner
of a 2x2 grid. It’s actually the Grid class that will
remember the button’s position, making the button class
independent of the Grid class.
We’ll use an attached property to set the
player’s start room, so let’s add an new player
class to the XAML.Mazes project:
public class Player
{
static Room _startRoom;
}
And of course give the Maze a player property:
private Player _player;
public Player Player
{
get { return _player; }
set { _player = value; }
}
Set one of the rooms as the start using the attached property
syntax:
<Room Player.Start="true" Name="room_1" ...
Attached properties are implemented by adding a static
Get<PROPERTY> and Set<Property> method:
public class Player
{
public static void SetStart(Room room, bool isStart)
{
if (room == null)
throw new ArgumentException("Invalid room");
_startRoom = room;
}
public static bool GetStart(Room room)
{
return _startRoom;
}
...
}
The Set method takes two arguments, the element where the
attached property is being used (our Room class) and the value of the
property (a Boolean in our case). Our implementation simply remembers
the room passed as the starting room (ignoring the Boolean argument).
Room for Improvement
We can make our XAML solution even better, by getting rid of
the Mapping processing instruction. Remove the <? Mapping
…> processing instruction from the XAML file, and add
the following code to the LoadMaze method in the console application:
static public Maze LoadMaze( string fromFile ) {
FileStream xamlFile
= new FileStream ( fromFile, FileMode.Open, FileAccess.Read );
ParserContext parserContext = new ParserContext();
XamlTypeMapper mapper = new XamlTypeMapper(new string[] { "XAML.Mazes" });
parserContext.XamlTypeMapper=mapper;
Maze theMaze =
(Maze)XamlReader.Load(xamlFile, parserContext);
return theMaze;
}
The XamlTypeMapper instructs the XamlReader to load the
XAML.Mazes assembly and process the XmlnsDefinitions, which identifies
our Maze classes.
XAML Cookbook
Just as an easy reference, let’s list a couple of
common things to do for XAML:
Q: You want to use a couple of classes in a XAML file, and all
of them are in the same CLR namespace?
A: Add a XML namespace declaration to the element looking like
this (UPPERCASE are place-holders):
xmlns:PREFIX="clr-namespace:NAMESPACE.CLASS;assembly=ASSEMBLYNAME"
Now you can use your objects using the
<prefix:class>xml element syntax.
Q: You need to use classes from multiple CRL namespaces in
your XAML file?
A: Use the XmlnsDefinition attribute to map your namespaces to
the same xml namespace, and then use the
<?Mapping XmlNamespace="…"
ClrNamespace="…" AssemblyName="…"?> XML
processing instruction. Or use the XamlTypeMapper.
Q: You want to have a shorter syntax for creating objects?
A: Use a custom TypeConverter, which creates your object by
parsing some string.
Q: You want to add multiple objects to your class, and have an
easy syntax?
A: Use the ContentPropertyAttribute. But beware, this will
only work for one of your properties, the others need to use the
Property-Element syntax.
Q: Objects need a way to talk to other objects which should be
independent of your class?
A: Use the Attached Property syntax. This way any object can
talk to you, but you have to handle them.
Q: You need an easy way to specify a graph of objects, and
build tools for easy editing?
A: Use XAML!
Peter Himschoot is an architect and trainer for U2U, specializing in .NET, Visual
Studio Team System, BizTalk Server and .NET 3.0 (aka WinFx).
He is also a Microsoft Regional
Director.
You can reach him at peter@u2u.net.
Or read his blog http://blog.u2u.info/DottextWeb/peter/
.
Favorite T-shirt: >> Code is poetry! <<
U2U Training and Consultancy Services is a Microsoft .NET competence center located
in Belgium, to learn more please visit www.u2u.be
.