image

This chapter focuses on the activities that enable you to work with collections of data. Windows Workflow Foundation (WF) includes a ForEach<T> activity that iterates over each element in a collection, executing the same activity for each element. WF also includes a parallel version of this activity and a set of activities that allow you to manipulate collections, adding and removing elements and so on. After a brief summary of these activities, this chapter presents an example that demonstrates their use.

The standard activities provided with WF are designed to work with simple collections, not dictionaries. To demonstrate one way to work with dictionaries, this chapter presents a set of custom activities. It then presents the original example workflow again using a dictionary instead of a collection.

Finally, the chapter ends with a discussion and demonstration of the InvokeMethod activity. This activity allows you to declaratively invoke an instance or static method.

Understanding the ForEach<T> Activity

The first collection-related activity that I’ll cover is ForEach<T>. Conceptually, it is designed to operate in a similar way to the foreach C# keyword. The foreach keyword allows you to iterate over the elements in a collection, executing the same block of code for each element. In a similar way, the ForEach<T> activity iterates over a collection (defined as either an argument or a variable) and executes a child activity for each element in the collection.

Here are the most important properties of the ForEach<T> activity:

image

To use the generic ForEach<T> activity, you first specify the generic type that identifies the type of elements in the collection. You then set the ForEach.Values property to an argument or variable that is a collection of the correct type. Finally, you add an activity to the ForEach.Body property. This activity will be executed once for each element in the collection. Of course, this activity can be a composite activity like Sequence that contains other child activities to execute.

The activity that you specify for the Body property has access to a named argument that represents the element to process. For more information on the plumbing that WF uses to make this work, please see the “Supplying Arguments with an ActivityAction” sidebar in this chapter.

images Note When the ForEach<T> activity executes, a single instance of the Body activity is constructed. That same instance is used to process all elements in the collection.

WF also includes a ParallelForEach<T> activity, which is used in the same way as ForEach<T> with one big exception. The ForEach<T> activity processes one element from the collection at a time. It schedules execution of the activity specified in the ForEach.Body property for each element. Execution of the Body activity isn’t scheduled for the next element until execution for the first element has completed.

In contrast with this, the ParallelForEach<T> activity immediately processes all the elements in the collection by scheduling execution of the Body activity for each of them. This means that depending on the type of work done by the Body activity, you may experience simultaneous execution of the Body activity.

The following are the most important properties of the ParallelForEach<T> activity:

image

The ParallelForEach<T> activity also includes an optional CompletionCondition property. Normally, the ParallelForEach<T> activity completes when all elements in the collection have been processed. By supplying a Boolean condition for this property, you can short-circuit the normal completion logic. If defined, this condition is evaluated each time the Body activity is completed for one of the elements. If the condition evaluates to true, no additional processing takes place for the other elements. Any work that has already been scheduled is canceled.

Supplying Arguments with an ActivityAction

You will see the ForEach<T> and ParallelForEach<T> activities in action later in the chapter. But before I can present the first example, you need to learn about the other collection-related activities.

Understanding the Collection Activities

The ForEach<T> and ParallelForEach<T> activities allow you to iterate over the elements in a collection. WF also includes a set of standard activities that enable you to perform common operations on a collection. Here is a quick recap of the available activities and their purpose:

image

All of these activities are generics, requiring you to specify the type of object that is contained within the collection. These activities reference the target collection using the Collection property. This property is typed as InArgument<ICollection<T>>; therefore, any collection that implements the ICollection<T> generic interface can be used. The collection that you reference can be a variable or argument.

Here are the most important properties of the AddToCollection<T> activity:

image

The AddToCollection.Item property is used to supply the new element that you want to add to the collection. It can reference a variable or argument, or you can construct the new object directly in an expression.

The RemoveFromCollection<T> activity supports a similar set of properties:

image

In addition to the Collection and Item properties, the RemoveFromCollection activity also includes a Boolean Result property. This property can be checked to determine whether the remove operation was successful.

The ExistsInCollection<T> doesn’t update the collection but is instead used to determine whether an element exists in the collection. These are the most important properties supported by the ExistsInCollection activity:

image

Finally, the ClearCollection<T> activity can be used to remove all elements from a collection. Here is the most important property for this activity:

image

images Tip Missing from this set of activities is the ability to find and retrieve an existing element from a collection. The ExistsInCollection<T> activity can be used to determine whether an element exists in the collection, but it doesn’t provide a way to retrieve the element when it does exist. This may not be important if you are working with a collection of simple intrinsic types, but it seems like a glaring omission if you are working with more complex types and need the ability to retrieve or update an existing object in a collection. To solve this, you will implement a custom FindInCollection<T> activity in the example that follows.

These activities make it very easy to declaratively work with collections. However, be aware of the potential performance implications of using activities such as ExistsInCollection<T> and RemoveFromCollection<T>. The actual mechanism used to locate the specified element in the collection depends on the implementation of the particular collection. Of course, these same performance concerns also apply when you are working with collections directly in code. For example, if your collection is a List<T>, a default comparer is used to locate the element in the collection. This will likely mean iterating over the entire collection to find the element that you want to process. In a very large collection, that may result in a performance penalty.

