Secrets of the XAML Build Task
(Or: [not] using XAML Build Task in a rehosted scenario.)
Motivated by some old, less old, and recent forum threads about allowing compositional workflows in a rehosted designer scenario, with the ability to reuse activities created in XAML, I have finally played through some options for building assemblies directly from rehosted designer XAML.
First though, let’s look at the standard option for compiling workflow assemblies.
What normally happens when you build XAML files in an Activity Library project in VS? Your XAML file is compiled, and a new Activity type is created in the output assembly.
XamlBuildTask is the name of the key msbuild task involved in generating classes and assemblies from workflow XAML. In the process of compiling simplest possible Activity Library project, a single XAML file results in the generation of many intermediate files.
We can see them in Solution Explorer if we turn on ‘show hidden items’.
The hidden files are something I don’t normally look at, but if you have ever had something weird happen during workflow construction, you may have found yourself walking the stack and browsing through the InitializeComponent method in Activity1.g.cs. And thereby discovering at least that one file.
[System.Diagnostics.DebuggerNonUserCodeAttribute()] public void InitializeComponent() { if ((this._contentLoaded == true)) { return; } this._contentLoaded = true; string resourceName = this.FindResource(); System.IO.Stream initializeXaml = typeof(Activity1).Assembly.GetManifestResourceStream(resourceName); System.Xml.XmlReader xmlReader = null; System.Xaml.XamlReader reader = null; System.Xaml.XamlObjectWriter objectWriter = null; try { System.Xaml.XamlSchemaContext schemaContext = XamlStaticHelperNamespace._XamlStaticHelper.SchemaContext; xmlReader = System.Xml.XmlReader.Create(initializeXaml); System.Xaml.XamlXmlReaderSettings readerSettings = new System.Xaml.XamlXmlReaderSettings(); readerSettings.LocalAssembly = System.Reflection.Assembly.GetExecutingAssembly(); readerSettings.AllowProtectedMembersOnRoot = true; reader = new System.Xaml.XamlXmlReader(xmlReader, schemaContext, readerSettings); System.Xaml.XamlObjectWriterSettings writerSettings = new System.Xaml.XamlObjectWriterSettings(); writerSettings.RootObjectInstance = this; writerSettings.AccessLevel = System.Xaml.Permissions.XamlAccessLevel.PrivateAccessTo(typeof(Activity1)); objectWriter = new System.Xaml.XamlObjectWriter(schemaContext, writerSettings); System.Xaml.XamlServices.Transform(reader, objectWriter); |
This method is a good place to visit because it gives you some idea about what XamlReaders and XamlWriters get up to. There’s XAML reading and writing happening whenever you construct one of your XAML-defined workflows is also good to know to help build intuitions about workflow loading performance etc.
XamlReaders(Writers) can read (or write) either XAML files, or objects. Huh.
Aside from the InitializeComponent() method (which is a staple of every custom activity) you will also find, specific to your own activity, the definitions of your Activity’s arguments and properties (If you have any.)
public System.Activities.InArgument<string> argument1 { get { return this._argument1; } set { this._argument1 = value; } } |
But, overall, aside from the arguments, this file is hardly going to vary at all, and the arguments follow a pretty simple schema.
Hmm. It looks like it would be straightforward to generate this class programmatically…
Today’s Idea
Using Reflection.Emit, we try to replicate the XAMLBuildTask functionality, so that we can have a XAML Compiler suitable for use in rehosting scenarios. There’s no need for msbuild.exe, no need for C# compiler, and no need for XamlBuildTask.dll either(!).
First useful observation
- our mission will be made easier if instead of doing Reflection.Emit to generate the entire body of InitializeComponent, we can instead just use Reflection.Emit to have a little stub call into a standardized, homogenized, parameterized version of InitializeComponent. We can munge InitializeComponent to have a signature like this:
public static void InitializeFromStream(Activity a, Assembly localAssembly, Stream xamlStream, XamlSchemaContext schemaContext);
|
But actually, some of those parameters can be autodetermined by deduction or convention. localAssembly is probably just a.GetType().Assembly. xamlStream could be assumed to be a resource stream embedded in the same assembly, with a name based on a.GetType().FullName. SchemaContext seems like the trickiest one to get right, but we can hardcode that too while we experiment with the concept. Nifty.
Putting idea into action, all in a day’s work I have Reflection.Emitted an assembly which seems like it should do everything I want – it has:
-the Activity class type definition, with all the properties, and invoking a substitute for InitializeComponent() in the constructor
-the embedded XAML file which I used as input
There’s just one problem… it doesn’t work! Here’s my XAML which I’m getting as input and embedding in the assembly.
<Activity mc:Ignorable="sap" x:Class="ActivityLibrary1.Hello" this:Hello.x="["Fred" + "Bob"]" xmlns="https://schemas.microsoft.com/netfx/2009/xaml/activities" xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mv="clr-namespace:Microsoft.VisualBasic;assembly=System" xmlns:mva="clr-namespace:Microsoft.VisualBasic.Activities;assembly=System.Activities" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:s1="clr-namespace:System;assembly=System" xmlns:s2="clr-namespace:System;assembly=System.Xml" xmlns:s3="clr-namespace:System;assembly=System.Core" xmlns:s4="clr-namespace:System;assembly=System.ServiceModel" xmlns:sad="clr-namespace:System.Activities.Debugger;assembly=System.Activities" xmlns:sap="https://schemas.microsoft.com/netfx/2009/xaml/activities/presentation" xmlns:scg="clr-namespace:System.Collections.Generic;assembly=System" xmlns:scg1="clr-namespace:System.Collections.Generic;assembly=System.ServiceModel" xmlns:scg2="clr-namespace:System.Collections.Generic;assembly=System.Core" xmlns:scg3="clr-namespace:System.Collections.Generic;assembly=mscorlib" xmlns:sd="clr-namespace:System.Data;assembly=System.Data" xmlns:sl="clr-namespace:System.Linq;assembly=System.Core" xmlns:st="clr-namespace:System.Text;assembly=mscorlib" xmlns:this="clr-namespace:ActivityLibrary1" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
<x:Members> <x:Property Name="x" Type="InArgument(x:String)" /> </x:Members> <sap:VirtualizedContainerService.HintSize>273,240</sap:VirtualizedContainerService.HintSize> <mva:VisualBasic.Settings>Assembly references and imported namespaces for internal implementation</mva:VisualBasic.Settings> <Sequence sad:XamlDebuggerXmlReader.FileName="c:\users\tilovell\documents\visual studio 2010\Projects\XamlActivityCompiler\ActivityLibrary1\Hello.xaml" sap:VirtualizedContainerService.HintSize="233,200"> <sap:WorkflowViewStateService.ViewState> <scg3:Dictionary x:TypeArguments="x:String, x:Object"> <x:Boolean x:Key="IsExpanded">True</x:Boolean> </scg3:Dictionary> </sap:WorkflowViewStateService.ViewState> <WriteLine sap:VirtualizedContainerService.HintSize="211,59" Text="["Hello " + x]" /> </Sequence> </Activity>
|
And here is the mysterious error message I get when I try and construct a Hello activity:
System.Xaml.XamlObjectWriterException occurred
Message='Cannot set unknown member 'ActivityLibrary1.Hello.x'.' Line number '1' and line position '63'.
Source=System.Xaml
LineNumber=1
LinePosition=63
StackTrace:
at System.Xaml.XamlObjectWriter.WriteStartMember(XamlMember property)
InnerException:
Wonderful… not! What the heck. I couldn’t figure out what could be going wrong. My type in the assembly is not exactly the same, but it should be functionally equivalent. It definitely has a property on it called ‘x’. Double-checked and triple-checked. Pulled out ildasm, did diffs, the whole 9 yards.
A night’s sleep proved helpful. Apparently it’s not the type which is the problem… but maybe it’s something else in that assembly… no wait, the manifest? Wait… what?
Revelation
It turns out that I fail. The XAML stream that XamlBuildTask will embed in your assembly is NOT actually the same as the original XAML which defines the workflow class. The XAML embedded in your assembly as a resource, and later loaded by InitializeComponent, actually looks more like this:
<?xml version="1.0" encoding="utf-8"?> <this:Hello this:x="Fred" mva:VisualBasic.Settings="Assembly references and imported namespaces for internal implementation" xmlns="https://schemas.microsoft.com/netfx/2009/xaml/activities" xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mv="clr-namespace:Microsoft.VisualBasic;assembly=System" xmlns:mva="clr-namespace:Microsoft.VisualBasic.Activities;assembly=System.Activities" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:s1="clr-namespace:System;assembly=System" xmlns:s2="clr-namespace:System;assembly=System.Xml" xmlns:s3="clr-namespace:System;assembly=System.Core" xmlns:sad="clr-namespace:System.Activities.Debugger;assembly=System.Activities" xmlns:sap="https://schemas.microsoft.com/netfx/2009/xaml/activities/presentation" xmlns:scg="clr-namespace:System.Collections.Generic;assembly=System" xmlns:scg1="clr-namespace:System.Collections.Generic;assembly=System.ServiceModel" xmlns:scg2="clr-namespace:System.Collections.Generic;assembly=System.Core" xmlns:scg3="clr-namespace:System.Collections.Generic;assembly=mscorlib" xmlns:sd="clr-namespace:System.Data;assembly=System.Data" xmlns:sl="clr-namespace:System.Linq;assembly=System.Core" xmlns:st="clr-namespace:System.Text;assembly=mscorlib" xmlns:this="clr-namespace:ActivityLibrary1;assembly=ActivityLibrary1" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"> <this:Hello.Implementation> <Sequence sad:XamlDebuggerXmlReader.FileName="c:\users\tilovell\documents\visual studio 2010\Projects\XamlActivityCompiler\ActivityLibrary1\Hello.xaml"> <WriteLine Text="["Hello " + x]" /> </Sequence> </this:Hello.Implementation> </this:Hello>
|
The key differences of this XAML to the XAML of our class definition are:
-there is no <Activity x:Class> - instead we have <this:Hello> as the root element
-there are no <x:Members>
I wonder how was this XAML generated? Somewhere inside XamlBuildTask no doubt!
Maybe we could generate it too, with a little less work given some assumptions that we are generating a straightfoward Activity?
Inspiration: InitializeComponent() could run in reverse!
*thinks*
What happens in InitializeComponent()?:
-an object comes in
-a XAML file comes in, is read, and sent to a writer which initializes properties on the object
What if we reverse that?
-an initialized object comes in
-the object properties are read and sent to a writer, which creates the equivalent XAML file
And maybe we can reverse it all very easily by just swapping XamlObjectWriter for XamlObjectReader, and XamlXmlReader for XamlXmlWriter??
It turns out there is a wrinkle or two here. Having generated a temporary version of the “Hello” type, here is the logical attempt to make the above described happen:
public static string GenerateXAML(ActivityBuilder builder, Type fakeType) { Activity fakeInstance = (Activity)Activator.CreateInstance(fakeType);
foreach (var prop in builder.Properties) { PropertyInfo pi = fakeInstance.GetType().GetProperty(prop.Name); pi.SetValue(fakeInstance, prop.Value, null); }
PropertyInfo implementationProperty = typeof(Activity).GetProperty("Implementation", BindingFlags.Instance | BindingFlags.NonPublic); implementationProperty.SetValue(fakeInstance, builder.Implementation, null);
PropertyInfo constraintsProperty = typeof(Activity).GetProperty("Constraints", BindingFlags.Instance | BindingFlags.NonPublic); var constraintsCollection = (Collection<Constraint>)constraintsProperty.GetValue(fakeInstance, null); foreach (var constraint in builder.Constraints) { constraintsCollection.Add(constraint); }
XamlObjectReader reader = new XamlObjectReader(fakeInstance);
StringBuilder sb = new StringBuilder(); StringWriter textWriter = new StringWriter(sb); XamlXmlWriter writer = new XamlXmlWriter(textWriter, new XamlSchemaContext());
XamlServices.Transform(reader, writer); writer.Flush(); textWriter.Flush();
string strXaml = sb.ToString(); return strXaml; }
|
This fails gloriously at runtime due to what seems like a minor oversight on my part:
Activity.Implementation is not actually type Activity, it is actually type Func<Activity> .
So calling PropertyInfo.SetValue() fails.
OK, so we fix it like this, right?
PropertyInfo implementationProperty = typeof(Activity).GetProperty("Implementation", BindingFlags.Instance | BindingFlags.NonPublic); implementationProperty.SetValue(fakeInstance, new Func<Activity>(() => builder.Implementation), null);
|
In an ideal world, yes, it would be that easy. Positively, we get some XAML generated by the above code. But it seems kind of… empty?
<?xml version="1.0" encoding="utf-16"?> <Hello x="["Fred" + "Bob"]" xmlns="clr-namespace:ActivityLibrary1;assembly=ActivityLibrary1" />
|
Because Implementation is a protected property, it will not be serialized to XAML by default.
Hmm.
And yet in InitializeComponent, somehow the protected implementation property was being set from XAML…
Oh yeah, there were some XamlObjectWriterSettings involved.
Maybe we can just use XamlObjectReaderSettings to fix things? Like this.
XamlObjectReaderSettings readerSettings = new XamlObjectReaderSettings { AllowProtectedMembersOnRoot = true }; XamlObjectReader reader = new XamlObjectReader(fakeInstance, readerSettings);
|
D’oh.
Apparently that idea was really, really close to working!
Just not quite.
I believe the reason this is unsupported is something to do with the fact that Activity.Implementation is kind of special, in that it uses a funky feature for deferred Xaml loading. The declaration of Implementation looks something like this:
[DefaultValue((string) null), XamlDeferLoad(typeof(FuncDeferringLoader), typeof(Activity)), Browsable(false), Ambient]
protected virtual Func<Activity> Implementation
Actually this lack of serialization is a darn useful feature most of the time, because this is what stops your custom activity XAML files from being recursively bloated by every child activity serializing its own internal implementation details, and their children’s, and their children’s children’s and so on, which would get totally ridiculous really fast.
OK. So now what? Hmm.. Hacks to the rescue!
Nobody is really forcing us to feed a genuine Activity to XamlObjectReader, are they, now?
Let’s just create a temporary fake type to stand in for our real activity type, which has public, non-deferring Implementation and Constraints properties… and…
Presto! A non-empty XAML!
<?xml version="1.0" encoding="utf-16"?> <Hello x="["Fred" + "Bob"]" xmlns="clr-namespace:ActivityLibrary1;assembly=ActivityLibrary1" xmlns:p="https://schemas.microsoft.com/netfx/2009/xaml/activities" xmlns:sad="clr-namespace:System.Activities.Debugger;assembly=System.Activities"> <Hello.Implementation> <p:Sequence sad:XamlDebuggerXmlReader.FileName="c:\users\tilovell\documents\visual studio 2010\Projects\XamlActivityCompiler\ActivityLibrary1\Hello.xaml"> <p:WriteLine Text="["Hello " + x]" /> </p:Sequence> </Hello.Implementation> </Hello>
|
Does it work?
Yes. At least, for the canonical hello world sample, it does! Here’s the test harness code in action:
static void Main(string[] args) { string xamlPath = @"c:\users\tilovell\documents\visual studio 2010\Projects\XamlActivityCompiler\ActivityLibrary1\Hello.xaml";
List<string> assemblyNames = new List<string>();
assemblyNames.Add("Microsoft.CSharp, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); assemblyNames.Add("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); assemblyNames.Add("System.Activities, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"); assemblyNames.Add("System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); assemblyNames.Add("System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); assemblyNames.Add("System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); assemblyNames.Add("System.ServiceModel.Activities, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"); assemblyNames.Add("System.ServiceModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); assemblyNames.Add("System.Xaml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); assemblyNames.Add("System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); assemblyNames.Add("System.Xml.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
assemblyNames.Add("XamlActivityCompiler, Version=1.0.0.0, Culture=neutral");
string assemblyPath = XamlActivityCompiler.Compiler.Compile(xamlPath, assemblyNames.ToArray());
var loadFile = Assembly.LoadFile(Path.Combine(Environment.CurrentDirectory, assemblyPath)); var newAssembly = Assembly.Load(loadFile.FullName); var newType = newAssembly.GetType("ActivityLibrary1.Hello");
var testInstance = Activator.CreateInstance(newType);
// Hello with default arguments WorkflowInvoker.Invoke((Activity)testInstance);
// Hello "Bertha" WorkflowInvoker.Invoke((Activity)testInstance, new Dictionary<string, object> { {"x", "Bertha"} }); } |
Which happily outputs
So wait, quick summary please, what does this mean - XamlBuildTask can be replaced in a few hundred lines of code, and in this way it can be integrated into a rehosted app, allowing me to generate activity class assemblies, which become reusable activity components like those you can produce with VS?
Yup, looks that way.
If this link works (fingers crossed), you can download the code HERE.
Disclaimer: You are free to use and modify it however you wish, but I am not giving any warranty for it, nor free support for this code.
This code really isn’t yet product-level quality. In particular - I didn’t think hard about the right way to initialize the XamlSchemaContext and get it the correct set of assemblies. And also, it doesn’t have support for every possible x:Class XAML you can have, such as custom Attributes on the Activity or its properties.
Enjoy.
Comments
Anonymous
August 30, 2011
I am confused by this post. If you host your designer in separate AppDomains that have shadow copy enabled, you can use codedom (or string.format) to generate the glue code and let MsBuild put together a new assembly that you can use at design time. At runtime all the flows are there, so just skip the Designer generation and you are good to go. While I enjoy a good turn of code as much as the next person I don't understand why we would not use the support that MSBUILD gives.Anonymous
April 18, 2012
I haven't gone through this post extensively but I am wondering if this technique could be used to convert a XAML workflow to it's C# equivalent?Anonymous
July 18, 2012
Congratulations for paper!! but... when activity is recursivity it break!Anonymous
March 06, 2014
This is what I was looking, However this works only one time, if I tried to change xaml and try to rebuild the dll it doesn't overwrite the dll and throws exception of "file is being used by another process" Any Idea?