XAML was originally developed for WPF (and WF) to allow a neutral, declarative way to build user interfaces. But in actuality, XAML has nothing to do with UI: it’s simply a language that allows creation of objects and setting of properties. The addition of type converters and markup extensions make this language extremely powerful for describing object hierarchies.
.NET 4 takes XAML to the next level, unbinding it from its WPF roots. A new assembly, System.Xaml.Dll hosts the generic usage of XAML. Most of the relevant types are in the System.Xaml namespace.
With this in mind, a XAML string can be turned into an object tree using this code:
namespace SimpleXaml {
public class Order {
public int OrderId { get; set; }
public double TotalPrice { get; set; }
public string ProductName { get; set; }
}
public class Customer {
public List<Order> Orders { get; private set; }
public string Name { get; set; }
public Customer() {
Orders = new List<Order>();
}
}
class Program {
static void Main(string[] args) {
string xaml = @"<Customer Name=""Bart"" xmlns=""clr-namespace:SimpleXaml;assembly=SimpleXaml"">
<Customer.Orders>
<Order OrderId=""1"" ProductName=""Computer"" TotalPrice=""1000"" />
<Order OrderId=""2"" ProductName=""Basket Ball"" TotalPrice=""50"" />
</Customer.Orders>
</Customer>";
Customer root = (Customer)XamlServices.Parse(xaml);
Console.WriteLine(root.Name);
Console.WriteLine("Orders: {0}", root.Orders.Count);
}
}
}
Note that we have to supply the default XML namespace to point to our .NET namespace for the types to be actually usable. We’ll get rid of that in a few moments.
Of course, files would be the usual XAML source. For this we can use one of the overloads of XamlServices.Load. This may be a straight file or a stream (and some other exotics we’ll soon see):
root = (Customer)XamlServices.Load("data.xml");
Console.WriteLine(root.Name);
Being it XAML, we can use a few tricks, such as the ContentProperty attribute. For example, we can make the “Orders'” property the content property of a Customer, so that the <Customer.Orders> element can be omitted:
[ContentProperty("Orders")]
public class Customer {
For this to compile, we need to add a using for System.Windows.Markup namesapce. Note that this is not from the WPF assemblies. it’s till from System.Xaml.Dll. There is no reference here to the WPF assemblies.
The XamlServices class is a convenient wrapper for parsing XAML that does all the hard work and hides all the details. If a more detailed traversing of the XAML tree is required, the lower level XamlReader (and mostly its derivatives) fits the bill.
Let’s return to the “clr-namespace” issue. Although this seems like a minor issue, it isn’t. One example where this gets uncomfortable is if I need to use multiple .NET namespaces within the same XAML. This forces me to create several xmlns declarations, of which only one can be the “default”. The markup will start to look cluttered. Furthermore, custom types (perhaps by some consumer libraries) will have to devise their own mappings. This is clearly different from the observed behaviour in WPF (and Silverlight), where the default XML namespace is mapped using some obscure URI and in actuality mapped to multiple .NET namespaces (for example, in WPF & Silverlight, mapped to System.Windows, System.Windows.Controls, System.Windows.Media and many more, all from a single XML namespace).
To get this behaviour, we must derive a class from XamlSchemaContext, which provides a “context” for XAML parsing. That base class is not abstract, but provides the mapping only for the “XAML” namespace (System.Windows.Markup) which is not nearly enough.
We must override a few key methods to map our XAML namespace (which can be any string, e.g. some fancy URI) to a set of .NET namespace(s). Here’s the basic idea:
class CustomerSchemaContext : XamlSchemaContext {
const string _namespace = "http://SomefancyName/Customers/2010";
public override IEnumerable<string> GetAllXamlNamespaces() {
yield return _namespace;
foreach(var ns in base.GetAllXamlNamespaces())
yield return ns;
}
private List<XamlType> _types;
public CustomerSchemaContext() {
_types = new List<XamlType> {
new XamlType(typeof(Customer), this),
new XamlType(typeof(Order), this)
};
}
public override ICollection<XamlType> GetAllXamlTypes(string xamlNamespace) {
if(xamlNamespace == _namespace)
return _types;
return base.GetAllXamlTypes(xamlNamespace);
}
protected override XamlType GetXamlType(string xamlNamespace, string name, params XamlType[] typeArguments) {
if(xamlNamespace == _namespace) {
var xtype = _types.Find(t => t.UnderlyingType.Name == name);
if(xtype != null)
return xtype;
}
return base.GetXamlType(xamlNamespace, name, typeArguments);
}
}
The only required method to override is the protected GetXamlType. The others are for convenience as far as the following code is concerned:
// use a custom schema
var ctx = new CustomerSchemaContext();
XamlXmlReader reader = new XamlXmlReader("data2.xml", ctx);
root = (Customer)XamlServices.Load(reader);
Console.WriteLine(root.Name);
The XAML looks like this:
<Customer xmlns="http://SomefancyName/Customers/2010" Name="Homer">
<Order OrderId="10" ProductName="Car" TotalPrice="20000" />
<Order OrderId="11" ProductName="Watch" TotalPrice="150" />
</Customer>
Note the xmlns definition.
This definitely opens up the possibilities with XAML, and is just the tip of the iceberg.
.