The examples that are presented in the next few sections demonstrate how to use several of these collection-related activities.

Using the ForEach<T> and Collection Activities

The example that follows demonstrates the use of the ForEach<T> activity along with several of the collection-related activities that were just discussed. The scenario for this example is an inventory update workflow. The workflow is passed two collections as arguments. The first collection contains the available inventory for several items and is the collection that is updated by the workflow. The second collection contains individual item sales. The quantity for each item sold is used to reduce the available inventory for that item.

The workflow also handles the peculiar situation where an item has been sold but is not in the collection representing the available inventory. In cases like this, the updated inventory is a negative amount.

Here are the steps that you will follow to implement this example:

  1. Implement simple C# classes to represent the item inventory and sales history.
  2. Implement a new custom activity to locate and retrieve an element in a collection.
  3. Declare a workflow to update a collection of item inventory elements.
  4. Host and test the workflow.

Creating the ActivityLibrary Project

To begin this example, create a new project named ActivityLibrary using the Activity Library template. Add it to a new solution that is named for this chapter. You can delete the Activity1.xaml file since it won’t be needed. This project will be used throughout this chapter and will house several custom activities as well as classes to define the item inventory and sales structures.

Implementing Item Structures

Add a new C# class to the ActivityLibrary project, and name it ItemInventory. This should be a normal C# class, not a workflow activity or class. This class defines the item inventory structure for a single sales item. A collection of these objects will be updated by the example workflow. Here is the code that you need for this class:

using System;

namespace ActivityLibrary
{
    public class ItemInventory : IEquatable<ItemInventory>
    {
        public Int32 ItemId { get; set; }
        public Int32 QuantityOnHand { get; set; }

        public bool Equals(ItemInventory other)
        {
            if (other == null)
            {
                return false;
            }
            else
            {
                return (this.ItemId == other.ItemId);
            }
        }
    }
}

I’ve chosen to have this class implement the IEquatable interface. This interface represents a type-safe way to check for the equality of two objects of the same type. If an object implements this interface, the Equals method that is defined by the interface is used by the collection-related activities to determine object equality.

If you don’t provide this interface or override the default Object.Equals method, the default behavior is a reference equality check (both objects referencing the same instance). For this example, you need the ability to determine whether two of these objects are the same based on the value of their ItemId.

You also need to implement a class that defines the individual sales that are applied to the inventory collection. Add another C# class to the same project, and name it SalesHistory. Here is the code you need for this class:

using System;

namespace ActivityLibrary
{
    public class SalesHistory
    {
        public Int32 ItemId { get; set; }
        public Int32 Quantity { get; set; }
    }
}

Implementing the FindInCollection<T> Activity

This example requires the ability to find and update an existing ItemInventory object in a collection. You can use the ExistsInCollection<T> activity to determine whether the object is in the collection, but it doesn’t provide a way to retrieve the object when it does exist. To remedy this situation, you will implement a custom activity that locates a requested object and returns it as a output argument that can be assigned to a workflow variable.

Add a new custom activity to the ActivityLibrary project, and name it FindInCollection. This is a code-based activity, so use the Code Activity new item template. Here is the code for this new activity:

using System;
using System.Activities;
using System.Collections.Generic;

namespace ActivityLibrary
{
    public class FindInCollection<T> : CodeActivity<Boolean>
    {

I follow the pattern established by the standard collection activities and define properties named Collection and Item. The Item property is the element that you want to find in the collection. The FoundItem property is an output argument that will reference the element that was found in the collection. The activity uses the generic form of CodeActivity as its base, so it also supports a Boolean Result property. This property is set to true if the requested element is found in the collection.

Note that I’ve added the RequiredArgument attribute to two of the properties. This presents an error to the developer if they fail to set values for these properties in the workflow designer.

        [RequiredArgument]
        public InArgument<ICollection<T>> Collection { get; set; }
        [RequiredArgument]
        public InArgument<T> Item { get; set; }
        public OutArgument<T> FoundItem { get; set; }

        protected override Boolean Execute(CodeActivityContext context)
        {
            Boolean result = false;
            FoundItem.Set(context, default(T));
            ICollection<T> collection = Collection.Get(context);
            T item = Item.Get(context);

            if (collection != null)
            {
                foreach (T entry in collection)
                {

The check for equality first determines whether the object implements the IEquatable interface. If it does, it calls the IEquatable.Equals method defined by that interface. If not, the standard Equals method that is defined by the Object class is invoked to test equality.

                    if (entry is IEquatable<T>)
                    {
                        if (((IEquatable<T>)entry).Equals(item))
                        {
                            FoundItem.Set(context, entry);
                            result = true;
                            break;
                        }
                    }
                    else if (entry.Equals(item))
                    {
                        FoundItem.Set(context, entry);
                        result = true;
                        break;
                    }
                }
            }
            return result;
        }
    }
}

