• No results found

Open the LoggerActivity.cs file in Code view.

Scenario: Logging Workflow Processing

5. Open the LoggerActivity.cs file in Code view.

6. Add the following to the top of the file with the rest of the using statements: using Microsoft.SharePoint.WorkflowActions;.

Part of the functionality of a composite activity is to control the execution of its children (wow, taken out of context, that sentence ends in a very dark, disturbing way). What this means is that not only does our activity need to initiate the execution of each child activity, it also needs to listen for the event fired when the child is finished executing so it can inform the runtime that the child activity is closed. In order to listen for this event, we need to implement an interface: IActivityEventListener. Right now, our class inherits from SequenceActivity,

which is fine. We just need to add the interface information to the end of it. Add the following to the end of the class line, immediately after SequenceActivity:

, IActivityEventListener < ActivityExecutionStatusChangedEventArgs >

Yes, the comma is required.

The next piece of code we need to add is for two dependency properties. We covered dependency properties in the simple activity earlier so I won’t rehash it here. Flip back to the “Adding Custom Properties” section if you need a refresher, but the Code Snippet really makes this a pretty simple operation. The first dependency property is going to be called Condition

and is of type ActivityCondition. This property will be used to determine whether we should write anything to the log. Because this property is of type ActivityCondition, it will be displayed in the Visual Studio Properties window just like Condition properties for other activities—which we’ll look at further in Chapters 6 and 8. As mentioned in the scenario, we are going to be tying this property to a global variable so that workflow builders can control the logging for their entire workflow from a single place.

The next dependency property we need to add is a Boolean flag used to indicate whether our activity is executing. As we’ll see, this will tie into the process of catching the event fired when our child activities finish processing. We’ll call it Running and as mentioned, it is of type

bool.

We’ll see both of these properties in action in just a moment. Next, though, we need to revisit our old friend the Execute method. Remember, it is this method that is called by the workflow host to start our activity processing.

Coding the Activity

Listing 5-19 shows the full contents of our Execute method. As before, line numbers are included for reference only. Here’s a rundown of what happens in the method:

• Lines 3 and 4 set some values so that we can keep track of the process of our activity.

Running is the property we created earlier. ActivityStarted is a local variable used to flag that our activity has started a child activity running. We’ll use it at the end to determine what value to return.

• Line 5 checks the Condition property we set up by calling its Evaluate method. If the condition set by the workflow builder indicates we should do some logging, we’ll continue on with lines 6 through 16. Otherwise, we jump right to line 18 and begin executing our child activities.

• Lines 7–16 loop through our child activities looking for the logging activity, which we actually add in the ToolboxItem class in just a few minutes. In the ToolboxItem class we will set a value in the UserData property so we could identify our activity here.

• Line 18 begins a loop through the child activities of our composite activity that have their

Enabled property set to true. This loop ends on line 27.

• Line 23 is where we set things up to be able to catch the event fired off when our child activity finishes executing. We make use of the generic RegisterForStatusChange

method and pass in a parameter that specifies what event it is that we are looking for—

• Line 24 actually executes our child activity within the context passed in for our activity. • Line 25 sets our ActivityStarted variable so that we know that we have launched at least

one child activity.

• Because they are contained within the loop started on line 18, lines 23–25 execute once for each enabled child activity.

• Line 28 sets up our return value based on whether or not we started up any child activities. If we did, we return a value indicating that our activity is still running. We’ll take care of closing out our activity when we handle the close events for our children. If we did not fire up any child activities, we just pass back a value indicating that our activity is closed and done processing.

Listing 5-19. The Execute Method for Our Composite Activity

1 protected override ActivityExecutionStatus Execute(➥

ActivityExecutionContext executionContext) 2 {

3 this.Running = true;

4 bool ActivityStarted = false;

5 If (this.Condition.Evaluate(this, executionContext)) 6 {

7 for (int i = 0; i < this.Activities.Count; i++) 8 { 9 if ((string)this.Activities[i].UserData["logger"] == "logger") 10 { 11 LogToHistoryListActivity logger = (➥ LogToHistoryListActivity)this.Activities[i]; 12 logger.HistoryDescription = string.Format(@"Begin ➥

13 Activity Execution:{0} with {1} enabled Children", ➥

this.QualifiedName, ➥ this.EnabledActivities.Count.ToString()); 14 break; 15 } 16 } 17 }

18 for (int childNum = 0; childNum < this.EnabledActivities.Count; ➥

childNum++) 19 {

20 Activity child = this.EnabledActivities[childNum] as Activity; 21 if (null != child) 22 { 23 child.RegisterForStatusChange(Activity.ClosedEvent, this); 24 executionContext.ExecuteActivity(child); 25 ActivityStarted = true; 26 } 27 }

28 return ActivityStarted? ActivityExecutionStatus.Executing : ➥

ActivityExecutionStatus.Closed; 29 }

