As an example of another general-purpose composite activity, consider an activity called Graph (shown in Listing B.2) that represents a graph. The nodes of the graph are the Graph activity's child activities. The arcs of the graph (connections between nodes) are specified as metadata that is held by Graph. For simplicity's sake, we will limit ourselves here to acyclic graphs but there is no fundamental reason to disallow more complex graphs with cycles. Listing B.2. Graph Activity using System; using System.Collections.Generic; using System.ComponentModel; using System.Workflow.ComponentModel; namespace EssentialWF.Activities { public class Graph : CompositeActivity { // All transitions must be true for a join // to take place private Dictionary<string, bool> transitionStatus; // If true, we have hit an exit activity and are // in the process of cancelling other activities private bool exiting; public Graph() : base() { base.SetReadOnlyPropertyValue(Graph.ArcsProperty, new List<Arc>()); } public static readonly DependencyProperty ArcsProperty = DependencyProperty.Register( "Arcs", typeof(List<Arc>), typeof(Graph), new PropertyMetadata(DependencyPropertyOptions.Metadata | DependencyPropertyOptions.ReadOnly, new Attribute[] { new DesignerSerializationVisibilityAttribute( DesignerSerializationVisibility.Content) } ) ); // Attached to exactly one child activity public static readonly DependencyProperty IsEntryProperty = DependencyProperty.RegisterAttached( "IsEntry", typeof(bool), typeof(Graph), new PropertyMetadata(DependencyPropertyOptions.Metadata) ); public static object GetIsEntry(object dependencyObject) { DependencyObject o = dependencyObject as DependencyObject; return o.GetValue(Graph.IsEntryProperty); } public static void SetIsEntry(object dependencyObject, object value) { DependencyObject o = dependencyObject as DependencyObject; o.SetValue(Graph.IsEntryProperty, value); } // Attached to zero or more child activities public static readonly DependencyProperty IsExitProperty = DependencyProperty.RegisterAttached( "IsExit", typeof(bool), typeof(Graph), new PropertyMetadata(DependencyPropertyOptions.Metadata) ); public static object GetIsExit(object dependencyObject) { DependencyObject o = dependencyObject as DependencyObject; return o.GetValue(Graph.IsExitProperty); } public static void SetIsExit(object dependencyObject, object value) { DependencyObject o = dependencyObject as DependencyObject; o.SetValue(Graph.IsExitProperty, value); } [DesignerSerializationVisibility( DesignerSerializationVisibility.Content)] public List<Arc> Arcs { get { return GetValue(ArcsProperty) as List<Arc>; } } protected override void Initialize( IServiceProvider provider) { exiting = false; transitionStatus = new Dictionary<string, bool>(); foreach (Arc arc in this.Arcs) { transitionStatus.Add(arc.name, false); } base.Initialize(provider); } protected override void Uninitialize(IServiceProvider provider) { transitionStatus = null; base.Uninitialize(provider); } protected override ActivityExecutionStatus Execute( ActivityExecutionContext context) { if (EnabledActivities.Count == 0) return ActivityExecutionStatus.Closed; foreach (Activity child in EnabledActivities) { // Graph validation logic ensures one entry activity bool entry = (bool)Graph.GetIsEntry(child); if (entry) { Run(context, child); break; } } return ActivityExecutionStatus.Executing; } private void Run(ActivityExecutionContext context, Activity child) { // we don't necessarily need to dynamically // create an AEC, but let's do it anyway // so we can more easily add looping later ActivityExecutionContextManager manager = context.ExecutionContextManager; ActivityExecutionContext c = manager.CreateExecutionContext(child); c.Activity.Closed += this.ContinueAt; c.ExecuteActivity(c.Activity); } void ContinueAt(object sender, ActivityExecutionStatusChangedEventArgs e) { e.Activity.Closed -= this.ContinueAt; ActivityExecutionContext context = sender as ActivityExecutionContext; // get the name before completing the AEC string completedChildName = e.Activity.Name; bool exitNow = (bool)Graph.GetIsExit(e.Activity); ActivityExecutionContextManager manager = context.ExecutionContextManager; ActivityExecutionContext c = manager.GetExecutionContext(e.Activity); manager.CompleteExecutionContext(c, false); if (exiting || exitNow) { // no executing child activities if (manager.ExecutionContexts.Count == 0) context.CloseActivity(); else if (exitNow) // just completed an exit activity { exiting = true; foreach (ActivityExecutionContext ctx in manager.ExecutionContexts) { if (ctx.Activity.ExecutionStatus == ActivityExecutionStatus.Executing) { ctx.CancelActivity(ctx.Activity); } } } } else { // mark all outgoing transitions as true foreach (Arc arc in this.Arcs) { if (arc.FromActivity.Equals(completedChildName)) this.transitionStatus[arc.name] = true; } foreach (Activity child in EnabledActivities) { bool entry = (bool)Graph.GetIsEntry(child); if (!entry) { // a child activity can run only // if all incoming transitions are true // and it is not the entry activity bool canrun = true; foreach (Arc arc in this.Arcs) { if (arc.ToActivity.Equals(child.Name)) if (transitionStatus[arc.name] == false) canrun = false; } if (canrun) { // when we run a child activity, // mark its incoming transitions as false foreach (Arc arc in this.Arcs) { if (arc.ToActivity.Equals(child.Name)) transitionStatus[arc.name] = false; } Run(context, child); } } } } } // Cancellation logic ... } } | One child activity of the Graph must be marked as the "entry" activitythe activity whose execution is the logical starting point for the set of child activities. Zero or more activities are marked as "exit" activities. If an "exit" activity completes successfully, all pending (still executing) activities elsewhere in the graph are canceled and the graph itself can report its own completion. We can use the Graph activity to write WF programs that look like this (in XAML): <Graph x:Name="graph1" xmlns="http://EssentialWF/Activities" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Graph.Arcs> <Arc FromActivity="A" ToActivity="B" /> <Arc FromActivity="B" ToActivity="C" /> <Arc FromActivity="B" ToActivity="D" /> <Arc FromActivity="C" ToActivity="E" /> <Arc FromActivity="D" ToActivity="E" /> <Arc FromActivity="E" ToActivity="F" /> </Graph.Arcs> <WriteLine x:Name="C" Text="c" /> <WriteLine x:Name="A" Text="a" Graph.IsEntry="True" /> <WriteLine x:Name="E" Text="e" /> <WriteLine x:Name="D" Text="d" /> <WriteLine x:Name="F" Text="f" Graph.IsExit="True" /> <WriteLine x:Name="B" Text="b" /> </Graph> The preceding WF program is a composition of activities that can be visualized as shown in Figure B.1. Figure B.1. A graph-based WF program Each node in the graph is an activity. The activities in the example are all of type WriteLine but in fact there is no restriction at all on the type of child activity that is allowed within a Graphthese activities can be Sequence, WriteLine, Interleave, Read, or any activity that you choose, even another Graph. The order in which the child activities of Graph execute is determined by the arcs that are captured as metadata associated with the Graph. In the preceding example, activity B acts like a split because the path of execution diverges at precisely that point in the program (activities C and D execute subsequently in an interleaved manner). Activity E acts like a join because two paths of execution are merged at precisely that point in the program (activity E does not execute until both C and D are complete). The metadata of the Graph activity is just a set of arcs. The associated Arc type is a straightforward class with two dependency properties that hold the names of the activities connected by an arc: using System; using System.Workflow.ComponentModel; namespace EssentialWF.Activities { public class Arc : DependencyObject { internal string name; public Arc() : base() { name = Guid.NewGuid().ToString(); } public Arc(string from, string to) : this() { this.FromActivity = from; this.ToActivity = to; } public static readonly DependencyProperty FromActivityProperty = DependencyProperty.Register( "FromActivity", typeof(string), typeof(Arc), new PropertyMetadata(DependencyPropertyOptions.Metadata) ); public static readonly DependencyProperty ToActivityProperty = DependencyProperty.Register( "ToActivity", typeof(string), typeof(Arc), new PropertyMetadata(DependencyPropertyOptions.Metadata) ); public string FromActivity { get { return GetValue(FromActivityProperty) as string; } set { SetValue(FromActivityProperty, value); } } public string ToActivity { get { return GetValue(ToActivityProperty) as string; } set { SetValue(ToActivityProperty, value); } } } } The code for the preceding Graph activity is not quite complete; it requires validation logic to ensure that the IsEntry and IsExit properties are used appropriately. It also requires standard cancellation logic, as discussed in Chapter 4. The Graph activity can be enhanced to allow for the association of conditions with the split and join points in the graph, and also to allow loops. |