If you haven’t done so already, you should build the ActivityLibrary project to ensure that everything builds correctly and that this activity is made available in the Toolbox.

Declaring the InventoryUpdate Workflow

Add a new project named InventoryUpdate to the solution using the Workflow Console Application template. You can delete the Workflow1.xaml file that was generated for you since it won’t be needed. Add a project reference to the ActivityLibrary project that should be in the same solution. You will be using a number of types that are defined in the ActivityLibrary namespace, so you might want to add this namespace to the Imports list for the workflow. Doing this avoids the need to fully qualify the types contained in this namespace.

Add a new workflow named InventoryUpdate to the project using the Activity template. This workflow will process two collections that are passed as arguments. The ArgSales argument is a collection of SalesHistory objects representing new sales that should be used to reduce the available inventory. The ArgInventory collection represents the available inventory for multiple items. Start the workflow declaration by adding these arguments to the workflow:

image

You can follow these steps to declare the remainder of workflow:

  1. Add a Sequence activity to the empty workflow, and then add a ForEach<T> activity to the Sequence activity. Select ActivityLibrary.ItemInventory as the generic type for this activity. The purpose of this activity is to display the starting values for the inventory collection, so change the DisplayName property to PrintInventory. Set the ForEach.Values property to ArgInventory. Note that the default name of item will be used for the argument that represents each element in the collection. This argument can be referenced by any child activities.
  2. Add a WriteLine as the child of the ForEach activity, and set its Text property to String.Format("Item {0} beginning inventory: {1}", item.ItemId, item.QuantityOnHand). Figure 6-1 shows the completed ForEach activity.
    images

    Figure 6-1. PrintInventory activity

  3. Add another ForEach<T> activity to the Sequence activity, directly under the first ForEach activity (PrintInventory). This is the main ForEach<T> activity that will process updates to the inventory. Set the generic type to ActivityLibrary.SalesHistory. Set the ForEach.Values property to ArgSales.
  4. Add a Sequence activity as the child of the ForEach. Add a Boolean variable to the Sequence activity that you just added, and name it IsItemExists.
  5. Add a WriteLine activity to the Sequence activity (the one that is the child of the ForEach<T> activity) to display the individual sales transactions as they are processed. Set the Text property to String.Format("Sales item: {0} quantity: {1}", item.ItemId, item.Quantity).
  6. Add an ExistsInCollection<T> activity under the WriteLine activity. Set the generic type to ActivityLibrary.ItemInventory. Set the Collection property to ArgInventory, the Result property to IsItemExists, and the Item property to this expression: New ActivityLibrary.ItemInventory With {.ItemId = item.ItemId}. This expression creates a new ItemInventory object using the ItemId from the current sales element. The new object is necessary only because an object of this type must be passed to the activity to determine whether the object already exists in the collection.
  7. Add an If activity directly under the ExistsInCollection<T> activity. Set the Condition property to IsItemExists. Add Sequence activities to the If.Then and If.Else properties. Change the DisplayName of the If.Then Sequence activity to ExistsSequence and the DisplayName of the If.Else Sequence activity to NotExistsSequence to make it easier to distinguish these activities from others of the same type.
  8. Add a new variable to the ExistsSequence activity named FoundItem with a type of ActivityLibrary.ItemInventory. This variable will reference the existing element in the collection that has been found.
  9. Add an instance of the custom FindInCollection<T> activity to the ExistsSequence activity. Set the generic type to ActivityLibrary.ItemInventory. Set the Collection property to ArgInventory, the FoundItem property to FoundItem, and the Item property to New ActivityLibrary.ItemInventory() With {.ItemId = item.ItemId}.
  10. Add an Assign activity directly below the FindInCollection<T> activity. This activity will reduce the inventory of the existing element that was just found. Set the Assign.To property to FoundItem.QuantityOnHand and the Assign.Value property to FoundItem.QuantityOnHand - item.Quantity.
  11. Navigate to the NotExistsSequence activity that was added to the If.Else property. Add an AddToCollection<T> activity to this Sequence activity. Set the generic type to ActivityLibrary.ItemInventory. Set the Collection property to ArgInventory and the Item property to New ActivityLibrary.ItemInventory() With {.ItemId = item.ItemId, .QuantityOnHand = (0 - item.Quantity)}. This adds a new ItemInventory object to the collection with a negative available quantity.

Figure 6-2 shows the child Sequence activity of the ForEach<T> activity, while Figure 6-3 shows the main ForEach<T> activity. Figure 6-4 is a top-level view of the entire workflow.

images

Figure 6-2. ForEach Sequence activity

images

Figure 6-3. ForEach activity

images

Figure 6-4. InventoryUpdate workflow

Hosting the Workflow

To host the workflow, open the Program.cs file in the InventoryUpdate project, and modify it to look like this:

using System;
using System.Activities;
using System.Collections.Generic;
using ActivityLibrary;

