Freigeben über


Being written by XamlWriter

A big part of WPF (Avalon) is the Xaml format for creating object trees. You can also go in the other direction – take an object tree and write it out to Xaml – with the XamlWriter class.

It’s frequently easy for XamlWriter to convert an object into a good Xaml representation. Sometimes, though, it’s harder to get right, because you can’t always tell how to write an object to Xaml just by looking at its properties. So when you’re creating a new class, there’s a few things you can do to help out. And when you do that, not only are you making your class work better in XamlWriter, you’re making your class work better in designer tools, such as Expression Interactive Designer (Sparkle) and Visual Studio’s Cider, since they use the same information.

The things you can do to your class to help XamlWriter is that the rest of this article is about …

What is XamlWriter?

XamlWriter is a class that lets you create Xaml from objects. That's easiest to explain with an example. This example uses WPF (Avalon) types, but none of this is specific to WPF; you can use XamlWriter and XamlReader for any managed types, such as those created in C#, VB.Net, etc.

Say we have this code snippet:

Button button = new Button();

button.Width = 100.0;

button.Background = KnownColors.Red;

… now write it to Xaml like this:

xamlString = XamlWriter.Save( button );

Console.WriteLine( xamlString );

… and you get this:

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

Width="100"

Background="Red"/>

The long xmlns Uri is used to map the tags to the assemblies. It's useful, but a bit distracting here, so I'll leave it out from here forward. Thus it's simply:

<Button Width="100" Background="Red"/>

Pretty simple -- write the object's type as a tag, and its property as an attribute.

KnownColors.Red in this example is a simple SolidColorBrush, which is easy to represent as an attribute. But a LinearGradientBrush is more difficult – that's going to require some further XML. For example:

<Button Width="100">

<Button.Background>

<LinearGradientBrush>

<GradientStop …="" />

...

</LinearGradientBrush>

</Button.Background>

</Button>

In this case we used Xaml's property element syntax; the dot in the Button.Background tag indicates that the content of that tag is actually a value for the Background property, a LinearGradientBrush in this case.

So that shows XamlWriter in a nutshell – the Save method takes objects and turns them into Xaml. The basic algorithm is to write an object's type name as the tag, and then write the object's property values as either attributes (if they're simple properties) or nested markup under a property element tag.

By the way, if you've looked at Xaml much already, you probably already know that sometimes the property element tag gets left out. For example, for Grid, the Children property is its content property, So this

<Grid>

<Grid.Children>

<Button />

<Button />

</Grid.Children>

</Grid>

… can be simplified to this:

<Grid>

<Button />

<Button />

</Grid>

That's basic Xaml syntax (based on the [ContentProperty] attribute), but the point here is that XamlWriter understands it, and will write the simpler syntax when possible.

What to expect from XamlWriter, and what not to expect

The basic algorithm above is pretty simple, but there are things that can't be written, at least not without some help. This is mostly due to the fact that not all classes can be represented in Xaml. For example, Xaml can't (for the most part) represent types unless they have a default constructor, and so XamlWriter won't write such a type either. As another example, Xaml can't natively represent graphs of objects. For example, if two objects are being written and they reference a shared object, the shared object will be written twice.

Other limitations:

· Fields
Only properties are written, not fields.

· Read-only properties
That is, a property with a setter but not a getter. The motivation for this is that there is no point in writing the property if it's invalid Xaml, and read-only properties can be set from Xaml. This can be overridden, though, with the [DesignerSerializationVisibility] attribute described below.

· Internal/private properties
Only public properties are written

· Internal/private classes
Similarly, only public classes are written. Actually, an attempt to write a non-public class causes an exception to be thrown.

· Event listeners
For example if you have a listener attached to a Button.Click event, you won't get <Button Click="OnClick"/>, the Click handler will be dropped.

Finally, if you read Xaml with XamlReader, then write it with XamlWriter, you won't get the exact same XML. For example, no attempt is made to preserve the formatting of the original Xaml. As another example, some markup extensions, such as {x:Static}, are resolved at load-time by XamlReader and the markup extension itself is discarded, so there is no means for XamlWriter to re-produce it.

Which properties to write?

So an object gets written as a tag, and properties get written as either an attribute or a property element tag. For the most part, you don't do anything, and the right properties get written. But maybe not, in which case you can do some work in the class definition to take more control. There are several different options …

Not writing the default value

In general, if a property is its default value, it would be redundant to write it out. A type author (i.e. someone defining a class or struct) can let XamlWriter know the default value of a property using the [DefalutValue] attribute. For example, in the following, MyProperty won't get written out on a MyClass object if its value is zero:

class MyClass

{

[DefaultValue(0)]

public int MyProperty

{

get … set …

}

}

This rule is slightly different, though, if the property is backed by a DependencyProperty. In that case, the property is only written if it is actually set. For example, the following button is getting its Background property set not on the button itself, but from the button’s style:

<Window>

<Window.Resources>

<Style TargetType="Button">

<Setter Property="Background" Value="Red" />

</Style>

</Window.Resources>

<Button Name="button1">Click</Button>

</Window>

So in this example, if you call button1.Background from code, you'll get a red brush. But if you write it with XamlWriter, you'll only get:

<Button Name="button1">Click</Button>

Writing read-only properties

There's usually no point in writing to Xaml the value of a read-only property, because it won't be possible to load it back from the Xaml, since Xaml can't set read-only properties. There is one case, however, where Xaml can set to a read-only property, and that's with collection type properties. In that case, it’s not setting the property itself, but adding to the collection. For example, the Grid.Children property is read-only, but you can still add new items to the Children property with:

<Grid>

<Grid.Children>

<Button …/>

<Button …/>

</Grid.Children>

</Grid>

So read-only collection properties can usually be written OK, but XamlWriter doesn't write them by default. To override this, you can override this in the class definition with the [DesignerSerializationVisibility] attribute:

class Grid

{

[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]

public UIElementCollection Children

{

get { … }

}

...

}

Not writing read/write properties

The opposite situation is if you have a property that is read/write and looks perfectly fine to write to Xaml, but you don't want it to be anyway. For example, in the following class the DoorChanges property is a runtime counter that doesn’t make sense to persist. Here we use the “Hidden” value of the [DesignerSerializationVisibility] attribute to keep it from being written:

class Car

{

int _doorChanges = 0;

[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]

public int DoorChanges

{

get { return _doorChanges; }

set { _doorChanges = value; }

}

...

}

Procrastinate and decide at run-time

So the DesignerSerializationVisibility attribute is a method to declaratively determine if a property gets written or not by XamlWriter. You can also be called by XamlWriter during the Save call, and then make a run-time decision. To do this, you add a "ShouldSerializeProperty" method along with the property, where "Property" is the name of the property. For example:

class MyClass

{

public int Foo

{

get { return _foo; }

set { _foo = value; }

}

public bool ShouldSerializeFoo()

{

if( … )

return false;

else

return true;

}

...

}

There is one additional variation on this run-time check if a property is backed by a DependencyProperty. In that case, you can still use this ShouldSerializeProperty pattern, or you can override the DependencyObject.ShouldSerializeProperty virtual. For example:

override bool ShouldSerializeProperty( DependencyProperty dp )

{

if( dp == FooProperty )

{

if( … )

return false;

}

return true;

}

Writing a property as an attribute with a TypeConverter

The last section was about whether or not a property should be written to Xaml. Once it's been decided that it should, the next decision is how to write it -- as an attribute like the Width property in the following example, or as markup like the Background below it:

<Button Width="100">

Click

<Button.Background>

<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">

<LinearGradientBrush.GradientStops>

<GradientStop Color="Blue" Offset="0"/>

<GradientStop Color="Red" Offset="1"/>

</LinearGradientBrush.GradientStops>

</LinearGradientBrush>

</Button.Background>

</Button>

How does the Width integer get converted to the string "100"? The answer is type converters. A type converter is associated with a type, and is used to convert to and from other types. When Xaml is loaded, type converters are used to convert from a string, and when writing Xaml type converters are used to convert to a string.

The double type has a built-in type converter which was used above to create the string "100". Similarly string, float, int, and all the other basic types have built-in type converters. Enum also has a built-in type converter, where you can simply use the field name. This is all standard type converter functionality; nothing about this is new or different for Xaml.

But since Xaml recognizes type converters, it means that you can create your own type converter for your class:

[TypeConverter( typeof( FooConverter ))]

class Foo

{

...

}

… so that if another class had a property of that type:

class Bar

{

public Foo Foo

{

get ... set ...

}

}

… it could be referenced in Xaml as an attribute using your type converter:

<Bar Foo="20" />

Just don’t forget that while all you need to implement in your type converter is ConvertFrom and CanConvertFrom, you also need to implement ConvertTo and CanConvertTo to be used by XamlWriter.

Also, in addition to registering a type converter with a class, you can also register it on a property. If a type converter is specified both on a property and on the property’s type, the one on the property wins. So, in:

class Bar

{

[TypeConverter( typeof( SpecialFooConverter ))]

public Foo Foo

{

get … set ...

}

}

… the Foo class has a type converter, but on Bar, Bar.Foo overrides it with its own type converter.

Writing a property as an attribute with a ValueSerializer

XamlWriter also introduces one new way to write a property value as an attribute, called ValueSerializer, which is similar to a type converter. It’s easiest to explain its use by describing the motivating problem …

One of the most common cases in WPF where a property is written as an attribute is the Brush. That's because everyone expects it to be possible to set a button’s Background property to “Red” using a simple attribute, not the verbose <Button.Background><SolidColorBrush …/></Button.Background>.

But Brush in particular presents a different kind of problem, because the way you can write a Brush to Xaml is ambiguous. That’s because Brush is the base class for SolidColorBrush, which gives you simple colors, as well as more complicated brushes such as LinearGradientBrush, ImageBrush, VisualBrush, and others. If you put the type converter on Brush, and XamlWriter calls your CanConvertTo method to see if you can convert to string, the answer is a definite "maybe". That's because if the value is a SolidColorBrush, you can convert it to string (either a simple string like "Red" or if fall back on "##AARRGGBB" syntax), but other brushes such as LinearGradientBrush can't be converted to an attribute string. The problem is that type converter doesn't give you a way to make this decision; when CanConvertTo is called, you don't get a reference to the actual Brush object being written.

The solution to that with XamlWriter is the ValueSerializer. A value serializer is much like a type converter, in that it has ConvertTo/From, CanConvertTo/From methods. You also point to it with the [ValueSerializer] attribute just like you point to a type converter with a [TypeConverter] attribute. One difference in value serializer from type converter is that it only converts objects to and from strings, rather than converting to/from any type. The other key difference is that it gives you the object being serialized.

So, in WPF, the Brush class looks something like this:

[TypeConverter(typeof(BrushConverter))]

[ValueSerializer(typeof(BrushValueSerializer))]

public abstract class Brush : ...

{

...

}

And the BrushValueSerializer looks something like this:

class BrushSerializer : ValueSerializer

{

...

public override bool CanConvertToString(object value, IValueSerializerContext context)

{

if( value is SolidColorBrush )

return true;

else

return false;

}

...

}

Notice that Brush has both a type converter as well as a value serializer. That’s because XamlReader doesn’t recognize value serializers; it only uses type converters. XamlWriter, on the other hand, when it sees that both a value serializer and a type converter is available, chooses the value serializer.

That’s It

So in the end, the key points are that you can control which properties on your class will get written by XamlWriter (or by designers like Sparkle and Cider), and you can control if they’re written to Xaml as attributes or as property elements. The majority of the time, classes you create will work well with XamlWriter even if you don’t do anything. By far the most likely problem you’ll run into is that read-only collection properties don’t get written by default. So the thing you’re most likely to have to do is set the [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] attribute on the property.

Comments

  • Anonymous
    May 01, 2007
    There are multiple ways to clone objects, and multiple definitions of what “clone” should even mean.

  • Anonymous
    April 10, 2008
    I don&#39;t know who said it first on our team during construction of the first XAML based system...perhaps

  • Anonymous
    June 01, 2009
    Internal thread that may be of use to more people: Question From: Microsoft Developer Is there a way