• mwinkle.blog

    Introduction to WF Designer Rehosting (Part 2)

    • 3 Comments

    standard beta disclaimer.  This is written against the beta1 API’s.  If this is 2014, the bits will look different.  When the bits update, I will make sure to have a new post that updates these (or points to SDK samples that do)

    In yesterday’s post, we went over the core components of the designer.  Let’s now take that and build that rehosts the designer, and then we’ll circle back around and talk about what we did and what comes next.

    Start VS, Create a new project, and select a WPF project

    image

    Inside the VS project add references to the System.Activities.* assemblies.  For now, that list looks like

    • System.Activities.dll
    • System.Activities.Design.Base.dll
    • System.Activities.Design.dll
    • System.Activities.Core.Design.dll

    image

    You might think the list of design assemblies is excessive.  We’ll be collapsing probably into two design assemblies, one with the designer infrastructure and one with the activity designers in subsequent milestones.

    Create some layout in the WPF window to hold the various designer elements.  I usually do a three column grid for toolbox, property grid and designer canvas.

    The XAML for this looks roughly like this:

    <Window x:Class="BlogPostRehosting.Window1"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="Window1" Height="664" Width="831">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="1*"/>
                <ColumnDefinition Width="3*"/>
                <ColumnDefinition Width="2*"/>
            </Grid.ColumnDefinitions>
        </Grid>
    </Window>

    Now that we’ve got the layout down, let’s get down to business.  First let’s just get an app that displays the workflow designer and then we will add some other interesting features. We wanted to make it easy to get a canvas onto your host application, and to program against it.  The key type that we use is WorkflowDesigner, this encapsulates all of the functionality, and operating context, required.  Let’s take a quick look at the type definition

     

    Name Description
    Context Gets or sets an EditingContext object that is a collection of services shared between all elements contained in the designer and used to interact between the host and the designer. Services are published and requested through the EditingContext.
    ContextMenu Gets the context menu for this designer.
    DebugManagerView Provides a DebuggerServicethat is used for runtime debugging.
    PropertyGridFontAndColorData Sets the property grid font and color data.
    PropertyInspectorView Returns a UI element that allows the user to view and edit properties of the workflow.
    Text Gets or sets the XAML string representation of the workflow.
    View Returns a UI element that allows the user to view and edit the workflow visually.

     

    The editing context is where we will spend more time in the future, for now the View is probably what’s most interesting, as this is the primary designer canvas.  There are also some useful methods to load and persist the workflow as well.

    Let’s start off real simple, and write some code that will display a basic sequence, and we’ll get more sophisticated as we go along.

       1:  using System.Windows;
       2:  using System.Windows.Controls;
       3:  using System.Activities.Design;
       4:  using System.Activities.Core.Design;
       5:  using System.Activities.Statements;
       6:   
       7:  namespace BlogPostRehosting
       8:  {
       9:      /// <summary>
      10:      /// Interaction logic for Window1.xaml
      11:      /// </summary>
      12:      public partial class Window1 : Window
      13:      {
      14:          public Window1()
      15:          {
      16:              InitializeComponent();
      17:              LoadWorkflowDesigner();
      18:          }
      19:   
      20:          private void LoadWorkflowDesigner()
      21:          {
      22:              WorkflowDesigner wd = new WorkflowDesigner();
      23:              (new DesignerMetadata()).Register();
      24:              wd.Load(new Sequence 
      25:                              { 
      26:                                  Activities = 
      27:                                  {
      28:                                      new Persist(), 
      29:                                      new WriteLine()
      30:                                  }
      31:                              });
      32:              Grid.SetColumn(wd.View, 1);
      33:              grid1.Children.Add(wd.View);
      34:          }
      35:      }
      36:  }

    Let’s walk through this line by line:

    • Line 22, construct the workflow designer
    • Line 23, Call Register on the DesignerMetadata class.  Note that this associates all of the out of the box activities with their out of the box designers.  This is optional as a host may wish to provide custom editors for all or some of the out of box activities, or may not be using the out of box activities.
    • Line 24-31, Call Load, passing in an instance of an object graph to display.  This gives the host some flexibility, as this instance could come from XAML, a database, JSON, user input, etc.  We simply create a basic sequence with two activities
    • Line 32, set the column for the view
    • Line 33, add the view to the display

    This gives us the following application:

    image

    Now, that was pretty simple, but we’re also missing some key things, namely, the property grid.  It’s important to note however that this has all of the functionality of the designer (the variables designer, the overview map, etc.  This will react just the same as if you were building the workflow in VS. 

    Let’s add the property grid by adding the following two lines:

    Grid.SetColumn(wd.PropertyInspectorView, 2);
    grid1.Children.Add(wd.PropertyInspectorView);

    This will let us see the property grid (so things get a little more interesting).

    image

    So, we’re able to display the workflow and interact with it, but we probably also want to have a constrained authoring experience (not just editing), so that comes in the form of the ToolboxControl.  For the sake of this blog post, we’ll use this in XAML, but we certainly can code against it imperatively as well. 

    <Window x:Class="BlogPostRehosting.Window1"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:sad="clr-namespace:System.Activities.Design;assembly=System.Activities.Design"
            xmlns:sys="clr-namespace:System;assembly=mscorlib"
            Title="Window1" Height="664" Width="831">
        <Window.Resources>
            <sys:String x:Key="AssemblyName">System.Activities, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35</sys:String>
        </Window.Resources>
        <Grid Name="grid1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="1*"/>
                <ColumnDefinition Width="3*"/>
                <ColumnDefinition Width="2*"/>
            </Grid.ColumnDefinitions>
            <sad:ToolboxControl>
                <sad:ToolboxControl.Categories>
                    <sad:ToolboxCategoryItemsCollection CategoryName="Basic">
                        <sad:ToolboxItemWrapper AssemblyName="{StaticResource AssemblyName}" ToolName="System.Activities.Statements.Sequence"/>
                        <sad:ToolboxItemWrapper AssemblyName="{StaticResource AssemblyName}" ToolName="System.Activities.Statements.If"/>
                        <sad:ToolboxItemWrapper AssemblyName="{StaticResource AssemblyName}" ToolName="System.Activities.Statements.Parallel"/>
                        <sad:ToolboxItemWrapper AssemblyName="{StaticResource AssemblyName}" ToolName="System.Activities.Statements.WriteLine"/>
                        <sad:ToolboxItemWrapper AssemblyName="{StaticResource AssemblyName}" ToolName="System.Activities.Statements.Persist"/>
                    </sad:ToolboxCategoryItemsCollection>
                </sad:ToolboxControl.Categories>
            </sad:ToolboxControl>
        </Grid>
    </Window>

     

     

    This lets me specify the items I want to allow a user to drop.

    image

    The thing that is interesting to point out here is that we’ve built a full featured, constrained editor (with things like copy paste, undo/redo, etc) with not too much code.

    Next time, we’ll get into to doing some more interesting bits as well to interact with the item being edited, serialize to XAML, and explore the editing context some more.  Let me know what you think!

  • mwinkle.blog

    Introduction to WF Designer Rehosting (Part 1)

    • 1 Comments

    standard beta disclaimer.  This is written against the beta1 API’s.  If this is 2014, the bits will look different.  When the bits update, I will make sure to have a new post that updates these (or points to SDK samples that do)

     

    In WF3, we allowed our customers to rehost the WF designer inside their own applications.  This has many reasons, usually about monitoring a workflow or allowing an end user to customize a constrained workflow (visual construction of an approval process, for instance). This article became the gold standard for writing rehosted applications.  As we were planning our work for the WF4 designer, this was certainly a scenario we considered, and one we wanted to make easier. 

    This post consists of a few parts

    • Designer architecture – introduce the pieces, parts and terms we’ll use throughout
    • Simple rehosting – getting it up and running
    • What to do next

     

    Designer Architecture

    image

    There are a few key components here

    • Source
      • In VS, this is xaml, but this represents the durable storage of the “thing” we are editing
    • Instance
      • This is the in memory representation of the item being edited.  In vs2010 for the WF designer, this is a hierarchy of System.Activities instances (an object tree)
    • Model Item Tree
      • This serves as an intermediary between the view and the instance, and is responsible for change notification and tracking things like undo state
    • Design View
      • This is the visual editing view that is surfaced to the user, the designers are written using WPF, which uses plain old data binding in order to wire up to the underlying model items (which represent the data being edited).
    • Metadata Store
      • This is a mapping between type and designers, an attribute table for lack of a better term.  This is how we know what designer to use for what type

    I’ll go into more detail about these pieces and parts in future posts as well, but this is the mental model of the things I will be talking about as we go through the designer.

     

    Stay tuned, part 2 will come tomorrow!

  • mwinkle.blog

    Types, Metatypes and Bears, Oh my!

    • 1 Comments

    ***** 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="http://schemas.microsoft.com/netfx/2009/xaml/activities/design"
         xmlns:__Sequence1="clr-namespace:WorkflowConsoleApplication1;" 
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:p="http://schemas.microsoft.com/netfx/2009/xaml/activities" 
         xmlns:x="http://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="http://schemas.microsoft.com/netfx/2009/xaml/activities/design" 
         xmlns:__foo="clr-namespace:bar.rock;" 
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:p="http://schemas.microsoft.com/netfx/2009/xaml/activities" 
         xmlns:x="http://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() } };
            }
        }
    }
Page 1 of 1 (3 items)