namespace InventoryUpdate
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Test InventoryUpdate...");
            RunWorkflow(new InventoryUpdate());

            Console.WriteLine("Press any key to exit");
            Console.ReadKey();
        }

The code to run the workflow creates two collections, one containing the available inventory by item and another containing individual item sales. Note that the sales history contains multiple sales for two of the items (ItemId 100 and 300). The final inventory for these two items should be reduced by all the sales history objects for each item. Also note that ItemId 300 doesn’t currently exist in the collection of available inventory. Therefore, any sales posted against that item should result in a negative amount for the available inventory. Both of these collections are passed as arguments to the workflow.

        private static void RunWorkflow(Activity workflow)
        {
            List<SalesHistory> salesHist = new List<SalesHistory>
            {
                new SalesHistory{ItemId = 100, Quantity = 5},
                new SalesHistory{ItemId = 200, Quantity = 25},
                new SalesHistory{ItemId = 100, Quantity = 7},
                new SalesHistory{ItemId = 300, Quantity = 75},
                new SalesHistory{ItemId = 100, Quantity = 30},
                new SalesHistory{ItemId = 300, Quantity = 26},
            };

            List<ItemInventory> inventory = new List<ItemInventory>
            {
                new ItemInventory{ItemId = 100, QuantityOnHand = 100},
                new ItemInventory{ItemId = 200, QuantityOnHand = 200},
            };

            WorkflowInvoker.Invoke(workflow,
                new Dictionary<string, object>
                {
                    {"ArgSales", salesHist},
                    {"ArgInventory", inventory}
                });

After running the workflow, the ending item inventory is displayed in order to determine whether the workflow applied the updates correctly.

            foreach (ItemInventory item in inventory)
            {
                Console.WriteLine("Item {0} ending inventory: {1}",
                    item.ItemId, item.QuantityOnHand);
            }
        }
    }
}

Testing the Workflow

You should now be able to build the solution and run the InventoryUpdate project. Here are my results when I run this project:


Test InventoryUpdate...

Item 100 beginning inventory: 100

Item 200 beginning inventory: 200

Sales item: 100 quantity: 5

Sales item: 200 quantity: 25

Sales item: 100 quantity: 7

Sales item: 300 quantity: 75

Sales item: 100 quantity: 30

Sales item: 300 quantity: 26

Item 100 ending inventory: 58

Item 200 ending inventory: 175

Item 300 ending inventory: -101

Press any key to exit

The results indicate that the updates were correctly applied to the available inventory. Since the inventory collection did not originally contain an object for item 300, a new instance was added with a negative inventory value.

Using the ParallelForEach<T> Activity

The ParallelForEach<T> activity works in a similar way as the ForEach<T> activity and supports the same properties. Therefore, the easiest way to see the ParallelForEach<T> activity in action is to modify the InventoryUpdate workflow from the previous example.

images Note The instructions for this example assume that you are modifying the workflow from the previous example. If you prefer being able to compare the results from the two workflows side by side, you can make a copy of the InventoryUpdate workflow, rename it, and modify the copy.

Follow these steps to modify the InventoryUpdate workflow:

  1. Open the InventoryUpdate.xaml file in Code View. This should open the file in the XML editor instead of the workflow designer. This file is located in the InventoryUpdate project.
  2. The top level of this workflow contains two ForEach<T> activities. The first one displays the beginning inventory, and the second one processes the sales history and applies it to the inventory. Locate each ForEach<T> activity under the root Sequence activity, and change the activity name to ParallelForEach. Make sure you also change the closing element name for each activity.

Here is an abbreviated copy of the revised Xaml file showing the location of the ParallelForEach<T> activities:

<Activity>

  <Sequence>
    <ParallelForEach x:TypeArguments="a:ItemInventory"
        DisplayName="PrintInventory" Values="[ArgInventory]">

    </ParallelForEach>
    <ParallelForEach x:TypeArguments="a:SalesHistory"
        DisplayName="ParallelForEach&lt;SalesHistory&gt;" Values="[ArgSales]">

    </ParallelForEach>
  </Sequence>
</Activity>

If you close the workflow in the XML editor and open it in the designer, it should look like Figure 6-5.

images

Figure 6-5. InventoryUpdate workflow using ParallelForEach

Testing the Revised Workflow

After building the solution, you should be ready to run the InventoryUpdate project. Here are the results that I see when the ParallelForEach activity is used:


Test InventoryUpdate...

Item 200 beginning inventory: 200

Item 100 beginning inventory: 100

Sales item: 300 quantity: 26

Sales item: 100 quantity: 30

Sales item: 300 quantity: 75

Sales item: 100 quantity: 7

Sales item: 200 quantity: 25

Sales item: 100 quantity: 5

Item 100 ending inventory: 58

Item 200 ending inventory: 175

Item 300 ending inventory: -101

Press any key to exit

The only difference between these results and those from the original version of the workflow is the sequence of the "beginning inventory" and "Sales item" lines. The difference in sequence is because of the way the ParallelForEach<T> activity immediately schedules execution of the child activity for all elements in the collection. The ForEach<T> activity schedules execution for the next element only after the current one has finished processing.