Next up is our event to process our child activities as they close. Listing 5-20 shows the code for this, and here is a rundown of the highlights:

• Line 6 causes our activity to stop listening to events for the particular child that we’re running for now. Since we have already received the Closed event for this child, there is no need to keep listening.

• Line 12 is the real meat of this method. It is contained within another loop through all enabled child activities so it will process for all child activities. It is responsible for checking the execution status of all of the child activities of our activity. If one of those children has a status of anything other than Initialized or Closed, then we can’t close yet. If, however, all of the child activities are in one of those two states, then it is safe to close our activity.

• Line 16 handles the process of notifying our workflow host that we are done processing. It will only execute if the check in line 15 indicates that we are done; all of our child activ- ities are done processing.

Listing 5-20. Listening for Our Child Activities to Finish

1 void IActivityEventListener<ActivityExecutionStatusChangedEventArgs>.➥

OnEvent(object sender, ActivityExecutionStatusChangedEventArgs e) 2 {

3 ActivityExecutionContext context = sender as ➥

ActivityExecutionContext;

4 if (e.ExecutionStatus == ActivityExecutionStatus.Closed) 5 {

6 e.Activity.UnregisterForStatusChange(Activity.ClosedEvent, this); 7 LoggerActivity lgr = context.Activity as LoggerActivity;

8 bool finished = true;

9 for (int childNum = 0; childNum < lgr.EnabledActivities.Count; ➥

childNum++) 10 {

11 Activity child = lgr.EnabledActivities[childNum];

12 if ((child.ExecutionStatus != ActivityExecutionStatus. ➥

Initialized) && (child.ExecutionStatus != ➥

ActivityExecutionStatus.Closed)) 13 finished = false; 14 } 15 if (finished) 16 context.CloseActivity(); 17 } 18 }

The last code for our Activity Definition class is to handle the cancellation of our activity— for any number of reasons. We’ll cover cancellation in Chapter 9, but for now, just know that if our composite activity is canceled, we need to handle canceling all of our child activities. Listing 5-21 shows this code. The code for this method is pretty straightforward. As with the previous two listings, we loop through all of our enabled child activities (lines 4 through 14). Line 7 checks to see whether our current child activity is currently executing. If it is, line 9 cancels it and line 10 sets a local variable that indicates we have just canceled a child activity. Lines 12 through 14 repeat a similar pattern, this time for child activities in a Faulting state. Finally in line 15 we determine our return value. If all of our child activities were closed before we got to them, we can tell our host that we are finished. If we had to signal a child activity to close, we return a value of Canceling.

Listing 5-21. Handling the Potentiality of Canceling Our Children When We Are Canceled

1 protected override ActivityExecutionStatus Cancel(ActivityExecutionContext➥

executionContext) 2 {

3 bool cancelled = true;

4 for (int childNum = 0; childNum < this.EnabledActivities.Count; ➥

childNum++) 5 {

6 Activity child = this.EnabledActivities[childNum];

7 if (child.ExecutionStatus == ActivityExecutionStatus.Executing) 8 { 9 executionContext.CancelActivity(child); 10 cancelled = false; 11 } 12 else if ((child.ExecutionStatus == ➥ ActivityExecutionStatus.Canceling) || (child.ExecutionStatus ➥ == ActivityExecutionStatus.Faulting)) 13 cancelled = false; 14 }

15 return cancelled ? ActivityExecutionStatus.Canceling : ➥

ActivityExecutionStatus.Closed; 16 }

That does it for the Activity Definition class.

Just as with our simple activity earlier, a composite activity is made up of multiple classes. We covered all the standard ones back in the simple activity. We’re going to touch on some of them again here, but only as deep as we need to in order to build our composite activity. If you’re interested in looking at these in further detail, we took care of that earlier in the chapter.

The first “other” class we’re going to look at is the ActivityToolboxItem class. The class file is not listed in the Solution Explorer in Visual Studio so we’re going to have to add it fresh. Add a new class file named LoggerActivityToolboxItem.cs.

