October 14, 2005 spout

Workflow Communication Spelunking

Here. The one where I figure out how to communicate into, out of and between WF activities, but not in a satisfying way.
October 14, 2005 fun

Quote of the day

More and more people I know know more and more people I know.” –Melissa Sells

October 14, 2005 spout

Workflow Communication Spelunking

Workflow Communication Spelunking

I just an interesting week taking a crash course in Windows Workflow Foundation (formerly WWF, not just WF) programming. My big hang-up was, how do I communicate into, out of and between activities?

I started with Dennis’s explanation of communications into and out of a workflow. The fundamental idea is that, once a workflow has started, you can’t talk to it directly. Instead, you set up a communications interface that the workflow can use to talk to the host and that the workflow can use to watch for events from the host, e.g.

[DataExchangeService]
public interface IOrderService {
  void CreateOrder(string customer, string orderDescription);
  event EventHandler<OrderEventArgs> OrderApproved;
}

The interface is marked with the DataExchangeServer attribute to mark it as an interface suitable for host<->workflow communication, but other than that, it’s a normal .NET interface. The host, i.e. the chunk of code that creates the WF runtime, implements data exchange service interfaces as singletons, e.g.

class OrderServiceImpl : IOrderService {
  Dictionary<Guid, Order> _workflowOrderMap = new Dictionary<Guid, Order>();

  public void CreateOrder(string customer, string desc) {
    _workflowOrderMap.Add(BatchEnvironment.CurrentInstanceId, new Order(customer, desc));
  }

  public void ApproveOrder(WorkflowInstance wf, string comment) {
    if (OrderApproved != null) {
      Guid wfId = wf.InstanceId;
      OrderApproved(null, new OrderEventArgs(wfId, _workflowOrderMap[wfId], comment));
    }
  }

  public event EventHandler<OrderEventArgs> OrderApproved;
}

With this implementation, the host is allowing the workflow to call the CreateOrder method (which we’ll see it do later) and to subscribe to the OrderApproved event. The CreateOrder method uses its arguments to create an Order object and associate it with the ID of the currently executing workflow (available via BatchEnvironment.CurrentInstanceId). Remember, the service implementation is a singleton, but any number of workflows can call it, so when they do, we track information on a per workflow basis.

WF Question #1: How do I associate objects directly w/ a running workflow instead of tracking things in dictionaries?

The OrderApproved event is used to get information into the workflow.

In our scenario, imagine we’re creating an order, approving it (w/ a comment) and logging the result. In my sample, I have a workflow imaginatively named Workflow2” which captures this sequence:

The invoke method activity is bound to the CreateOrder method of the IOrderMethod method:

<InvokeMethodActivity
ID="invokeMethodActivity1"
  MethodName="CreateOrder"
  InterfaceType="EventSinkAndMethodInvoke2.IOrderService">
  <InvokeMethodActivity.ParameterBindings>
    <wcm:ParameterBinding ParameterName="customer" xmlns:wcm="ComponentModel">
      <wcm:ParameterBinding.Value>
        <?Mapping XmlNamespace="System" ClrNamespace="System" Assembly="mscorlib" ?>
        <ns0:String xmlns:ns0="System">Fabrikam</ns0:String>
      </wcm:ParameterBinding.Value>
    </wcm:ParameterBinding>
    <wcm:ParameterBinding ParameterName="orderDescription" xmlns:wcm="ComponentModel">
      <wcm:ParameterBinding.Value>
        <?Mapping XmlNamespace="System" ClrNamespace="System" Assembly="mscorlib" ?>
        <ns0:String xmlns:ns0="System">42" Plasma TV</ns0:String>
      </wcm:ParameterBinding.Value>
    </wcm:ParameterBinding>
  </InvokeMethodActivity.ParameterBindings>
</InvokeMethodActivity>

In this case, we’re hard-coding the custom and order description fields, but in a real workflow, you’d take those as input parameters.

WF Question #2: How do you pass input parameters into a workflow?

WF Question #3: Is it legal XML to have a processing instruction in the middle of a file, e.g. ?

After the workflow creates the order, it waits for a human to approve it via an event sink activity:

<EventSinkActivity
ID="eventSinkActivity1"
  EventName="OrderApproved"
  InterfaceType="EventSinkAndMethodInvoke2.IOrderService">
  <EventSinkActivity.ParameterBindings>
    <wcm:ParameterBinding ParameterName="Comment" xmlns:wcm="ComponentModel">
      ...
    </wcm:ParameterBinding>
    <wcm:ParameterBinding ParameterName="Order" xmlns:wcm="ComponentModel">
      ...
    </wcm:ParameterBinding>
  </EventSinkActivity.ParameterBindings>
</EventSinkActivity>