images Tip Remember that the actual sequence in which the children of parallel activities are executed greatly depends on the type of work that they perform. In this case, none of the work was asynchronous or would have otherwise caused the workflow to become idle. If this was not the case, the actual execution sequence would have been much different. Please refer to the discussion of the Parallel activity in Chapter 5 for more information.

Working with Dictionaries

The standard WF activities provide good support for working with simple collections. However, they don’t directly address the more specialized needs when you are working with dictionaries. The primary reason you would use a dictionary instead of a simple collection is the requirement to quickly locate any particular element based on its unique key. Of course, you can search for a particular element in a simple collection, but that search is likely implemented by iterating through all the elements. That can quickly become a performance problem if the collection contains thousands of elements.

Fortunately, it is fairly easy to implement a set of custom activities that work with dictionaries instead of simple collections. In this section, I present a set of custom activities that work in a similar way as the standard collection-related activities. After implementing the activities, I present a revised version of the InventoryUpdate workflow that uses a dictionary.

images Tip The goal of this section is not to present a set of production-ready dictionary-related activities. I’m sure you will be able to easily improve on my code. These activities are presented to once again drive home the point that you should create your own custom activities. You should never feel that you are limited to using only the out-of-the-box activities provided with WF. If Microsoft hasn’t provided an activity that meets your needs, just build it yourself.

Here are the steps that you will follow to implement this example:

  1. Implement a set of custom dictionary-related activities.
  2. Declare a workflow to update a dictionary of item inventory elements instead of a simple collection.
  3. Host and test the workflow.

images Note The downloadable code for this book includes a set of simple unit tests for these custom activities. I’ve omitted those tests from the book to keep the focus of this chapter on implementing and using the activities.

Implementing the Dictionary-Related Activities

Here is a quick recap of the custom dictionary-related activities that you will implement:

image

You will add all of these activities to the ActivityLibrary project that you created earlier in the chapter. They are all implemented in code, so you should use the Code Activity add item template when adding them to the ActivityLibrary project. Since the activities are very similar to each other, you’ll find that copying part of the code from the first activity is a great way to implement the others.

Implementing AddToDictionary<TKey, TValue>

Here is the code to implement the AddToDictionary<TKey, TValue> activity:

using System;
using System.Activities;
using System.Collections.Generic;

namespace ActivityLibrary
{
    public class AddToDictionary<TKey, TValue> : CodeActivity
    {
        [RequiredArgument]
        public InArgument<IDictionary<TKey, TValue>> Dictionary { get; set; }
        [RequiredArgument]
        public InArgument<TKey> Key { get; set; }
        [RequiredArgument]
        public InArgument<TValue> Item { get; set; }

        protected override void Execute(CodeActivityContext context)
        {
            IDictionary<TKey, TValue> dictionary = Dictionary.Get(context);
            TKey key = Key.Get(context);
            TValue item = Item.Get(context);
            if (dictionary != null)
            {
                dictionary.Add(key, item);
            }
        }
    }
}

Notice that in addition to the Item argument, which was also implemented in the standard collection-related activities, a Key argument is included. This is needed since a dictionary requires you to set a unique key for each element. You’ll see this additional argument in most of these custom activities.

Implementing RemoveFromDictionary<TKey, TValue>

Here is the code to implement the RemoveFromDictionary<TKey, TValue> activity:

using System;
using System.Activities;
using System.Collections.Generic;

namespace ActivityLibrary
{
    public class RemoveFromDictionary<TKey, TValue> : CodeActivity<Boolean>
    {
        [RequiredArgument]
        public InArgument<IDictionary<TKey, TValue>> Dictionary { get; set; }
        [RequiredArgument]
        public InArgument<TKey> Key { get; set; }

        protected override Boolean Execute(CodeActivityContext context)
        {
            Boolean result = false;
            IDictionary<TKey, TValue> dictionary = Dictionary.Get(context);
            TKey key = Key.Get(context);
            if (dictionary != null)
            {
                if (dictionary.ContainsKey(key))
                {
                    result = dictionary.Remove(key);
                }
            }
            return result;
        }
    }
}
Implementing ExistsInDictionary<TKey, TValue>

Here is the code to implement the ExistsInDictionary<TKey, TValue> activity:

using System;
using System.Activities;
using System.Collections.Generic;

namespace ActivityLibrary
{
    public class ExistsInDictionary<TKey, TValue> : CodeActivity<Boolean>
    {
        [RequiredArgument]
        public InArgument<IDictionary<TKey, TValue>> Dictionary { get; set; }
        [RequiredArgument]
        public InArgument<TKey> Key { get; set; }

        protected override Boolean Execute(CodeActivityContext context)
        {
            Boolean result = false;
            IDictionary<TKey, TValue> dictionary = Dictionary.Get(context);
            TKey key = Key.Get(context);
            if (dictionary != null)
            {
                result = dictionary.ContainsKey(key);
            }
            return result;
        }
    }
}
Implementing FindInDictionary<TKey, TValue>