The first thing we need to add is some using statements, and then set up our class and a few constructors. Listing 5-22 shows the code for this. There’s nothing overly special about any of this except for the serialization elements, but even that is pretty standard .NET stuff.

Listing 5-22. Setting Up Our Class with Two Available Constructors using System; using System.ComponentModel; using System.ComponentModel.Design; using System.Runtime.Serialization; using System.Workflow.ComponentModel; using System.Workflow.ComponentModel.Design; using Microsoft.SharePoint.WorkflowActions; namespace KCD.Sharepoint.Activities.Composite { [Serializable]

internal class LoggerActivityToolboxItem : ActivityToolboxItem {

public LoggerActivityToolboxItem(Type type): base(type) {

}

private LoggerActivityToolboxItem(SerializationInfo info, ➥

StreamingContext context) {

this.Deserialize(info, context); }

The only other code we need to add to this class is an override of the CreateComponentsCore

method, shown in Listing 5-23. This method is called by Visual Studio when you add an activity to the Designer. It gives us the opportunity to do some processing before our activity is added to a workflow. In our case, we are going to use that to add the LogToHistoryList activity as a child of our activity. We will also add some information to the UserData property so that we can identify it later. We saw the other half of this back on line 9 of Listing 5-19 earlier. At the end, we simply return our activity and Visual Studio goes on its merry way.

Listing 5-23. The CreateComponentsCore Method

protected override IComponent[] CreateComponentsCore(IDesignerHost host) {

System.Workflow.ComponentModel.CompositeActivity activity = ➥

new LoggerActivity();

LogToHistoryListActivity logger = new LogToHistoryListActivity(); logger.UserData["logger"] = "logger";

activity.Activities.Add(logger); return new IComponent[] { activity }; }

The next and last class we need to add to our activity is the Activity Designer class. Unlike the simple activity where we made significant modifications to the look of our activity via the designer class, this time around, we’re not going to change the look at all. Instead, we’re going to make use of some additional functionality exposed via our base class

(SequentialActivityDesigner) to control how our activity manages its child activities as well as the process of new activities being added.

To take care of this, we need to add three methods. First, add another class file to your solution and replace the default contents with the contents of Listing 5-24.

Listing 5-24. Setting Up Our Class

using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Workflow.ComponentModel; using System.Workflow.ComponentModel.Design; namespace KCD.Activities {

public class LoggerActivityDesigner : SequentialActivityDesigner {

Next up, we need to override the first of our methods. This one allows you to control what happens when a workflow builder attempts to drop an activity into ours. In our scenario, we want to make sure that our LogToHistoryList activity is and remains the first child activity. Listing 5-25 shows the code for this, which is surprisingly simple. It is not evident from the code, but the net effect is to not allow users to drop an activity in front of our LogToHistoryList activity. In the Designer, this translates to not showing the workflow builder the small green plus sign indicator that marks valid drop locations for the space in front of our activity—a pretty slick outcome for only one line of code.

Listing 5-25. Keeping Our LogToHistoryList Activity As the First Child

public override bool CanInsertActivities(HitTestInfo insertLocation, ➥

ReadOnlyCollection<Activity> activitiesToInsert) {

return insertLocation.MapToIndex() != 0; }

This takes care of part of the functionality required to keep our LogToHistoryList activity first. There are two other situations we need to take care of—moving activities that have already been added to our activity, and deleting the LogToHistoryList activity. Listing 5-26 is similar to Listing 5-25 and has the same effect—it prevents users from dropping (in this case as part of a move operation) an activity in front of our LogToHistoryList activity. Listing 5-27 is a bit different but still pretty simple. It merely checks whether the activity (or one of the activities, it there are multiple) being deleted is our LogToHistoryList activity. If so, it simply disallows the operation.

Listing 5-26. Controlling Activity Moves

public override bool CanMoveActivities(HitTestInfo moveLocation, ➥

ReadOnlyCollection<Activity> activitiesToMove) {

return moveLocation.MapToIndex() != 0; }

Note

Listing 5-27 hints at some pretty powerful capabilities that are available to us—the ability to restrict the activities our composite activity can contain based on any number of properties or details. For more capa- bilities and scenarios available to you, see the sidebar “Box? What Box?”

Listing 5-27. The Final Piece to Keeping Our LogToHistoryList Activity As the First Child

public override bool CanRemoveActivities(ReadOnlyCollection<Activity> ➥

activitiesToRemove) {

foreach (Activity a in activitiesToRemove) { if ((string)a.UserData["logger"] == "logger") { return false; } } return true; }