Saturday, April 26, 2008

Invoking a XOML workflow from a workflow

The built-in InvokeWorkflow activity requires a compiled workflow type which isn't too useful if you're trying to execute a XOML-based workflow. Initially I thought I could compile the XOML workflow and then use the InvokeWorkflow activity. The problem with this approach is that the InvokeWorkflow's TargetWorkflow property can't be set at run-time.

The solution is pretty straightforward, compile the workflow then use the IStartWorkflow service to start the workflow (which is what InvokeWorkflow does internally). But there are still a couple of issues with this.

First, there is no way to pass parameters to a XOML workflow, since there is no way to add top-level properties. You could workaround this by sub-classing the SequentialWorkflowActivity and adding properties for the parameters you want to pass.

The second problem is if you want the child workflow to finish before continuing with the parent workflow. rather than starting the child workflow asynchronously. I've seen examples of how to do this by the child workflow signalling when it's complete but I've not seen this done in a generic way that doesn't require the child workflow to be modified.

Anyway, here's my code which might be useful.

using System;
using System.IO;
using System.Reflection;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Compiler;
using System.Xml;

namespace WFBuild.Activities
{
  public class InvokeXomlWorkflowActivity: Activity
  {
    public InvokeXomlWorkflowActivity()
    {
    }

    private string xomlFile;
    public string XomlFile
    {
      get { return xomlFile; }
      set { xomlFile = value; }
    }

    protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
    {
      string className = Path.GetFileNameWithoutExtension(xomlFile);

      // compile workflow
      WorkflowCompiler compiler = new WorkflowCompiler();
      WorkflowCompilerParameters wfParams = new WorkflowCompilerParameters();
      wfParams.LibraryPaths.Add(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
      wfParams.ReferencedAssemblies.Add("WFBuild.Activities.dll");
      wfParams.OutputAssembly = string.Format("{0}.dll", className);

      // add x:Class attribute
      string tempFileName = Path.GetTempPath() + "temp.xoml";
      XmlDocument doc = new XmlDocument();
      doc.Load(xomlFile);
      const string namespaceUri = "http://schemas.microsoft.com/winfx/2006/xaml";
      if (doc.DocumentElement.GetAttribute("Class", namespaceUri) == "")
      {
        doc.DocumentElement.SetAttribute("Class", namespaceUri, className);
      }
      doc.Save(tempFileName);

      WorkflowCompilerResults results = compiler.Compile(wfParams, tempFileName);

      if (results.Errors.HasErrors)
      {
        string errors = "";
        for (int i = 0; i < results.Errors.Count; i++)
        {
          errors += results.Errors[i].ErrorText + "\n";
        }
        throw new Exception(errors);
      }
      
      Type[] types = results.CompiledAssembly.GetTypes();
      Type workflow = types[0];

      IStartWorkflow service = executionContext.GetService(typeof(IStartWorkflow)) as IStartWorkflow;
      if (service == null)
      {
        throw new InvalidOperationException("No IStartWorkflow service available");
      }
      Guid guid = service.StartWorkflow(workflow, null);
      if (guid == Guid.Empty)
      {
        throw new InvalidOperationException("Failed to start the workflow");
      }
      return ActivityExecutionStatus.Closed;
    }
  }
}

15 comments:

Anonymous said...

Windows workflow Deserialization
Is anybody can help me? How to load .dll file and xoml file of existing workflow and run it.
No matter manually add in dll reference in console project or use provider.addAssemblyReference to add in the code. I always got problem : “Can not create workflow definition. Input markup is invalid”.

Here is the code:
WorkflowRuntime workflowRuntime = new WorkflowRuntime();
workflowRuntime.StartRuntime();

XmlTextReader reader = new XmlTextReader(“C:\xxx.xoml”);
try
{
instance = workflowRuntime.CreateWorkflow(reader);
instance.Start();
}
catch (WorkflowValidationFailedException exp)
{

StringBuilder errors = new StringBuilder();

foreach (ValidationError error in exp.Errors)
{
errors.AppendLine(error.ToString());
}

MessageBox.Show(errors.ToString(), "Validation errors");
}

my email is jok141@hotmail.com
wait for your response

Doogal said...

I'm not clear on what you're trying to do here. Are you trying to execute a XOML workflow that uses some custom activities compiled in an assembly? If so, you should just be able to add reference to the assembly in your project and the workflow should execute.

Anonymous said...

my aims are using existing workflow dll and xoml to run workflow. latter on, dll and xoml will be loaded from database.this code for test purpose.
I just used code-behind workflow.somebody told this bit code is only used for xaml-only. this workflow uses codeactivity and one input.

Anonymous said...

hi Doogal Bell, I just change the way of coding. still doesnt work.

XmlReader reader = XmlReader.Create(xomlpath);
WorkflowMarkupSerializer wms = new WorkflowMarkupSerializer();
Activity wf1 = wms.Deserialize(reader) as Activity;
Dictionary(of String,Object) para = new Dictinonary(Of String,Object);
para.add("'TheNumber'",1);
NewInstance = WFRuntime.CreateWorkflow(wf1.GetType(),para);
NewInstance.Start();

do you have any ideas? btw, my name is ryan

Doogal said...

Ryan, I don't think what you're trying to do is possible. If you have a XOML only workflow the first code should work and if you've got a workflow compiled into a DLL you can load it up via reflection and execute it but you can't mix and match XOML and a compiled workflow.

Anonymous said...

hi Doogal, for no-code workflow, the dll just for business logic. if use no-code,I need to create custom activity and manually write down the xoml file?
for compiled workflow which i dont know how to do that.
I dont wanna mix them,but find either way to do it.

ryan

Unknown said...

Hi Doogal, would you know how I can invoke a workflow within another workflow asynchronously and specify the workflow instance id? The StartWorkflow() does not allow me to specify the workflow instance id.

Thanks!
-Zhou

Doogal said...

I don't think there's any way to specify the instance ID of a workflow when executing it, the runtime is in charge of allocating an ID, to ensure it's unique.

Unknown said...

Thanks for the reply Doogal!

What if I use the following in a local service?

instance = workflowRuntime.CreateWorkflow(reader);
instance.Start();

I'm not really clear on what the difference is when I create and start a workflow this way vs. using StartWorkflow(). Do you know?

Thanks!
-Zhou

Doogal said...

I think that will work but it still won't let you specify the instance ID of your child workflow as far as I'm aware.

Unknown said...

Oops... I meant to type:

CreateWorkflow(workflowType, null, instanceId);

Doogal said...

Ah, didn't know that existed. I learn something new every day. But why do you need to specify the instance ID?

Unknown said...

The key reason is that I have co-dependencies between workflows. For example, a parent workflow will invoke a bunch of child workflows, but between the child workflows, some may depend on others to finish something before continuing. In which case, I provide each with the instance id of the workflows that they'll need to raise events to. I create all of these relationships before hand and then invoke all the child workflows. Of course I can change my code so that I start all the workflows first, then build the relationships and pass to the workflows what other workflows to raise events to, but I don't have enough time left to change my code :)

Anonymous said...

Hi Doogal,

Wonderful post, really helped with a jam I'm having...

I noticed one thing, you said that you saw somewhere that it was possible for the child (i.e. "invoked") workflow to signal to it's "invoker" that it has completed its work... Can you maybe point me to where you've see it?

Any help would be greatly appreciated!

My email is asaf.peleg_AT_gmail.com

Doogal said...

There's an example hereThis uses a custom service to tell you when the child workflow has completed