Here is the code to implement the FindInDictionary<TKey, TValue> activity:

namespace ActivityLibrary
{
    using System;
    using System.Activities;
    using System.Collections.Generic;

    public class FindInDictionary<TKey, TValue> : CodeActivity<Boolean>
    {
        [RequiredArgument]
        public InArgument<IDictionary<TKey, TValue>> Dictionary { get; set; }
        [RequiredArgument]
        public InArgument<TKey> Key { get; set; }
        public OutArgument<TValue> FoundItem { get; set; }

        protected override Boolean Execute(CodeActivityContext context)
        {
            Boolean result = false;
            FoundItem.Set(context, default(TValue));
            IDictionary<TKey, TValue> dictionary = Dictionary.Get(context);
            TKey key = Key.Get(context);
            if (dictionary != null)
            {
                if (dictionary.ContainsKey(key))
                {
                    FoundItem.Set(context, dictionary[key]);
                    result = true;
                }
            }
            return result;
        }
    }
}
Implementing ClearDictionary<TKey, TValue>

Here is the code to implement the ClearDictionary<TKey, TValue> activity:

using System;
using System.Activities;
using System.Collections.Generic;

namespace ActivityLibrary
{
    public class ClearDictionary<TKey, TValue> : CodeActivity
    {
        [RequiredArgument]
        public InArgument<IDictionary<TKey, TValue>> Dictionary { get; set; }

        protected override void Execute(CodeActivityContext context)
        {
            IDictionary<TKey, TValue> dictionary = Dictionary.Get(context);
            if (dictionary != null)
            {
                dictionary.Clear();
            }
        }
    }
}

images Note Since a dictionary is technically also a collection, you can use the standard ClearCollection<T> activity to clear it. However, I’ve included the ClearDictionary<TKey, TValue> activity to round out a uniform set of dictionary-related activities. As an alternate implementation of the ClearDictionary<TKey, TValue> activity, you could use composition to wrap the ClearCollection<T> activity.

Declaring the InventoryUpdateDictionary Workflow

After building the solution to ensure that everything builds correctly, you are ready to implement a workflow that uses these new activities. The structure of this workflow will be similar to the InventoryWorkflow that you declared earlier in the chapter, and many of the steps needed to declare the workflow will be the same. The difference is that the collection of ItemInventory objects has been replaced with a dictionary of these objects. The collection-related activities that were previously used to manipulate this collection have now been replaced with the new dictionary-related activities that you just implemented. Most of the dictionary-related activities require an additional Key argument.

Instead of creating a new project, add a new workflow named InventoryUpdateDictionary to the existing InventoryUpdate project using the Activity add item template. Add these arguments to the workflow:

image

Follow these steps to declare the remainder of workflow:

  1. Add a Sequence activity to the empty workflow, and then add a ForEach<T> activity to the Sequence activity. Select ActivityLibrary.ItemInventory as the generic type for this activity. Change the DisplayName property to PrintInventory. Set the ForEach.Values property to ArgInventory.Values. Notice that you are iterating over the Values property of the ArgInventory dictionary instead of the dictionary itself. The Values property of a dictionary is exposed as a simple collection.
  2. Add a WriteLine as the child of the ForEach activity, and set its Text property to String.Format("Item {0} beginning inventory: {1}", item.ItemId, item.QuantityOnHand). The PrintInventory activity should look just like the one that you declared for the previous example that is shown in Figure 6-1.
  3. Add another ForEach<T> activity to the Sequence activity, directly under the first ForEach<T> activity. This is the main ForEach<T> activity that will process updates to the inventory. Set the generic type to ActivityLibrary.SalesHistory. Set the ForEach.Values property to ArgSales.
  4. Add a Sequence activity as the child of the ForEach<T>. Add a Boolean variable to the Sequence activity named IsItemExists.
  5. Add a WriteLine activity to the Sequence activity to display the individual sales transactions as they are processed. Set the Text property to String.Format("Sales item: {0} quantity: {1}", item.ItemId, item.Quantity).
  6. Add an ExistsInDictionary<TKey,TValue> activity under the WriteLine activity. Set the generic types to Int32 and ActivityLibrary.ItemInventory. Set the Result property to IsItemExists, the Dictionary property to ArgInventory, and the Key property to item.ItemId. This passes the ItemId of the current sales element to the activity for direct lookup based on the unique key.  
  7. Add an If activity directly under the ExistsInDictionary<TKey,TValue> activity. Set the Condition property to IsItemExists. Add Sequence activities to the If.Then and If.Else properties. Change the DisplayName of the If.Then Sequence activity to ExistsSequence and the DisplayName of the If.Else Sequence to NotExistsSequence.
  8. Add a new variable to the ExistsSequence activity with a name of FoundItem and a type of ActivityLibrary.ItemInventory. This variable will reference the existing element in the collection that has been found.
  9. Add an instance of the FindInDictionary<TKey,TValue> activity to the ExistsSequence. Set the generic types to Int32 and ActivityLibrary.ItemInventory. Set the Dictionary property to ArgInventory, the FoundItem property to FoundItem, and the Key property to item.ItemId.
  10. Add an Assign activity directly below the FindInDictionary<TKey,TValue> activity. This activity will update the inventory of the existing element that was just found. Set the Assign.To property to FoundItem.QuantityOnHand and the Assign.Value property to FoundItem.QuantityOnHand - item.Quantity.
  11. Navigate to the NotExistsSequence activity that was added to the If.Else property. Add a generic AddToDictionary<TKey,TValue> activity to the Sequence activity. Set the generic types to Int32 and ActivityLibrary.ItemInventory. Set the Dictionary property to ArgInventory, the Key property to item.ItemId, and the Item property to New ActivityLibrary.ItemInventory() With {.ItemId = item.ItemId, .QuantityOnHand = (0 - item.Quantity)}.

Hosting the Workflow

To host this new workflow, you can revise the Program.cs file in the InventoryUpdate project. Instead of changing the existing code that executes the InventoryUpdate workflow, you can add a method that runs the new InventoryUpdateDictionary workflow and passes it a dictionary argument instead of a simple collection. Here is the revised code for the Program.cs file:

namespace InventoryUpdate
{
    class Program
    {
        static void Main(string[] args)
        {

Add these lines to execute the new workflow after the existing call to the RunWorkflow method:

            Console.WriteLine(" Test InventoryUpdateDictionary...");
            RunDictionaryWorkflow(new InventoryUpdateDictionary());

        }

Add this new method to run the new workflow that uses a dictionary instead of a collection. You can copy the existing RunWorkflow method and change the definition of the inventory variable to a dictionary.

        private static void RunDictionaryWorkflow(Activity workflow)
        {
            List<SalesHistory> salesHist = new List<SalesHistory>
            {
                new SalesHistory{ItemId = 100, Quantity = 5},
                new SalesHistory{ItemId = 200, Quantity = 25},
                new SalesHistory{ItemId = 100, Quantity = 7},
                new SalesHistory{ItemId = 300, Quantity = 75},
                new SalesHistory{ItemId = 100, Quantity = 30},
                new SalesHistory{ItemId = 300, Quantity = 26},
            };

            Dictionary<Int32, ItemInventory> inventory
                = new Dictionary<int, ItemInventory>
            {
                { 100, new ItemInventory{ItemId = 100, QuantityOnHand = 100}},
                { 200, new ItemInventory{ItemId = 200, QuantityOnHand = 200}},
            };

            WorkflowInvoker.Invoke(workflow,
                new Dictionary<string, object>
                {
                    {"ArgSales", salesHist},
                    {"ArgInventory", inventory}
                });

            foreach (ItemInventory item in inventory.Values)
            {
                Console.WriteLine("Item {0} ending inventory: {1}",
                    item.ItemId, item.QuantityOnHand);
            }
        }
    }
}

Testing the Workflow

After building the solution, you can run the InventoryUpdate project. It will first execute the current version of the InventoryUpdate workflow (which should be the one that uses the ParallelForEach<T> activity), followed by the new InventoryUpdateDictionary workflow. Here are the results that I see when I run the revised project:


Test InventoryUpdate...

Item 200 beginning inventory: 200

Item 100 beginning inventory: 100

Sales item: 300 quantity: 26

Sales item: 100 quantity: 30

Sales item: 300 quantity: 75

Sales item: 100 quantity: 7

Sales item: 200 quantity: 25

Sales item: 100 quantity: 5

Item 100 ending inventory: 58

Item 200 ending inventory: 175

Item 300 ending inventory: -101


Test InventoryUpdateDictionary...

Item 100 beginning inventory: 100

Item 200 beginning inventory: 200

Sales item: 100 quantity: 5

Sales item: 200 quantity: 25

Sales item: 100 quantity: 7

Sales item: 300 quantity: 75

Sales item: 100 quantity: 30

Sales item: 300 quantity: 26

Item 100 ending inventory: 58

Item 200 ending inventory: 175

Item 300 ending inventory: -101

Press any key to exit

Understanding the InvokeMethod Activity

In the previous examples, the updates to the item inventory were accomplished using an expression that was entered in an Assign activity like this: FoundItem.QuantityOnHand - item.Quantity. Although this is a fine solution, WF also supports other mechanisms to accomplish the same goal. For example, WF allows you to declaratively invoke a public method on an object using the InvokeMethod activity. You can also use this activity to invoke a public static method that is defined for a type.

images Note The InvokeMethod activity isn’t directly related to the processing of data in collections and, as such, doesn’t really follow the theme of this chapter. However, the examples in this chapter present an opportunity for improvement by using this activity, so I’ve presented this activity here.

Here are the most important properties of the InvokeMethod activity:

image

