Conditions are set in activities to control the behavior of the workflow. Conditions are used by the following four WWF activities (by setting their Conidtion property):
For example, an IfElseBranch activity executes only if its Condition property evaluates to true (recall that the last IfElseBranch does not need to have its Condition set, as it will automatically execute if the Condition for all the other IfElseBranch activities evaluate to false).
Conditions can be used with any of the above four WWF activity by setting the appropriate Conidtion property. This property can be either specified as 'Code Condition', which will have a code handler, or as a 'Declareative Rule Conditions'. These two approaches are illustrated below:
To use conditions using the Code Condition approach, create a code-condition handler (shown below) and assign it to an activity's Condition property (see next figure):
// A code-condition is similar to an event handler; the runtime engine executes
the method
// and uses the value of e.Result as the result of the condition evaluation
private void ifNoItems_Condition(object sender, ConditionalEventArgs e)
{
e.Result = (this.SomePropertyValue > 10) ? true : false;
}
The following shows how a code condition is set for the
ifNoItems IfElseBranchActivity
activity:
The other approach is to use Declarative Rule Conditions which internally creates a RuleConditionReference instance which points to a RuleCondition definition that is usually specified in a .rules file. Rule conditions can be used instead of code conditions in any activity that supports conditions. The steps are as follows:
These steps are shown below:
The main reason to use a rule condition instead of a code condition is that rule conditions become part of the model and can be dynamically updated at run time on executing workflow instances. A secondary advantage of rule conditions is that as part of the model, more sophisticated tools can be built on top of the model to provide additional authoring experiences, dependency management, cross-condition analysis, and so on.
This example uses the ConditiondActivityGroup.
ConditionedActivityGroup is a
group of child activities whose execution is controlled by conditions. For
example, you can use a ConditionedActivityGroup activity to loop through a set
of activities conditionally, based on criteria specific to each activity, until
an Until condition is true for the
ConditionedActivityGroup as a whole.
Typically you use
ConditionedActivityGroup activity by applying a
When condition
to each child activity and an Until condition to the
ConditionedActivityGroup
activity. When a ConditionedActivityGroup activity first starts executing, it
evaluates the Until condition. If the
Until condition evaluates to false, the
When condition of all the first-descendant child activities are evaluated. If a
child activity's When condition evaluates to
true, it is scheduled for
execution. This Until and When condition evaluation is repeated every time that
a first-descendant child activity is completed. Therefore, the other
first-descendant child activities can be newly scheduled or rescheduled for
execution depending on what occurred in the just-completed activity. As soon as
the Until condition on the ConditionedActivityGroup activity evaluates to
true,
it immediately cancels any currently executing child activities.
Rules are a much more robust form of conditions that enable you to
programmatically, or through an XML file, define what business rules your
workflows must follow. A RuleSet is a set of rules and their resulting actions.
Think of a rule as an if-then-else statement, with the rule condition
corresponding to the if expression, and the actions defining the behaviour of the
then and else clauses.
The following figures should help understand rule sets. The first figure shows a
selected PolicyActivity activity and its properties,
with emphasis on RuleSetReferece property, while the second
figure shows how RuleSet1 is composed of a number of rules. The selected rules can
then be edited to set its name, priority, re-evaluation condition, Condition,
etc:
As is obvious from the above figures, you use the PolicyActivity activity to encapsulate the definition and execution of a RuleSet. The steps to add a policy activity are as follows:
Given that a RuleSet is a collection of rules, the evalaution of any given rule set proceeds as follows:
Assume you have the following rules with the indicated names, priorities, and actions:
Rule1 (priority = 1): if B = 2 then D = 1
Rule2 (priority = 2): if C = 0 then A = 1
Rule3 (priority = 3): if A = 1 then B = 2
Assume initial data is A = 0, B = 0, C = 0, D = 0. Evaluation then proceeds as follows:
The example above showed how Rule1, Rule2, and Rule3 are chained based on identified dependencies among them. In general, these dependencies can be declared in three ways: Implicit, Attribute-based, and . Explicit.
Implicit dependencies are identified automatically by the engine. In the example above, Rule3 has a dependency on Rule2; this means that the engine will ensure that Rule3 is re-evaluated when Rule2 executes its THEN condition.
When a rule calls methods instead of setting property values, it becomes difficult to deterministically evaluate the reads/writes that occur. To resolve this issue, WF provides three attributes that can be applied to a method to indicate its actions:
Example 1
Rule1: IF this.Total = 0 THEN this.SetDiscount( 0.05);
Rule2: IF this.Discount > 0 THEN this.Total = this.Total - (this.Total *
this.Discount)
[RuleWrite("Discount")]
void SetDiscount(double requestedDiscount)
{
// some code that updates the Discount property
}
Because Rule1 uses the
SetDiscount method to modify the
Discount property, and
because Rule2 reads the Discount property,
Rule2 depends on Rule1.
Example 2
Rule1: IF this.Total = 0 THEN this.SetCustomDiscount( 0.05);
Rule2: IF this.Discount > 0 THEN this.Total = this.Total - (this.Total *
this.Discount)
[RuleInvoke("SetDiscount")]
void SetCustomDiscount(double requestedDiscount)
{
...
SetDiscount(requestedDiscount);
...
}
[RuleWrite("Discount")]
void SetDiscount(double requestedDiscount)
{
// some code that updates the Discount property
}
It is important to recognize that the field or property referenced in the attribute path refers to a field or property on the same class as the method, which is not necessarily the root object passed to the RuleSet for execution. Finally, note the following points about these attributes:
The last mechanism for indicating field/property dependencies is through the use of the Update statement. The statement takes as its argument either a string that represents the path to a field or property or an expression that represents a field/property access. The Update statement indicates that the rule writes to the indicated field/property. This would have the same effect as a direct set of the field/property in the rule or the calling of a method with a RuleWrite attribute for the field/property. For example:
Update(this.customer.Name)
The Update command also supports the use of wildcards. For example, you could add the following Update command to a rule:
Update("this/customer/*")
This would cause any rules that use any property on the Customer instance in their conditions to be re-evaluated. In other words, each of the following two rules would be re-evaluated.
IF this.customer.ZipCode == 98052
THEN ...
IF this.customer.CreditScore < 600
THEN ...
Generally, the implicit chaining should provide the required dependency analysis
and chaining behavior in most scenarios. Method attributing should support the
most prevalent scenarios in which implicit chaining cannot identify the
dependencies. In general, method attributing would be the preferred method for
indicating dependencies over the use of the Update statement, since the
dependency can be identified once on the method and leveraged across many
different rules that use that method.
However, there will be some scenarios in which the
Update statement will be the
appropriate solution, such as when a field/property is passed to a method on a
class that the workflow writer does not control and therefore cannot attribute,
as is the case in the following code.
IF ...
THEN this.customer.UpdateCreditScore(this.currentCreditScore)
Update(this.currentCreditScore)
Sometimes the rule writer wants the ability to provide more control over the chaining behavior, specifically the ability to limit the chaining that takes place. This enables the rule modeller to:
This level of control is facilitated in WF rules by these two properties:
Both of these values can be set in the RuleSet Editor.
The Chaining Behavior property on the RuleSet has three possible values: Full, UpdateOnly, or None.
The Full Chaining option is the default and provides the behavior described up
to this point. The UpdateOnly chaining option turns off the implicit and
attribute-based chaining and prescribes that chaining should only occur for
explicit Update statements. This gives the rule writer complete control over
what rules cause reevaluation. Typically this would be used to either avoid
cyclic dependencies that cause excessive (or even runaway) rule re-execution, or
to boost performance by eliminating superfluous rule reevaluation not required
to provide functional completeness of the RuleSet.
The final option is
None. This option will cause the engine to evaluate
the rules in strictly linear fashion. Each rule would be evaluated once and only
once and in the order of priority. Rules with a higher priority could impact
rules with lower priorities, but the inverse would not be true since no chaining
would occur. Therefore, this option would be used with explicit priority
assignments unless no dependencies existed among the rules.
The Reevaluation Behavior on the rule has two possible values: Always and Never.
Always is the default and provides the behavior previously discussed, namely
that the rule will always be reevaluated based on chaining due to the actions of
other rules. Never, as the name implies, turns off this reevaluation. The rule
will be evaluated once but will not be reevaluated if it has previously executed
any actions. In other words, if the rule has previously been evaluated and
consequently executed its Then or Else actions, it will not be reevaluated.
Execution of an empty action collection in the Then or Else actions will not
mark a rule as having been executed, though.
Typically this property would be used—at the rule level—to prevent infinite
looping due to dependencies that the rule has, either on its own actions or
other rules. For example, the following rule would create its own infinite loop
and reevaluation is not required to fulfil the functional requirements of the
rule.
IF this.shippingCharge < 2.5 AND this.orderValue > 100
THEN this.shippingCharge = 0
See MSDN.
This example consists of three project. The SalesTypes project to define types used in the workflow, the PointOfSaleWorkflow project to define the actual workflow, and the Client project to host the workflow and control it:
// Coupon.cs
using System;
...
namespace SalesTypes
{
public enum eCouponType
{
None,
PercentTotal,
PercentMostExpensive,
PercentCheapest
};
// This class is used to describe a coupon
public class Coupon
{
private string _strDescription;
private eCouponType _eCouponType;
public Coupon(eCouponType type)
{
CouponType = type;
switch (type)
{
case eCouponType.PercentTotal:
Description = "10% Off Total";
break;
case eCouponType.PercentMostExpensive:
Description = "20% Off Most expensive";
break;
case eCouponType.PercentCheapest:
Description = "30% Off Cheapest";
break;
case eCouponType.None:
Description = "No Coupon applied";
break;
default:
throw new InvalidOperationException("Type " + type.ToString() + " is not
supported");
}
}
public string Description
{
get { return _strDescription; }
set { _strDescription = value; }
}
public eCouponType CouponType
{
get { return _eCouponType; }
set { _eCouponType = value; }
}
}
}
// SaleItem.cs
using System;
...
namespace SalesTypes
{
public class SaleItem : IComparable<SaleItem>
{
private string _strName;
private double _dPrice;
#region Properties
public SaleItem(string name, double price)
{
Name = name;
Price = price;
}
public string Name
{
get { return _strName; }
set { _strName = value; }
}
public double Price
{
get { return _dPrice; }
set { _dPrice = value; }
}
#endregion
#region IComparable<SaleItem> Members
public int CompareTo(SaleItem other)
{
if (other == null) return -1;
if (other == this) return 0;
// Do actual comparison
return Price.CompareTo(other.Price);
}
#endregion
#region IComparer
private class AscendingPrice : IComparer<SaleItem>
{
#region IComparer<SaleItem> Members
public int Compare(SaleItem x, SaleItem y)
{
return x.Price.CompareTo(y.Price);
}
#endregion
}
#endregion
public static IComparer<SaleItem> GetAscPriceComparer()
{
return new SaleItem.AscendingPrice();
}
}
}
Project workflow design is shown below:
using System;
...
namespace PointOfSale
{
// Workflow used to process a shopping cart. Note the following points:
// 1. How do you pass the contents of the shopping basket and the used coupons
// to the workflow? This is an example of passing parameters to workflow.
// The workflow is created with WorkflowRuntime.CreateWorkflow that takes
// a generic dictionary with each item in the dictionary consisting of a string
// key and object value.
// 2. Do you pass the end result to the client app. This example uses the WorkflowCompleted
// event to pass values from the workflow back to the client (see
'communication
// between workflow and hosting environemen' section in the 'Basic State
Workflow'
// chapter for more information)
public sealed partial class PointOfSaleWorkflow: SequentialWorkflowActivity
{
#region Data members
private string _strNoItemsError = "Please add items to the shopping basket";
private Coupon _coupon = null;
private List<SaleItem> _lstSaleItems = null;
private double _dSubtotal = 0;
private double _dTotal = 0;
private double _dDiscount = 0;
private double _dMostExpensive = 0;
private double _dCheapest = 0;
#endregion
#region Constructors
public PointOfSaleWorkflow()
{
InitializeComponent();
}
#endregion
#region Properties
#region Properties for parameters
// The following properties correspond to keys in a dictionary that was supplied
// to CreateWorkflow method. This is how parameters can be passed to a workflow!
public Coupon Coupon
{
get { return _coupon; }
set { _coupon = value; }
}
public List<SaleItem> SaleItems
{
get { return _lstSaleItems; }
set { _lstSaleItems = value; }
}
#endregion
public double Discount
{
get { return _dDiscount; }
set { _dDiscount = value; }
}
public double SubTotal
{
get { return _dSubtotal; }
set { _dSubtotal = value; }
}
public double Total
{
get { return _dTotal; }
set { _dTotal = value; }
}
#endregion
#region No Coupons Branch
#region 'ifNotItems' activity
// This condition is executed by the ifNoItems activity to determine whether
// the activity should run or not. Note that a code condition is used to test
// whether ifNotItems acitivity should execute (we could have use a declarative
// rule as well.)
private void ifNotItems_Condition(object sender, ConditionalEventArgs e)
{
// If count of sale items is 0, e.Result will be set to true and this means
// that the ifNoItems activity will execute. If e.Result was false, ifNoItems
// activity will not execute
e.Result = (SaleItems.Count == 0) ? true : false;
}
#endregion
#region 'NoItems' TerminateActivity code
// Retuns error message if no items were added to the cart
public string NoItemsError
{
get { return _strNoItemsError; }
set { _strNoItemsError = value; }
}
#endregion
#endregion
#region Has Coupons Branch
#region 'codeCalculateSubTotal' CodeActivity code
// Calculate total for sale items and identifies most expensive and cheapest
private void CalculateSubtotal(object sender, EventArgs e)
{
// Sort the sale items to help identify cheapest and most expensive
_lstSaleItems.Sort(SaleItem.GetAscPriceComparer());
// Calculate sub-total
_dSubtotal = 0;
foreach (SaleItem si in _lstSaleItems)
_dSubtotal += si.Price;
// Get min and max (list sorted in ascending order)
_dCheapest = _lstSaleItems[0].Price;
_dMostExpensive = _lstSaleItems[_lstSaleItems.Count - 1].Price;
}
#endregion
#region Rules helpers
private void SetDiscount(eCouponType coupontype)
{
switch (coupontype)
{
case eCouponType.PercentCheapest: // 30% Off Cheapest
_dDiscount = 0.3 * _dCheapest;
break;
case eCouponType.PercentMostExpensive: // 20% Off Most expensive
_dDiscount = 0.2 * _dMostExpensive;
break;
case eCouponType.PercentTotal:
// 10% Off Total
_dDiscount = _dSubtotal * 0.10;
break;
}
}
#endregion
#region RemoveCoupon CodeActivity code
private void RemoveCoupon(object sender, EventArgs e)
{
_coupon.CouponType = eCouponType.None;
}
#endregion
#endregion
#region 'codeCalculateTotal' CodeActivity code
// Calculate total for sale items
private void CalculateTotal(object sender, EventArgs e)
{
Total = _dSubtotal - _dDiscount;
}
#endregion
}
}
When you execute workflows, you have the option of passing data that is used for initialization through the use of parameters. This is done by creating a Dictionary-based collection that contains key-value pairs that directly map to workflow-defined properties and their associated values.
using System;
...
namespace Client
{
// Interaction logic for Window1.xaml
public partial class MainWindow : System.Windows.Window
{
#region Data
members
// The
following two members are used to hold user selections for items and coupon
private List<SaleItem> _lstSaleItems
= new List<SaleItem>();
private eCouponType _eCouponType =
eCouponType.None;
// The
following represnets an isntance of a workflow
WorkflowRuntime _wfRuntime =
new WorkflowRuntime();
#endregion
public MainWindow()
{
InitializeComponent();
InitializeWorkFlow();
}
#region event
handlers
// Loaded event
handler is used to populate all list views
private void
Window_Loaded(object sender, RoutedEventArgs args)
{
InitListViewsForSaleItems();
InitListViewsForCoupons();
}
// Add items to
cart
private void
AddToCart_Click(object sender, RoutedEventArgs args)
{
if (lvAvailableItems.SelectedItems.Count
== 0) return;
foreach (SaleItem
si in lvAvailableItems.SelectedItems)
lvAddedItems.Items.Add(si);
}
// Remove items
from cart
private void
RemoveFromCart_Click(object sender, RoutedEventArgs args)
{
if (lvAddedItems.SelectedItems.Count
== 0) return;
lvAddedItems.Items.RemoveAt(lvAddedItems.SelectedIndex);
}
private void AddCoupon_Click(object
sender, RoutedEventArgs args)
{
if (lvAvailableCoupons.SelectedItems.Count
== 0) return;
// Remove coupon from list of avialable coupons and add to list of added
coupones
Coupon
coupon = (Coupon)lvAvailableCoupons.Items[lvAvailableCoupons.SelectedIndex];
lvAvailableCoupons.Items.Remove(coupon);
lvUsedCoupons.Items.Add(coupon);
btnAddCoupon.IsEnabled = false;
}
private void
RemoveCoupon_Click(object sender, RoutedEventArgs args)
{
if (lvUsedCoupons.SelectedItems.Count
== 0) return;
// Remove coupon from list of added coupons and add to list of available
coupones
Coupon
coupon = (Coupon)lvUsedCoupons.Items[lvUsedCoupons.SelectedIndex];
lvUsedCoupons.Items.Remove(coupon);
lvAvailableCoupons.Items.Add(coupon);
btnAddCoupon.IsEnabled = true;
}
private void Checkout_Click(object
sender, RoutedEventArgs args)
{
// Shopping basket and selected coupon are passed to the workflow using
//
parameters.
BuildWorkflowParameters();
// Construct workflow parameters. Note that the keys "SaleItems" and "Coupon"
// correspond
to the actual names of properties on class PointOfSaleWorkflow
Dictionary<string, object> dictWFArgs = new Dictionary<string, object>();
dictWFArgs.Add("SaleItems", _lstSaleItems);
dictWFArgs.Add("Coupon", new Coupon( _eCouponType) );
// Execute workflow with parameters
WorkflowInstance wfi = _wfRuntime.CreateWorkflow(typeof(PointOfSaleWorkflow),
dictWFArgs);
wfi.Start();
}
// Use the
WorkflowCompleted event to retrieve the actual total
private delegate void
delUpdateTotals(List<double> totals);
void _wfRuntime_WorkflowCompleted(object
sender, WorkflowCompletedEventArgs e)
{
// NOTE: e.OutputParameters will contain the value of all public properties
defined
// on the
workflow
List<double> lstTotals = new List<double>();
lstTotals.Add((double)e.OutputParameters["Discount"]);
lstTotals.Add((double)e.OutputParameters["SubTotal"]);
lstTotals.Add((double)e.OutputParameters["Total"]);
// Workflow event will come on a different thread than the main UI thread.
// If so,
re-invoke on the main thread (similar to InvokeRequires in Windows Forms)
if (!Dispatcher.CheckAccess())
Dispatcher.Invoke(DispatcherPriority.Normal, new delUpdateTotals(UpdateTotals),
lstTotals);
else
UpdateTotals(lstTotals);
}
private void UpdateTotals(List<double>
totals)
{
tbDiscount.Text = totals[0].ToString();
tbSubTotal.Text = totals[1].ToString();
tbTotal.Text
= totals[2].ToString();
}
// Output a
debugging message to indicate that workflow has terminated
void _wfRuntime_WorkflowTerminated(object
sender, WorkflowTerminatedEventArgs e)
{
MessageBox.Show("Workflow terminated");
}
#endregion
#region Helpers
private void
InitializeWorkFlow()
{
_wfRuntime.StartRuntime();
_wfRuntime.WorkflowCompleted
+= new EventHandler<WorkflowCompletedEventArgs>(_wfRuntime_WorkflowCompleted);
_wfRuntime.WorkflowTerminated += new EventHandler<WorkflowTerminatedEventArgs>(_wfRuntime_WorkflowTerminated);
}
private void
InitListViewsForSaleItems()
{
// Note how data are added to the list view (there is not SubItems
// method
similar to Windows Forms). The SaleItem defines the object
// to be
added, and then GridViewColumn is used to bind to a specific
// property.
Note that we perform data binding on both the 'available'
// the
'added' list views
GridView gvAvailable = (GridView)lvAvailableItems.View;
gvAvailable.Columns[0].DisplayMemberBinding = new Binding("Name");
gvAvailable.Columns[1].DisplayMemberBinding = new Binding("Price");
GridView
gvAdded = (GridView)lvAddedItems.View;
gvAdded.Columns[0].DisplayMemberBinding = new Binding("Name");
gvAdded.Columns[1].DisplayMemberBinding = new Binding("Price");
lvAvailableItems.Items.Add(new SaleItem("Apples", 1.0));
lvAvailableItems.Items.Add(new SaleItem("Oranges", 2.0));
lvAvailableItems.Items.Add(new SaleItem("Avocados", 3.0));
lvAvailableItems.Items.Add(new SaleItem("Pears", 4.0));
lvAvailableItems.Items.Add(new SaleItem("Banana", 5.0));
}
private void InitListViewsForCoupons()
{
GridView
gvAvailable = (GridView)lvAvailableCoupons.View;
gvAvailable.Columns[0].DisplayMemberBinding = new Binding("Description");
GridView
gvAdded = (GridView)lvUsedCoupons.View;
gvAdded.Columns[0].DisplayMemberBinding = new Binding("Description");
// Available coupons. No need to data binding as there is only one column
lvAvailableCoupons.Items.Add( new Coupon(eCouponType.PercentTotal));
lvAvailableCoupons.Items.Add(new Coupon(eCouponType.PercentMostExpensive));
lvAvailableCoupons.Items.Add(new Coupon(eCouponType.PercentCheapest));
}
private void BuildWorkflowParameters()
{
// Process sale items
foreach (SaleItem si in lvAddedItems.Items)
_lstSaleItems.Add(si);
// Process coupons (if selected)
if (lvUsedCoupons.Items.Count
> 0)
_eCouponType = ((Coupon)lvUsedCoupons.Items[0]).CouponType;
}
#endregion
}
}
Screen output is shown below: