XAML as DSL

May 5, 2013

one comment

About 3 years ago, when .NET 4 and Visual Studio 2010 were just released, I’ve blogged (part 1, part 2) about the changes that took XAML from its WPF inception to the System.Xaml namespace and System.Xaml.Dll assembly, to be available more generally, not just for WPF. I’ve shown that XAML is just a declarative way of creating objects, setting their properties – sometimes in interesting ways. I did promise at the end of that second post that I’d show how to use attached properties, but never did have the time to deliver. 3 years later, it’s high time I make good on that promise.

The idea of a Domain Specific Language (DSL) is not new. Probably the most well known DSL is SQL – a language for working with databases. A DSL is typically constrained to a specific domain or a specific task (hence the name). A DSL may be a completely new language (such as SQL), and this is termed “external DSL”, and it can be constructed using a specific language semantics, such as C#, by leveraging certain language features that make up the “new” language. in C#, extension methods and “fluent interfaces” are most often used for that task.

What about XAML? The declarative nature of XAML makes it a good candidate to be a DSL. It has simple and strict rules, but it allows extensions, with type converters, markup extensions and attached properties.

An example

Suppose we want to create a text adventure game (“interactive fiction”) language (yes, I know I have a fetish for text adventures…). We could use XAML to describe not just the world that comprises the game, but also the actions that need to take place in certain scenarios.

The first step is to create an object model suitable to describe a text adventure. Here are a few simple classes to get us started:

[Serializable]

[DictionaryKeyProperty("Name")]

[DebuggerDisplay("{Name}")]

public abstract class Entity {

    Dictionary<string, object> _properties;

 

    public string Name { get; set; }

    public IDictionary<string, object> Properties {

        get {

            return _properties ?? (_properties = new Dictionary<string, object>(4, StringComparer.CurrentCultureIgnoreCase));

        }

    }

}

public enum RelativeLocation {

    In, On, Under, Behind

}

 

[Serializable]

[DebuggerDisplay("{Name} ({GetType().Name}) {RelativeLocation} {Location}")]

public abstract class Thing : Entity {

    public Thing() {

        Article = "a";

        Count = 1;

    }

 

    public string Location { get; set; }

    public RelativeLocation RelativeLocation { get; set; }

 

    [TypeConverter(typeof(StringListConverter))]

    public string[] Nouns { get; set; }

 

    [TypeConverter(typeof(StringListConverter))]

    public string[] Adjectives { get; set; }

 

    public string Article { get; set; }

    public bool IsTransparent { get; set; }

    public int Weight { get; set; }

    public int Size { get; set; }

    public string ShortDesc { get; set; }

    public string LongDesc { get; set; }

}

[Serializable]

public class Item : Thing {

    public Item() {

        CanTake = true;

    }

 

    public bool IsOpen { get; set; }

    public bool CanOpen { get; set; }

    public bool IsContainer { get; set; }

    public bool IsLocked { get; set; }

    public string Key { get; set; }

    public bool CanLock { get; set; }

    public bool CanTake { get; set; }

}

A few interesting things here:

  • 1. The DictionaryKeyProperty attribute indicates which property should be considered a key, should such an object be placed in a dictionary. This will allow us to avoid using the more awkward and generic x:Key XAML attribute.
  • 2. The TypeConverter attribute indicates how a string is to be converted to a more complex type, such as the Nouns property:
  • [TypeConverter(typeof(StringListConverter))]

    public string[] Nouns { get; set; }

This particular converter turns a string into an array of strings. Here’s the implementation:

public class StringListConverter : TypeConverter {

    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) {

        var target = context as IProvideValueTarget;

        Type type = null;

        if(target != null) {

            type = (target.TargetProperty as PropertyInfo).PropertyType;

        }

        string svalue = (string)value;

        var words = svalue.Split(new [] {','}, StringSplitOptions.RemoveEmptyEntries);

        return type == typeof(string[]) ? (object)words : words.ToList();

    }

 

    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) {

        return sourceType == typeof(string);

    }

}

The code can actually convert to an array of strings or a list of strings (it looks for a comma as a separator to split the words). Converters is one of those things that make XAML friendly, readable and easy to use.

Let’s take a look at something more interesting: markup extensions. Suppose we want to be able to place items in rooms or other containers. For example, a ring may be in the bedroom, or on the table, or in a box. We want to be able to “say” that as easily as possible (we are striving for a DSL). Here’s some XAML that accomplishes this:

<Item Name="ring" Location="{In box}" 

        Nouns="ring" Adjectives="small,diamond" Article="a">

</Item>

<Item Name="box" Location="kitchen" Nouns="box" IsOpen="true">

 

</Item>

<Room Name="kitchen" Attributes="Cold,Dark" ShortDesc="A small kitchen">

</Room>

Note the {In box} location of the ring. To make that work, there must be a class named In or InExtension that accepts a string as a constructor argument. In fact, we can create a bunch of those like so:

public abstract class RelativeLocationExtension : MarkupExtension {

    public RelativeLocation RelativeLocation { get; private set; }

    public string Name { get; private set; }

 

    protected RelativeLocationExtension(RelativeLocation rl, string name) {

        RelativeLocation = rl;

        Name = name;

    }

 

    public override object ProvideValue(IServiceProvider sp) {

        var target = sp.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;

        if(target != null) {

            if(target.TargetObject.GetType().IsSubclassOf(typeof(Thing))) {

                target.TargetObject.GetType().GetProperty("RelativeLocation").SetValue(target.TargetObject, RelativeLocation);

            }

            else

                throw new ArgumentException("object must be a thing for a location");

        }

        return Name;

    }

}

 

public class OnExtension : RelativeLocationExtension {

    public OnExtension(string name) : base(RelativeLocation.On, name) {

    }

}

 

public class InExtension : RelativeLocationExtension {

    public InExtension(string name)

        : base(RelativeLocation.In, name) {

    }

}

 

public class UnderExtension : RelativeLocationExtension {

    public UnderExtension(string name) : base(RelativeLocation.Under, name) {

    }

}

Any markup extension must implement the ProvideValue method. The provided IServiceProvider can be used to get the context in which the markup extension is used. The IProvideValueTarget interface is the most common and useful to use. It lets the code know on which object and which property is set with the markup extension. Markup extensions provide a powerful extensibility mechanism that’s still easy to use by our DSL.

Attached properties

WPF invented attached properties, now used in other XAML based technologies. Attached properties are defined by one type but can be set on any object. They are “attached” to objects, providing the idea of context properties. By the way, they don’t have to be dependency properties (as in WPF/Silverlight) in any way. Their implementation is irrelevant to the XAML parser.

In fact, to parse attached properties, the XAML parser requires the defining class to have a static GetXxx and/or SetXxx for the attached property Xxx. In the implementation, we need to store/retrieve the value of the property from some attached property store – conceptually similar to the way WPF manages an attached property – by using some global static variable.

Technically, that store needs to implement the IAttachedPropertyStore interface. Although it’s not difficult to implement (most use a dictionary), .NET provides a default implementation accessible with the convenient AttachablePropertyServices static class. Here’s an example of creating an attached property named Commands.Script (in this case read only):

public static class Commands {

    public static IList<ScriptCommand> GetScript(Entity entity) {

        IList<ScriptCommand> value;

        AttachablePropertyServices.TryGetProperty<IList<ScriptCommand>>(entity, new AttachableMemberIdentifier(

            typeof(Commands), "Script"), out value);

        if(value == null) {

            value = new List<ScriptCommand>();

            AttachablePropertyServices.SetProperty(entity, new AttachableMemberIdentifier(

                typeof(Commands), "Script"), value);

        }

        return value;

    }

}

Now we can use the property like so (for a Room in this example):

<Room Name="kitchen" Attributes="Cold,Dark" ShortDesc="A small kitchen">

  <Commands.Script>

    <ScriptCommand Verb="smell">

      <Print Text="You smell hot peppers eveywhere." />

    </ScriptCommand>

    <ScriptCommand Verb="listen">

      <Print>You hear nothing special.</Print>

    </ScriptCommand>

  </Commands.Script>

</Room>

ScriptCommand and Print are some other classes that are part of the object model for the game.

From XAML to Objects

Here’s some part of a complete XAML we may wish to parse:

<GameFile xmlns="http://xamlventure/2013" >

  <GameFile.Resources>

    <Variable Name="light carried" Value="True" />

    <CanMoveToCondition Direction="North" Name="canMoveNorth" />

  </GameFile.Resources>

    <Item Name="ring" Location="{In box}" 

            Nouns="ring" Adjectives="small,diamond" Article="a">

    <Commands.Script>

      <ScriptCommand Verb="take" DirectObject="ring">

        <Print Text="Your hand is stopped by an invisible shield around the ring." 

            Condition="{Property Glowing, Object=ring, Value=True}" Continue="false"/>

        <Print>The ring shines momentarily as you grab it in your hand.</Print>

        <PausePrint />

      </ScriptCommand>

    </Commands.Script>

    </Item>

  <Item Name="box" Location="kitchen" Nouns="box" IsOpen="true">

    

  </Item>

  <Room Name="kitchen" Attributes="Cold,Dark" ShortDesc="A small kitchen">

    <Commands.Script>

      <ScriptCommand Verb="smell">

        <Print Text="You smell hot peppers eveywhere." />

      </ScriptCommand>

      <ScriptCommand Verb="listen">

        <Print>You hear nothing special.</Print>

      </ScriptCommand>

    </Commands.Script>

  </Room>

The XAML uses other classes not described above, with some other markup extensions, such as {Property} and {Condition}.

Note the XML namespace at the top of the file. This defines the relationship between XAML types and .NET types. The .NET types may be spread across multiple .NET namespaces, but the XML namespace maps to all of those. This is done using the following attributes:

[assembly: XmlnsDefinition("http://xamlventure/2013", "XamlVenture.ObjectModel")]

[assembly: XmlnsDefinition("http://xamlventure/2013", "XamlVenture.ObjectModel.MarkupExtensions")]

[assembly: XmlnsDefinition("http://xamlventure/2013", "XamlVenture.ObjectModel.Actions")]

[assembly: XmlnsDefinition("http://xamlventure/2013", "XamlVenture.ObjectModel.Conditions")]

This is easier than the alternative XAML namespace mapping (this is the way WPF/Silverlight works to get the correct mappings).

The next step is to turn the XAML into the GameFile root object with everything inside. The simplest way to do this is with the XamlServices class:

var root = (GameFile)XamlServices.Load(path);

This returns the root GameFile object and everything else inside it. GameFile is defined like so (partial):

[Serializable]

[ContentProperty("Entities")]

public sealed class GameFile {

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]

    Dictionary<string, Entity> _entities = new Dictionary<string, Entity>(32, StringComparer.CurrentCultureIgnoreCase);

 

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]

    Dictionary<string, Verb> _verbs = new Dictionary<string, Verb>(8, StringComparer.CurrentCultureIgnoreCase);

 

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]

    Dictionary<string, Resource> _resources;

 

    public IDictionary<string, Entity> Entities {

        get { return _entities; }

    }

 

    public IDictionary<string, Verb> Verbs {

        get { return _verbs; }

    }

 

    public IDictionary<string, Resource> Resources {

        get {

             if(_resources == null)

                _resources = new Dictionary<string, Resource>(4, StringComparer.InvariantCultureIgnoreCase);

            return _resources; 

        }

    }

 

    public Game Game { get; set; }

}

The ContentProperty attribute is another common helper that sets the “default” property for the particular type.

If more control is required during parsing, such as running some extra initialization after or during certain object creation, a more elaborate parsing loop can be created:

using(var reader = new XamlXmlReader(path)) {

    using(var writer = new XamlObjectWriter(new XamlSchemaContext())) {

        while(reader.Read()) {

            switch(reader.NodeType) {

                case XamlNodeType.StartObject:

                    writer.WriteNode(reader);

                    break;

 

                case XamlNodeType.StartMember:

                    goto default;

 

                case XamlNodeType.EndObject:

                    writer.WriteNode(reader);

                    AnalyzeObject(writer.Result);

                    break;

 

                default:

                    writer.WriteNode(reader);

                    break;

            }

        }

    }

}

After each XAML token is read, the code can do something with it. The most useful node type is EndObject, indicating a complete object has been written. This is a perfect opportunity to do something with that object, such as create extra objects based on its state, set some other properties or anything else that’s required. (the AnalyzeObject dummy method above indicates such operations).

After the parsing is complete, the resulting object model can be transformed to another form or another object model if needed; or it can just be used as is in the target application.

Conclusion

XAML is an effective DSL; being XAML – and XML – makes it easy to work with – as a developer and as a user, as there are many ways to simplify XML input.

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

one comment

  1. Pingback: Xaml Dsl | Quick Speed Test - Info