The properties that you set for the InvokeMethod activity vary depending on whether you are invoking an instance or static method. If you are invoking an instance method, you must set the TargetObject property and the MethodName. If you are invoking a static method, you must set the TargetType and the MethodName. The TargetObject must not be set when you are invoking a static method. In either case, you set values for the arguments that are passed to the method using the Parameters property. Method arguments must be added to the Parameters collection in the same order as they are defined in the method signature. If the method is a generic method, you also need to identify types for each generic type defined by the method.

Using the InvokeMethod Activity

To demonstrate the InvokeMethod activity, you will revise the InventoryUpdateDictionary workflow that you declared in the previous example. Instead of reducing the inventory for an item using an expression, you will invoke a new method that you add to the ItemInventory class. And instead of creating a new ItemInventory object in an expression, you will invoke a new static factory method to create the object.

Revising the ItemInventory Class

Before you can invoke the new methods, they must be added to the ItemInventory class that you previously implemented (located in the ActivityLibrary project). Here are the additional methods that you should add to this class:

using System;

namespace ActivityLibrary
{
    public class ItemInventory : IEquatable<ItemInventory>
    {

        public void ReduceInventory(Int32 adjustment)
        {
            QuantityOnHand -= adjustment;
        }

        public static ItemInventory Create(Int32 itemId, Int32 quantity)
        {
            return new ItemInventory
            {
                ItemId = itemId,
                QuantityOnHand = quantity
            };
        }
    }
}

Build the solution before proceeding with the next steps.

Modifying the Workflow

Open the InventoryUpdateDictionary workflow in the designer, and follow these steps:

  1. Navigate to the If activity that determines whether an existing inventory element should be updated or a new one is added.
  2. Navigate to the Sequence activity under the If.Then property (named ExistsSequence). Delete the Assign activity that immediately follows the FindInDictionary activity since it is no longer needed.
  3. Add an InvokeMethod activity where the Assign activity was located. Set the TargetObject property to FoundItem and the MethodName property to ReduceInventory. Add a single InArgument to the Parameters collection, using a type of Int32 and a value of item.Quantity. Figure 6-6 shows the revised ExistsSequence activity with the InvokeMethod activity.
    images

    Figure 6-6. Revised ExistsSequence

  4. Navigate to the NotExistsSequence activity under the If.Else property. Add a new variable to this Sequence activity. Name the variable NewItem with a type of ActivityLibrary.ItemInventory.
  5. Add an InvokeMethod activity as the first activity in the Sequence activity. Set the TargetType to ActivityLibrary.ItemInventory and the MethodName to Create. Set the Result property to NewItem. Add two InArguments to the Parameters property. The first argument is an Int32 and has a value of item.ItemId. The second argument is also an Int32 and has a value of 0 - item.Quantity.
  6. Make one change to the properties of the AddToDictionary activity. Set the expression for the Item property to NewItem. This will add the ItemInventory object that was constructed by the InvokeMethod activity to the collection. Figure 6-7 shows the revised NotExistsSequence with the InvokeMethod activity.
    images

    Figure 6-7. Revised NotExistsSequence

Testing the Workflow

After rebuilding the solution, you should be ready to test the revised workflow. When I run the InventoryUpdate project, I see the same results as before:


Test InventoryUpdate...

Item 200 beginning inventory: 200

Item 100 beginning inventory: 100

Sales item: 300 quantity: 26

Sales item: 100 quantity: 30

Sales item: 300 quantity: 75

Sales item: 100 quantity: 7

Sales item: 200 quantity: 25

Sales item: 100 quantity: 5

Item 100 ending inventory: 58

Item 200 ending inventory: 175

Item 300 ending inventory: -101


Test InventoryUpdateDictionary...

Item 100 beginning inventory: 100

Item 200 beginning inventory: 200

Sales item: 100 quantity: 5

Sales item: 200 quantity: 25

Sales item: 100 quantity: 7

Sales item: 300 quantity: 75

Sales item: 100 quantity: 30

Sales item: 300 quantity: 26

Item 100 ending inventory: 58

Item 200 ending inventory: 175

Item 300 ending inventory: -101

Press any key to exit

In this particular case, I think the use of the InvokeMethod activity makes a lot of sense. This is especially true when creating a new ItemInventory instance. Although you can construct a simple object like this using an expression, it seems more intuitive to implement a factory method to create new object instances. Doing so allows you to hide any initialization details in the code rather than having to deal with them in an expression.

Summary

The focus of this chapter was the activities that enable you to work with collections of data. The ForEach<T> and ParallelForEach<T> activities iterate over the elements in a collection, while the collection-related activities such as AddToCollection<T> and ExistsInCollection<T> work with the elements in the collection. The use of these activities was demonstrated in a series of example workflows and activities.

This chapter also presented a set of custom activities that are designed to work with dictionaries of data instead of simple collections. Using a dictionary provides potential performance improvements over a simple collection, especially when individual elements from a large collection must be located and updated.

The chapter concluded with a demonstration of the InvokeMethod activity. This activity allows you to invoke an instance method on an object or a static method defined for a type.

In the next chapter, you will learn about the flowchart modeling style and the activities that are provided to implement this style.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.211.106