On Wednesday I gave a talk on xaml usage outside of the UI. This post is basically a recap of that talk. There are three useful attributes in the System.Windows.Markup namespace that give you some control over how classes can be used in Xaml, and xaml tools like Expression Blend and Visual Studio. This has an impact mostly on human readability and legibility of the xaml, and can benefit anyone who reads and writes raw xaml markup. There are three main attributes in the System.Windows.Markup namespace: XmlnsDefinition, XmlnsPrefix and ContentProperty.
XmlnsDefinition
XmlnsDefinition is added to the AssemblyInfo.cs file in the following form.
[assembly: XmlnsDefinition("http://www.csmac.co.nz/default", "csmac.XamlCollectionParsing.Default")]
The first argument is the xml namespace you wish to associate with your .Net namespace, and the second is the .Net namepace. The first thing to note is that this can be used to associate a single xml namespace with multiple .Net namespaces. The opposite is also true the but the former is more useful, because this mapping is supported by Blend 4 and Visual Studio 2010. This means that you can designate and use your own namespace for your own class libraries in the same way that http://schemas.microsoft.com/winfx/2006/xaml/presentation is used to refer to the Silverlight or WPF Presentation framework classes such as Button and TextBlock.
Of course one thing to be careful that if you map multiple .Net namespaces to an xml namespace, is that each class name within this xml namespace is unique, to avoid ambiguity. If two classes in different .Net namespaces have the same class name, and are both used in the same xml namespace, Things may not work as expected, so try to avoid this. This feature is now used by the latest Silverlight 4 toolkit and sdk to give you a single namespace for each, rather than needing multiple clr-namespace declarations to use different parts of these libraries.
XmlnsPrefix
The second attribute, XmlnsPrefix, is used is conjunction with XmlnsDefinition, and again is supported by Blend 4 and VS 2010. This is the default preferred prefix to use for a namespace, and looks like this.
[assembly:XmlnsPrefix("http://www.csmac.co.nz/default", "default")]
The first argument is the xml namespace, as defined by an XmlnsDefinition, and the second is the preferred prefix. When you drag an item from the associated namespace from the toolbox onto the designer, the tool will use this prefix when declaring the namespace at the top of the file. xmlns:default="http://www.csmac.co.nz/default"
will be added to the root xml element, rather then using the default prefix my
, control
, or csmac_xamlCollectionParsing_default
as you might see happen when you do not use these attributes. Both XmlnsDefinition
and XmlnsPrefix
can greatly improve readability of your xaml, and XmlnsPrefix
is also used by the latest silverlight 4 toolkit and sdk, using the prefixes sdk and toolkit, good choices IMHO.
ContentProperty
The last of the three is the ContentProperty attribute. this is used at the class level, to mark at class with which property should be used as its Content property. In xaml, this is used for mapping the xml element(s) contained inside the class’ xml element to a property on that class. For example, if I have a class called SimpleObject, with a property called Value of type string, I can use the following attribute to mark up the class.
[ContentProperty("Value")]
public class SimpleObject...
Now when I have the following xaml (note the namespace declaration) It will be parsed to set the Value property to the string “Hello World!”.
<SimpleObject xmlns="http://www.csmac.co.nz/default">Hello World!</SimpleObject>
This is the basics of how the content property works, and you are able to add other objects inside if the type of the property matches the type of that object. For example, if XObject has is Content Property set to a property of type YObject, we can use this in our xaml:
<Object xmlns="http://www.csmac.co.nz/default">
<YObject>Hello World!</YObject>
</XObject>
And this can be extended out as deep as you are willing to make it.
Collections
Now If you would like to set the content property to one that is a Collection type, things work in much the same way, although the parse is clever enough to be able to allow you to not have to declare the actual collection object in xaml, and instead will add the child objects to your existing collection. Basically you can add you objects directly inside the parent one after the other and it will add each one it turn to the collection. I will note that this is done by creating the list item object, then calling the add method on the content property which is of type ICollection (which is basically all standard collection types in C#).
If we use our example from earlier, but change XObject’s content property to be an ObservableCollection of YObjects we can do this:
<XObject xmlns="http://www.csmac.co.nz/default">
<YObject>Hello World!</YObject>
<YObject>HelloAgain</YObject>
<YObject>Hi</YObject> </XObject>
And this will just work, and works for all inheritance and all situations where you would be able to add that object to the collection normally. If you want to use collections of strings or other primative types, it is probably best create a reference to the system assembly and use the string object as an element tag like <sys:string>This</sys:string>
. This performs the correct string object creation and you can individually define the list items. This works with double and float, char and all the others as expected, based on how the xaml loader works.
If an element does not have any attributes, and has some content which is plain text, the Loader will attempt to find a way to convert the content text the the element class type. This generally is through the TypeConverter mechanism of .Net. This is something to be aware of and is made use of later in this post.
Inline Strings
Inline strings in xaml are a special case, as previously mentioned. In the case where we do declare an attribute and have inline text, the content property is used as the target for the string. Where this type is not of type string, the TypeConverter paradigm is again used.
Now collections of objects work in a very similar fashion. If you have played with any of the presentation classes that accept Inlines, such as Textblock in Silverlight 4 or WPF, you will have noticed you can mix strings with Inline types and it manages to wrap a string into a Run(which is an Inline). Unfortunately this wrapping is not really an out of the box straight forward feature, as I learned the hard way. Fortunately, it uses a pretty standard way of doing this.
IList has a method called Add that takes an object, and attempts to add this object to the collection. If this fails we get an exception. The presentation collections have a an implementation of the IList Add method which in a roundabout way determines that this is a string and wraps it into a Run. The good news here is that we too can modify the IList.Add method to create a custom string to object wrapping. I have done this using the Observable Collection as a base.
public class SimpleObjectCollection : ObservableCollection<SimpleObject>, IList
{
int IList.Add(object value)
{
if (value is string)
{
SimpleObject c = new SimpleObject() { Text = value as string };
value = c;
}
if (value is SimpleObject)
{
Add((SimpleObject)value);
}
else
{
throw new ArgumentException("Invalid Type for adding to a Generic Collection of type SimpleObject");
}
return (this.Count - 1);
}
}
What makes this possible is that the generic collections use private implementations of the IList Methods. this means that the implemented methods are not accessible unless you cast your object into the interface type. This seems to allow the XamlReader to use our custom class’ IList.Add method instead of the ObservableCollections one. In my method I check if a string is being handed in and wrap it into a SimpleObject as required.
Demo
Below is an application that implements a simple object and complex object, where the complex object contains a collection of simple objects. You can view some xaml and load it in to see if it is parse-able or not. I have used three difference scenarios here. Scenario 1 is default, where i have only added the above mentioned three attributes, and my collection is an ObservableCollection<SimpleObject>
. Scenario 2 is Listed, where I have used my custom SimpleObjectCollection
to wrap strings into SimpleObjects. In Scenario 3, Working, I have used the SimpleObjectCollection
but added a type converter to my ComplexObject
. This converter converts strings into ComplexObjects containing a single SimpleObject
with that string. This is a special case for the scenario where you have no property attributes on your xml element, and the first item inside the element is a string. This, as mentioned earlier to string elements, will look for a converter to convert into the ComplexObject
, and adding this converter here makes this situation work the way I intend it to. This examples is seen in sample 5.
[silverlight: uploads/2010/11/csmac.XamlCollectionParsing.xap, 550, 250] (TODO: Fix this demo)
Notice that Sample 6 fails or all three. Since my collection is readonly, I haven’t found a way to make this work, but for my scenario, I won’t be using the class.property syntax.
The Source For this Demo : csmac.XamlCollectionParsing.zip