Types, Metatypes and Bears, Oh my!

***** UPDATE: Please see this post for how these features and functionality work in Beta2 *****

 

Polar Bear

image courtesy of flickr user chodhound

This post comes about after a little conversation on the forums where I was talking about using the xaml stack save and load objects.

Here’s what I said:

Bob  (it's a little late in Seattle, so I don't have a code editor handy, so there may be some minor errors below),
If you want to serialize any object graph to xaml, simply look at the XamlServices.Save() api that's in System.Xaml.  I'm sure there are a few SDK samples around that as well.  It takes in a file name to output to, a text writer or a stream, so you get to pick your poison for destination.  Similarly, if you want to get an object graph deserialized from Xaml, you can just use XamlServices.Load() again, with overloads for files, streams, text, xml, and xaml readers.
To see this API, just do something like

 Sequence s = new Sequence { Activities = { new Persist(), new Persist() } };
XamlServices.Save(fileNameVar, s);

If you want to read, basically do the reverse.
Save and Load are convinient helper functions to operate on the whole of the doc, there Xaml stack surfaces much more programmability and has a nice node stream style API that lets you plug in while nodes are being read and written.
Now, if you want to deserialize a Xaml file that contains an x:Class directive, you are going to need to do a bit more work (and it depends what you want to serialize to or from).  I'll try to blog about that in the next week or so.

Now, I want to take a little bit of time to explain the last part.

A convenient way to think about XAML is a way to write down objects, really, instances of objects.  That said I am not limited to writing down just instances, I can actually write down type definitions as well.  I do this using the x:Class attribute. 

Consider the following XAML snippet

 <Activity x:Class="foo" ...

This is roughly equivalent to the following C#

 public class foo : Activity
{
...

Now, “normally” what happens with XAML like this in .xaml files is that it is used in a VS project and it is set up so that there is a build task who has the job of generating a type from that XAML so that you can use that type subsequently in your application.  This works basically the same way for WPF as well as WF.

However, if you think about trying simply deserialize this, it’s a little confusing what this will actually deserialize to.  This is a type definition, so if you simply try to pass it to XamlServices.Load(), you will encounter an exception:

Error System.Xaml.XamlObjectWriterException: No matching constructor found on type System.Activities.Activity. You can use the Arguments or FactoryMethod direct ives to construct this type. ---> System.MissingMethodException: No default constructor found for type System.Activities.Activity. You can use the Arguments or FactoryMethod directives to construct this type.
at System.Xaml.Runtime.ClrObjectRuntime.DefaultCtorXamlActivator.EnsureConstructorDelegate(XamlType xamlType)
at System.Xaml.Runtime.ClrObjectRuntime.DefaultCtorXamlActivator.CreateInstance(XamlType xamlType)
at System.Xaml.Runtime.ClrObjectRuntime.CreateInstanceWithCtor(XamlType xamlType, Object[] args)
at System.Xaml.Runtime.ClrObjectRuntime.CreateInstance(XamlType xamlType, Object[] args)
--- End of inner exception stack trace ---

So, if we want to deserialize that, we need to ask the question first: “Why do we want to deserialize it?”

Deserialize to Execute

If we want to simply use the activity we’ve created and have it execute, we have a special type, DynamicActivity.  DynamicActivity lets you execute the activity, and rather than creating a type for it, will allow you to pass in the arguments in the typical Dictionary<string,object> way that we are used to. 

Imagine a xaml file, just sitting on disk somewhere that looks like this (a workflow that concats two strings): 

 <p:Activity mc:Ignorable=""
    x:Class="WorkflowConsoleApplication1.Sequence1" 
     xmlns="https://schemas.microsoft.com/netfx/2009/xaml/activities/design"
     xmlns:__Sequence1="clr-namespace:WorkflowConsoleApplication1;" 
     xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006" 
     xmlns:p="https://schemas.microsoft.com/netfx/2009/xaml/activities" 
     xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <x:Members>
    <x:Property Name="argument1" Type="p:InArgument(x:String)" />
    <x:Property Name="argument2" Type="p:InArgument(x:String)" />
  </x:Members>
  <p:Sequence>
    <p:WriteLine>[argument1 + " " + argument2]</p:WriteLine>
  </p:Sequence>
</p:Activity>

The code for this is the following:

 object o = WorkflowXamlServices.Load(File.OpenRead("Sequence1.xaml"));
Console.WriteLine("Success {0}", o.GetType());
DynamicActivity da = o as DynamicActivity;da.Properties.ToList().ForEach(ap => Console.WriteLine("argument: {0}", ap.Name));
WorkflowInvoker.Invoke(da, new Dictionary<string, object> { { "argument1", "foo" }, { "argument2", "bar" } });

Deserialize to “Design”

At design time, we have an even more interesting problem.  Our designer is an instance editor, and, as such, it must always edit an instance of “something”.  In our case, we actually do some work in our design time xaml reader and writer to deserialize into a metatype, an instance of a type whose sole purpose in life is to describe the activity.  In Beta1, this is called ActivitySchemaType.  ActivitySchemaType simply models the type structure of an activity, complete with properties, etc  If you want to construct an instance of an ActivitySchemaType in code, you can, and then you could use the DesignTimeXamlWriter in order to properly serialize out to an Activity x:class xaml file.  The following code works on an ActivitySchemaType in memory and then serializes it:

 XamlSchemaContext xsc = new XamlSchemaContext();
ActivitySchemaType ast = new ActivitySchemaType()
{
    Name = "foo",
    Members = 
     {
        new Property { Name="argument1", Type = xsc.GetXamlType(typeof(InArgument<string>)) }

     },
    Body = new Sequence { Activities = { new Persist(), new Persist() } }
};
StringBuilder sb = new StringBuilder();

DesignTimeXamlWriter dtxw = new DesignTimeXamlWriter(
    new StringWriter(sb),
    xsc, "foo", "bar.rock");

XamlServices.Save(dtxw, ast);
Console.WriteLine("you wrote:");
ConsoleColor old = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine(sb.ToString());
Console.ForegroundColor = old;

This is what the output looks like:

 <p:Activity mc:Ignorable=""
     x:Class="foo" 
     xmlns="https://schemas.microsoft.com/netfx/2009/xaml/activities/design" 
     xmlns:__foo="clr-namespace:bar.rock;" 
     xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006" 
     xmlns:p="https://schemas.microsoft.com/netfx/2009/xaml/activities" 
     xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <x:Members>
    <x:Property Name="argument1" Type="p:InArgument(x:String)" />
  </x:Members>
  <p:Sequence>
    <p:Persist />
    <p:Persist />
  </p:Sequence>
</p:Activity>

If you want to read in from this, you have to do a little bit of trickery with the designer as DesignTimeXamlReader is not a public type in beta1. 

 WorkflowDesigner wd = new WorkflowDesigner();
wd.Load("Sequence1.xaml");
object obj = wd.Context.Services.GetService<ModelService>().Root.GetCurrentValue();
Console.WriteLine("object read type: {0}", obj.GetType());
ActivitySchemaType schemaType = obj as ActivitySchemaType;
Console.WriteLine("schema type name: {0}", schemaType.Name);
schemaType.Members.ToList().ForEach(p => Console.WriteLine("argument: {0}, type: {1}", p.Name, p.Type));

That wraps up our tour of the ways to read (and write)  <Activity x:Class xaml

Here’s the full text of the little program I put together to execute this, also make sure to drop a sequence1.xaml into your execution directory if you want this to not throw :-)


 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Xaml;
 using System.Activities.Statements;
 using System.Activities;
 using System.IO;
 using System.Activities.Design.Xaml;
 using System.Windows.Markup;
 using System.Xml;
 using System.Activities.Design;
 using System.Activities.Design.Services;
  
 namespace ConsoleApplication2
 {
     class Program
     {
         [STAThread()]
         static void Main(string[] args)
         {
             bool doStuff = true;
             Guid g = Guid.NewGuid();
             while (doStuff)
             {
                 Console.WriteLine();
                 Console.WriteLine("What do you want to do?");
                 Console.WriteLine("    read [x]:class xaml with XamlServices");
                 Console.WriteLine("    read x:class xaml with W[o]rkflowXamlServices");
                 Console.WriteLine("    [w]rite an ActivitySchemaType");
                 Console.WriteLine("    r[e]ad an ActivitySchemaType");
                 Console.WriteLine("    [q]uit");
                 Console.WriteLine();
                 char c = Console.ReadKey(true).KeyChar;
                 switch (c)
                 {
                     case 'w':
                         XamlSchemaContext xsc = new XamlSchemaContext();
                         ActivitySchemaType ast = new ActivitySchemaType()
                         {
                             Name = "foo",
                             Members = 
                              {
                                 new Property { Name="argument1", Type = xsc.GetXamlType(typeof(InArgument<string>)) }
  
                              },
                             Body = new Sequence { Activities = { new Persist(), new Persist() } }
                         };
                         StringBuilder sb = new StringBuilder();
                         
                         DesignTimeXamlWriter dtxw = new DesignTimeXamlWriter(
                             new StringWriter(sb),
                             xsc, "foo", "bar.rock");
                         
                         XamlServices.Save(dtxw, ast);
                         Console.WriteLine("you wrote:");
                         ConsoleColor old = Console.ForegroundColor;
                         Console.ForegroundColor = ConsoleColor.DarkGray;
                         Console.WriteLine(sb.ToString());
                         Console.ForegroundColor = old;
                         break;
  
                     case 'x':
                         try
                         {
                             object o = XamlServices.Load(File.OpenRead("Sequence1.xaml"));
                             Console.WriteLine("Success{0}", o.GetType());
                         }
                         catch (Exception ex)
                         {
                             Console.WriteLine("Error {0}", ex);
                         }
                         break;
                     case 'o':
                         try
                         {
                             object o = WorkflowXamlServices.Load(File.OpenRead("Sequence1.xaml"));
                             Console.WriteLine("Success {0}", o.GetType());
                             DynamicActivity da = o as DynamicActivity;
                             da.Properties.ToList().ForEach(ap => Console.WriteLine("argument: {0}", ap.Name));
                             WorkflowInvoker.Invoke(da, new Dictionary<string, object> { { "argument1", "foo" }, { "argument2", "bar" } });
                         }
                         catch (Exception ex)
                         {
                             Console.WriteLine("Error {0}", ex);
                         }
                         break;
                     case 'e':
                         WorkflowDesigner wd = new WorkflowDesigner();
                         wd.Load("Sequence1.xaml");
                         object obj = wd.Context.Services.GetService<ModelService>().Root.GetCurrentValue();
                         Console.WriteLine("object read type: {0}", obj.GetType());
                         ActivitySchemaType schemaType = obj as ActivitySchemaType;
                         Console.WriteLine("schema type name: {0}", schemaType.Name);
                         schemaType.Members.ToList().ForEach(p => Console.WriteLine("argument: {0}, type: {1}", p.Name, p.Type));
                         break;
                     case 'q':
                         doStuff = false;
                         break;
  
                     default:
                         break;
                 }
  
             }
             Console.WriteLine("All done");
             Console.ReadLine();
         }
  
         private static object CreateWfObject()
         {
             return new Sequence { Activities = { new Persist(), new Persist() } };
         }
     }
 }