The event sink waits for the host to fire an event, which has several interesting bits. The first interesting bit is the parameter names, which are bound to the public properties of the OrderEventArgs class passed to the OrderEvent event:

[Serializable]
public class OrderEventArgs : WorkflowMessageEventArgs {
  Order _order;
  public Order Order { get { return _order; } }

  string _comment;
  public string Comment { get { return _comment; } }

  public OrderEventArgs(Guid workflowInstanceId, Order order, string comment)
: base(workflowInstanceId) {
    _order = order;
    _comment = comment;
  }
}

Notice that the custom OrderEventArgs class derives from the WorkflowMessageEventArgs class and passes in the workflow instance ID. This is required so that the event can be routed to the appropriate workflow. Without it, you’ll get the following illuminating error in beta 1:

An unhandled exception of type System.Workflow.Runtime.EventDeliveryFailedException’ occurred System.Workflow.Runtime.dll”

WF Question #4: Can we get more descriptive exception messages?

Luckily, this error only happens when you’re running under the debugger; it’s swallowed completely when your program runs normally.

WF Question #5: Can we get exceptions at runtime, too?

Notice also that the OrderEventArgs class is marked with the Serializable attribute. This is required to cross the boundary into the workflow. Without it, you’ll get the ever helpful EventDeliveryFailedException exception.

WF Question #6: What boundary are we crossing when fire an event into a workflow?

Further, all objects sent into a workflow need to be serializable as well, like the Order class (also yielding EventDeliveryFailedException if you forget):

[Serializable]
public class Order {
  Guid _orderId = Guid.NewGuid();
  public Guid OrderId { get { return _orderId; } }

  string _customer;
  public string Customer {
    get { return _customer; }
    set { _customer = value; }
  }

  string _desc;
  public string Description {
    get { return _desc; }
    set { _desc = value; }
  }

  public Order(string customer, string desc) {
    _customer = customer;
    _desc = desc;
  }
}

Firing the event is as easy as calling our helper function from outside of our workflow to cross the boundary into the workflow:

class OrderServiceImpl : IOrderService {
  ...
  public void ApproveOrder(WorkflowInstance wf, string comment) {
    if (OrderApproved != null) {
      Guid wfId = wf.InstanceId;
      OrderApproved(null, new OrderEventArgs(wfId, _workflowOrderMap[wfId], comment));
    }
  }

  public event EventHandler<OrderEventArgs> OrderApproved;
}

Notice that we need the workflow instance ID, which we pass in via the WorkflowInstance we get when starting the workflow:

static void Main() {
  WorkflowRuntime workflowRuntime = new WorkflowRuntime();

  // Add IOrderService implementation
  OrderServiceImpl orderService = new OrderServiceImpl();
  workflowRuntime.AddService(orderService);

  workflowRuntime.StartRuntime();
  workflowRuntime.WorkflowCompleted += OnWorkflowCompleted;

  Type type = typeof(EventSinkAndMethodInvoke2.Workflow2);
  WorkflowInstance wf = workflowRuntime.StartWorkflow(type);

  // Simulate human decision time and approve the order
  System.Threading.Thread.Sleep(1000);
  orderService.ApproveOrder(wf, "this is a *fine* order!");

waitHandle.WaitOne();
  workflowRuntime.StopRuntime();
}

Once the event data is fired into the event, the event sink’s parameter binding provide enough infrastructure to be able to access the data in subsequent activities, e.g. the code activity that comes right after the event activity:

<Code ExecuteCode="code1_ExecuteCode" ID="code1" />

In the code1_ExecuteCode method, my code can reach over to the event sink activity’s parameter bindings and access the data that was fired into it:

public partial class Workflow2 : SequentialWorkflow {
  void code1_ExecuteCode(object sender, EventArgs e) {
    Console.WriteLine("Order: approved w/ comment= {0}",
eventSinkActivity1.ParameterBindings["Comment"].Value);
}

There are three reasons I really don’t like this code. The first is that I have to cast to get something typed out of the Value property and I don’t like casting. The second reason is that this technique only works through code; I can’t bind to a parameter from an event sink to a property on another activity, e.g. the Error property on a Termination activity. The third, and most damning reason, is because this induces coupling from my code activity to my event sink activity. I don’t want this coupling to be captured in code, which is another reason to really like the declarative data binding solution.

WF Question #7: Why can’t I bind event sink parameters as input into other activities?

Unfortunately, while I can’t solve reasons #2 or #3 very well, I can solve them partially and I can solve #1 nicely by adding some fields to my workflow class:

public partial class Workflow2 : SequentialWorkflow {
  Order _approvedOrder;
  string _approvalComment;

void code1_ExecuteCode(object sender, EventArgs e) {
    Console.WriteLine("Order: approved w/ comment= {0}", _approvalComment);
  }
}

The _approvedOrder and _approvalComment fields can be bound to the event sink parameters like so:

<EventSinkActivity
ID="eventSinkActivity1"
  EventName="OrderApproved"
  InterfaceType="EventSinkAndMethodInvoke2.IOrderService">
  <EventSinkActivity.ParameterBindings>
    <wcm:ParameterBinding ParameterName="Comment" xmlns:wcm="ComponentModel">
      <wcm:ParameterBinding.Value>
        <wcm:ActivityBind Path="_approvalComment" ID="{/Workflow}" />
      </wcm:ParameterBinding.Value>
    </wcm:ParameterBinding>
    <wcm:ParameterBinding ParameterName="Order" xmlns:wcm="ComponentModel">
      <wcm:ParameterBinding.Value>
        <wcm:ActivityBind Path="_approvedOrder" ID="{/Workflow}" />
      </wcm:ParameterBinding.Value>
    </wcm:ParameterBinding>
  </EventSinkActivity.ParameterBindings>
</EventSinkActivity>

Now, when the event sink activity fires, these two workflow fields are populated so that by the time the code activity is executed, they’re ready for use, all typed up and ready to go. However, while this reduces coupling to some degree, i.e. the code activity just hopes that somebody provides the data and not that is has to be a specific activity, this is not the same as failing to execute an activity until you have valid values to pass to a function. Instead, it’s like setting global variables, hoping they’re set properly when the code that accesses them needs them and having no idea what’s affected if you have to remove them.

Still, with the parameter binding in place between the event sink parameters and the workplace class fields, I can bind the input to an activity:

<Terminate
Error="*d2p1:ActivityBind(ID={/Workflow};Path=_approvalComment)" ID="terminate1"
  xmlns:d2p1="ComponentModel" />

Clearly, this is binding syntax only a mother could love, but luckily the designer did it for me, it’s going to change in the next beta and it does most of what I want, i.e. bind the input of the Error property on the Terminate activity to something produced by another activity. It’s not the same as binding the event sink activity parameters directly, thereby allowing me to shed this dirty global variable feeling, but it’s oh so close.

The VS05b2, WF/WinFXb1 sample code is available for your enjoyment.

Props to Dennis Pilarinos and Anandhi Somasekaran from the WF group for help figuring this out.

October 13, 2005 fun

His Outsourced Life

Here.

An Esquire magazine author hires not one but two Indian assistants and outsources practically everything but watching TV to them. By the end of the article, he’s outsourced writing the article. I don’t know if it’s true, but it is funny (and scary).

October 13, 2005 .net

How is “workflow” different from “visual programming?”

Is there something intrinsic about workflow” that needs to be surfaced to folks or would any visual programming language do? I’m not finding any commercial products in my web searches, but I know that there have been some integrated component”-style programming languages where you lay flow and logic out from a set of components on a toolbox. Is Windows Workflow Foundation one of those that MS just happens to be doing or is there something important about workflow” that interests folks?
October 11, 2005 .net

What interests you about Windows Workflow Foundation?

The PDC was a buzz with folks praising WWF. Please tell me a) what you think is cool about WWF and b) how it will improve your life or the lives of your colleagues or customers. Thanks!
October 10, 2005 fun

It’s a Windows Life

A story about how the world would be if MS never existed.

I come from a world where I had a Mac at home and a Unix box at work, giving me the best of both worlds in the late 80s. After that, I got a job at Intel programming Windows 3.1 and wondered why the hell this piece o’ crap ran most of the world. 15 years later, I’m a happy Windows user and programmer. Did I change or did Windows?

October 10, 2005 spout

Open Office is a “real” thing

I use my family as a litmus test of when things are real” or not. None of them have technology backgrounds or training, so if something is real to them, it’s real. For example, it was my grandmother that asked whether she need Windows 95 (she didn’t have a computer, mind you). I head from my folks about spyware and phish attacks. And now, my sister-in-law asked me if she should be upgrading her Windows ME machine to OpenOffice.

Of course, OpenOffice isn’t an OS, but as far as she knows, MS Office is her current OS, so maybe OpenOffice is what she needs.

I believe that it’s in her best interest for me to steer her away from unsupported OSS software unless I want to become her tech support liaison (difficult, since I’ve never used OpenOffice). Also, she’s looking for something that will function with little or no problems,” which I can’t claim of any software, free or not.

Still, it makes you wonder if these OSS guys could really make a credible play for the desktop. Obviously, they’re not going to produce Office 12 or Windows Vista anytime soon, but they’re nipping at our heels pretty credibly if they can attract the attention of the secular members of my family. How much longer before OSS becomes good enough” for most uses and the latest thing you buy from actual software vendors is just fancier? In fact, how many actual software vendors do we really have left anyway?


← Newer Entries Older Entries →