Conditions and Rules

Summary

Conditions

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):

  1. IfElseBranch
  2. While
  3. Replicator
  4. ConditionedActivituyGroup (CAG)

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:

Condition: Code Condition

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:

Condition: Declarative Rule Conditions

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:

  1. Select the activity and set its Condition property to 'Delcarative Rule Condition'. This will cause the Condition property to be expanded to show CodnitionName and Expression properties (see figure below)
  2. Enter a proper name for ConditionName, and then select the ellipsis in the Expression property to launch the 'Rule Condition Editor'. Specify a boolean condition such as this.Coupons.Count == 0. Note that this editor has Intellisense support so always start your boolean conditions with this. .
  3. Select OK. This will create a RuleDefition object that is serialized into a <WorkflowName>.rules file that is added to the project (never need to edit this file manually).

 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.

ConditiondActivityGroup

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

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:

Rules Evaluation

Given that a RuleSet is a collection of rules, the evalaution of any given rule set proceeds as follows:

  1. Find the highest priority rule (priority 5 is higher than priority 0)
  2. Evaluate the rule and execute its Then/Else actions as appropriate.
  3. If the actions of a rule update a field/property that is used by a previous rule in the list (i.e., it had higher priority), re-evaluate that previous higher-priority rule and execute its actions as appropriate.
  4. Continue the process until all rules in the RuleSet have been evaluated.
Example

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:

  1. Rule3 runs first because it has the highest priority. It evaluates to false and does nothing since there is no 'else' condition.
  2. Rule2 is evaluated next because it has the next highest priority. Rule2 evaluates to true and sets A to 1. Since Rule3 which has a higher priority uses A, Rule3 is re-evaluated again setting B to 2. Rule2 is not revaluated again since it has no dependency on B. Evaluation then proceeds to the next rule which is Rule1.
  3. Rule1 evaluates to true and sets D to 2.

Forward Chaining

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

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.

Attribute-based

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:

Explicit

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)

Forward Chaining Control

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:

  1. Limit the repetitive execution of rules, which may give incorrect results.
  2. Increase performance.
  3. Prevent runaway loops.

This level of control is facilitated in WF rules by these two properties:

  1. A ChainingBehavior property on the RuleSet.
  2. A ReevaluationBehavior property on each rule.

Both of these values can be set in the RuleSet Editor.

ChainingBehavior Property

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.

ReevaluationBehavior Property

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

Collection Processing

See MSDN.

Example

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:

SaleTypes Project

// 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();
        }
    }
}

PointOfSaleWorkflow Project

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: