The Trouble with System.Activities.ForEach (and ParallelForEach)

(Subtitle: Why you need IActivityTemplateFactory)

Another forum post reminded me of one thing about ActivityAction that throws me for a loop every time. Every time, that is, that I'm rehosting workflow designer or creating new custom activities, so maybe not that often but it's kind of annoying.

Imagine, here's us, creating a toolbox for our rehosted application:

   using System.Activities.Statements;

   using System.Activities.Presentation.Toolbox;

   ...

   toolbox.Categories.Add(

       new System.Activities.Presentation.Toolbox.ToolboxCategory

       {

           CategoryName = "Trouble",

           Tools = {

               new ToolboxItemWrapper(typeof(ForEach<>)),

               new ToolboxItemWrapper(typeof(ParallelForEach<>))

           }

       }

    );

 

Which looks fine, right? It appears to work. We can drag and drop to create a ForEach<T> activity into our workflow. We can even apparently drag and drop a flowchart, for example, inside the ForEach. We can continue happily for quite a while until suddenly we notice: there is something really weird going on here.

 

When we save to XAML, the flowchart we added inside the ForEach isn't saved. If we double-click the flowchart, we end up in a breadcrumb view of the flowchart, but with no parent view to go back to! Somehow our flowchart has become disconnected from the rest of the workflow. At this point I went crazy wondering: is it a bug with ForEach activity? No, it works just perfectly in Visual Studio. Why?

 

Hint:
Look at the XAML serialization for a typical ForEach activity (created in VS, of course):

 

<ForEach x:TypeArguments="x:Int32" Values="[{1, 2, 3, 4}]">

<ActivityAction x:TypeArguments="x:Int32">

<ActivityAction.Argument>

<DelegateInArgument x:TypeArguments="x:Int32" Name="item" />

</ActivityAction.Argument>

<WriteLine Text="Can I have a little more" />

</ActivityAction>

</ForEach>

Bigger hint:
Now here is a ForEach activity from our rehosted application which didn't serialize its contents properly at all:

<ForEach x:TypeArguments="x:Int32" Values="[{1, 2, 3, 4}]"/>

So there is no ActivityAction. [What's ActivityAction? Here's the lowdown from Matt.]

So, how does that explain what is going on? To understand why the flowchart appeared to be added even though there was no ActivityAction to add it to, understand the problem is that the WPF binding in the Activity Designer for ForEach is silently failing. Here are two interesting controls on the ForEach designer

<TextBox Text="{Binding Path=ModelItem.Body.Argument.Name, Mode=TwoWay, ValidatesOnExceptions=True}" />

 

<sap:WorkflowItemPresenter IsDefaultContainer="True"

                  HintText="{DynamicResource dropActivityHint}"

                  Item="{Binding Path=ModelItem.Body.Handler, Mode=TwoWay}"

                  AllowedItemType="{x:Type sa:Activity}" />

(There is also one more control, an ExpressionTextBox.)

The first control is the TextBox where we declare the name of the ForEach activity's 'loop variable' (such as 'i'). The second control is the holder for the child activity we drag and drop: flowchart. When we drag and drop, the binding Binding Path=ModelItem.Body.Handler fails to evaluate, because ModelItem.Body is null. But the visual of course doesn't fail to update, which makes us think everything is fine even though it's really not.

OK, so now we know the problem how can we fix it? Well, we could fix our app by writing an IActivityTemplateFactory like this:

public class ForEachFactory<T> : IActivityTemplateFactory

{

    public Activity Create(DependencyObject target)

    {

        return new ForEach<T>

        {

            DisplayName = "ForEachFromFactory",

            Body = new ActivityAction<T>

            {

                Argument = new DelegateInArgument<T>("i")

            }

        };

    }

}

and add that to our toolbox instead (... new ToolboxItemWrapper(typeof(ForEachFactory))). Or, now that we understand that, we could be a little bit lazier:

new ToolboxItemWrapper(typeof(System.Activities.Presentation.Factories.ForEachWithBodyFactory<>)),

new ToolboxItemWrapper(typeof(System.Activities.Presentation.Factories.ParallelForEachWithBodyFactory<>))

Yep, these classes exist in the framework already. Note, they are in a different assembly. System.Activities.Core.Presentation, not System.Activities.