Click here to Skip to main content
Click here to Skip to main content

And Now for XAML Completely Different

, 1 Apr 2010
Rate this:
Please Sign up or sign in to vote.
Using Markup Extensions to build individual markup based declarative systems with XAML

Introduction

This article shows the power of XAML and how to use it beyond the well known W*F implementations.

Many people meanwhile are familiar with Microsoft’s W*F implementations, being WPF, WCF or WF, but not all may have realized the power behind XAML and how to use it for completely different things, so this article shows some of the techniques used behind the scenes and how to use them. Even if this special sample seems useless to you, it still can help you design your own implementation.

Using XAML instead of just XML to describe your objects, you get an editor with syntax highlighting, code completion and intellisense as well as the loading, parsing and object instantiation engine – all without writing a single line of code.

This sample shows a simple declarative scripting language based on Markup Extensions. It seems to be a totally redundant YASL (YetAnotherScriptingLanguage), but its lightweight implementation and ease of use has its own charm. It can be a base for many things that need scripting or a scripting like configuration, for example integration tests, where you need statements to describe the user action and statements for the verification, and since you can easily limit the command set to what is really needed to do the job, the learning curve to write scripts is much better than with a more powerful language with thousands of functions irrelevant to the job.

Screen1

Since this article mainly focuses on the usage of XAML and markup extensions, I’ll only describe this part; if you want to dig into more details, just have a look into the source code of the project.

The Idea

A program is an expression tree, so that’s a perfect candidate for XML. A very simple program would look like this:

xaml1

As you may have noticed, the namespace declared in the Main node is just our projects namespace – no W*F here. We can create only objects from our own assembly this way, but that’s OK since we don’t need anything else for now.

Assuming that the MarkupScript namespace contains classes ‘Main’, ‘Echo’, ‘If’, etc., we now can call this from C#:

Main main = (Main)XamlReader.Load(fileStream);
foreach (var statement in main.Statements)
{ 
    statement.Evaluate(); 
}

All the objects declared in the XAML will be instantiated by this simple call, and our expression tree is ready to be executed. The call stack is just represented by the level in the object tree, and execution is just walking through the expression tree.

Using All Benefits of Markup

To use all the benefits you can get from XAML, there are three classes we should have a special eye on:

  1. MakrupExtension
  2. TypeConverter
  3. ContentPropertyAttribute

MarkupExtension

The objects could be implemented using simple classes ‘Main’, ‘Echo’, ‘If’, etc. You would be able to write something like the above in XAML and load it with the XAML reader. There is just one basic thing missing: Objects may need to know each other. While it’s easy for nodes to know their children, the opposite may turn into writing extra code for every property or collection to initialize it with its parent object. Here is where the MarkupExtension class comes in handy, so every object in our tree is derived from it:

public abstract class Node : MarkupExtension, INode
{
  ...

  #region MarkupExtension overrides

  public override object ProvideValue(IServiceProvider serviceProvider)
  {
    // If the target object is an INode, that's our parent:
    if (serviceProvider != null)
    {
      if (Parent == null)
      {
        IProvideValueTarget provideValueTarget = 
	(IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
        Parent = provideValueTarget.TargetObject as INode;
      }
    }
    // Inplace implementation, just return ourselves.
    return this;
  }

  #endregion
}

When the XML loader assigns a MarkupExtension object to a property, it calls MarkupExtension.ProvideValue so the extension can provide the proper object. We don’t need to provide an extra object, so we simply can return the extension object itself, but we want to remember the target object we’ve got assigned to, since that’s our parent.

TypeConverter

Now since we want to build an expression tree, most of the properties expect an expression object to be assigned to, so all these properties are of type IExpression. Assignment is straightforward if we explicitly assign some other object that implements IExpression; however we may also want to simply assign constant values by just typing the value and not constructing a constant expression around it every time. This is exactly what TypeConverters are for:

/// <summary>
/// Simple type converter to allow setting constant expressions 
/// by simply typing the value in XAML.
/// </summary>
public class ExpressionTypeConverter : TypeConverter
{
  public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
  {
    return sourceType == typeof(string);
  }

  public override object ConvertFrom
	(ITypeDescriptorContext context, CultureInfo culture, object value)
  {
    string stringValue = (string)value;

    bool booleanResult;
    if (bool.TryParse(stringValue, out booleanResult))
    {
      return new ConstantExpression(booleanResult);
    }

    double doubleResult;
    if (double.TryParse(stringValue, NumberStyles.Any, 
	CultureInfo.InvariantCulture, out doubleResult))
    {
      return new ConstantExpression(doubleResult);
    }

    return new ConstantExpression(value);
  }
}

This type converter will take any string value and wrap it with a ConstExpression, so it can be assigned everywhere an IExpression is expected. Now you may ask how the XAML reader knows about the existence of this converter and where to apply it. The answer is the TypeConverterAttribute, that we just assign to the IExpression interface:

[TypeConverter(typeof(ExpressionTypeConverter))]
public interface IExpression : INode
{
  ...

This way anything that’s not an object derived from IExpression will be passed through the ExpressionTypeConverter when the target property is of type IExpression.

ContentProperty

Another markup specific attribute used here is the ContentPropertyAttribute. As you may have noticed, e.g. the Main object directly contains several statements, which in fact are markup extensions, and markup extensions are expected to be assigned to properties. If we look at the code of the Main class, we’ll see how this is done:

[ContentProperty("Statements")]
public class Main : Library
{
  ...
  public NodeCollection<istatement /> Statements { get; private set; }

The ContentPropertyAttribute tells the XAML reader to just add all content of the Main node to its Statements property. Since there can be multiple statements, this property needs to be a collection of objects. Since we use a typed collection here, the XAML editor’s intellisense will only show valid statements:

XAML2

Using It

Create a New Script

  • Create a new text file with the extension “.xaml”. You could use any extension, but if you want to make use of VisualStudio's intellisense for XAML files, you should stay with the default so you don’t confuse VS.
  • Add this XAML file to a VS project. This is required since the VS editor needs project references to resolve the namespaces - so make sure your project has a reference to the MarkupScript project or executable. If you are just playing around, you can alternatively add the new XAML file directly to the MarkupScript project as I did with the test files.
  • In the files properties window, set “Build Action” to “None” – otherwise you will get build errors. Optionally set “Copy to Output Directory” to “Copy always” to have your scripts near the executable.
  • Initialize the new file with the following content:

    XAML3

Now you are ready to start coding your script. See the provided samples for reference, the comments in there should provide a good overview of what you can do and how to use it.

Run a Script

After you have compiled the MarkupScript.exe, just call it with the scripts name as the first argument. Additional arguments may be provided for use by the script. If the script defines some arguments, you can get a simple help screen by calling “MarkupScript <ScriptFileName> /?

Samples

There are some sample scripts in the scripts subfolder of the MarkupScript project. Use these as a reference to the language; I’ve tried to use and comment as much as possible. The SimpleGame.xaml shows just the basics. Simply start it and see it running.

The TestRunner.xaml and TestClient.xaml show more advanced usage. They are my integration test reference. Only TestRunner is intended to be started directly while it internally starts TestClient with several arguments and checks its output.
If you’ve never run it before, you need to call it with the /recording switch so it gathers the output of the TestClient and stores it for comparison with subsequent runs.

Give it a try and start with this to see the integrated help screen:

MarkupScript scripts\TestRunner.xaml /? 

Then run:

MarkupScript scripts\TestRunner.xaml /Recording 

Now you have prepared the test environment and can start the test itself:

MarkupScript scripts\TestRunner.xaml 

History

  • V1.00 Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

tom-englert
Software Developer (Senior)
Germany Germany
No Biography provided

Comments and Discussions

 
GeneralNice job! PinmvpJosh Smith1-Apr-10 19:50 
GeneralCool idea! PinmemberAshley Davis1-Apr-10 4:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140721.1 | Last Updated 1 Apr 2010
Article Copyright 2010 by tom-englert
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid