Simple Application Extensibility with WF Rules


Overview:

In this post, we take a lot at how we can use WF Rules to implement simple application extensibility. For some applications, you might want to enhance the flexibility and extensibility of your application without having to invest extraordinary amounts of time in devising and testing a complicated plug-in architecture. This article will show how you can leverage WF Rules using the RuleSetService together with a minimum amount of custom code to accomplish this goal.

 

Applying Rules to non-Workflow types:

 

Typically, Windows Workflow Foundation Rules are applied to Workflow Type, either as a Declarative Rule Condition, or as part of a PolicyActivity. Within a Rule condition and/or its associate "If" or "Else" Actions you can access the Workflow fields, properties and methods. In addition, the Rule can also access the members of objects within your Workflow class.

 

The Rules sub-system of Windows Workflow Foundation was architect to be independent the Workflow types, and RuleSets were designed to be able to operate on ANY type, not just a type derived from System.Workflow.ComponentModel.Activity. Therefore, you can apply a RuleSet to any of the types in your own assemblies.

 

This fact opens up some interesting possibilities. In a future post, I will present a sample that uses WF rules to perform Rule-based custom validation of WinForms and ASP.NET Web forms. In this post, we will focus on how to leverage WF Rules to achieve simple application extensibility.

 

A description of the most important interactions between the different components is provided below:

 

The application defines an extensibility point. This extensibility point contains code, which leverages the RuleSetService to load, validate and execute a RuleSet.

 

The RuleSetService loads the RuleSet from the Rules database.

 

The RuleSet has been written using the RuleSetTool.exe Rule editor. The .NET type it operates on is an Extensibility Interface, which is implemented by the application. When the developer created the RuleSet, he selected the type of this extensibility interface as the "Associated Type" for the RuleSet. For details on how to do this, refer to the "Creating the RuleSet" section of this post.

 

The different Rules that make up the RuleSet are applied during the execution of the RuleSet. During their execution, these Rules can invoke the properties and methods defined by the extensibility interface.

 

Because the application implements the extensibility interface, the RuleSet is able to extend the functionality of the application, implement custom behavior etc.

 

In the above pattern, note that the use of an Extensibility Interface is not absolutely necessary. But, if we provided the main type of the application itself (for example the Main Form class) as the associated type of the workflow, then our Rules would be able to affect all aspects of our application, which is probably NOT what we really want. Using an extensibility interface allows the designer to reduce the "surface area" of the application on which the RuleSet can operate.

 

Sample Application:

 

In this sample we assume that we are working on an e-commerce application. We will create a simple WinForms application, which displays a grid of summary records of Return Merchandise Authorization (RMA) information, grouped by Product Code.

 

Each summary record contains the following information:

 

The Product Code:

 

The number of returns and their monetary value for the current quarter. The number of returns and their monetary value for the previous quarter. The number of returns and their monetary value for the current year (Year-to-date).

 

Our application should be able to tailor the columns shown based upon the currently logged-in user. An application might want to do this for both security- and job functionality reasons (people in certain functional areas might want to see only the information that is relevant to them).

 

In our sample application, we will include a drop-down where we can select the user for which we want to test the application. In a real-world application, you would probably use the WindowsIdentity class to determine the current user's identity.

 

Creating the application skeleton:

 

Create a new WinForms application, and add the following references: 

  • System.Workflow.Activities.dll
  • System.Workflow.ComponentModel.dll
  • System.Workflow.Runtime.dll
  • RuleSetServices.dll: This is our RuleSetService implementation mentioned in the "external rules" article.
  • ExternalRuleSetLibrary.dll: This is the Entity library for the RuleSetService.

We will model the RMA information by a class called RMAQuartelySummary.cs. Add this class to the project. This is a simple data container class, the source Code is shown below:

 

 1 using System;

 2

 3 namespace SampleApplication

 4 {

 5     /// <summary>

 6     /// These are the quarterly summary figures.

 7     /// Each instance represents the figures for 

 8     /// one particular product

 9     /// </summary>

10     public class RMAQuarterlySummary

11     {

12         private String m_productCode;

13         private int m_numberOfReturnsThisQuarter;

14         private int m_numberOfReturnsLastQuarter;

15         private int m_ytdNumberOfReturns;

16         private Decimal m_valueOfReturnsThisQuarter;

17         private Decimal m_valueOfReturnsLastQuarter;

18         private Decimal m_ytdValueOfReturns;

19

20         public String ProductCode

21         {

22             get { return m_productCode; }

23             set { m_productCode = value; }

24         }

25

26         public int NumberOfReturnsThisQuarter

27         {

28             get { return m_numberOfReturnsThisQuarter; }

29             set { m_numberOfReturnsThisQuarter = value; }

30         }

31

32         public int NumberOfReturnsLastQuarter

33         {

34             get { return m_numberOfReturnsLastQuarter; }

35             set { m_numberOfReturnsLastQuarter = value; }

36         }

37

38         public Decimal ValueOfReturnsThisQuarter

39         {

40             get { return m_valueOfReturnsThisQuarter; }

41             set { m_valueOfReturnsThisQuarter = value; }

42         }

43

44         public Decimal ValueOfReturnsLastQuarter

45         {

46             get { return m_valueOfReturnsLastQuarter; }

47             set { m_valueOfReturnsLastQuarter = value; }

48         }

49

50         public int YtdNumberOfReturns

51         {

52             get { return m_ytdNumberOfReturns; }

53             set { m_ytdNumberOfReturns = value; }

54         }

55

56         public Decimal YtdValueOfReturns

57         {

58             get { return m_ytdValueOfReturns; }

59             set { m_ytdValueOfReturns = value; }

60         }

61

62         public RMAQuarterlySummary(

63             string productCode,

64             int numberOfReturnsThisQuarter,

65             Decimal valueOfReturnsThisQuarter,

66             int numberOfReturnsLastQuarter,

67             Decimal valueOfReturnsLastQuarter,

68             int ytdNumberOfReturns,

69             Decimal ytdValueOfReturns)

70         {

71             m_productCode = productCode;

72             m_numberOfReturnsThisQuarter = numberOfReturnsThisQuarter;

73             m_valueOfReturnsThisQuarter = valueOfReturnsThisQuarter;

74             m_numberOfReturnsLastQuarter = numberOfReturnsLastQuarter;

75             m_valueOfReturnsLastQuarter = valueOfReturnsLastQuarter;

76             m_ytdNumberOfReturns = ytdNumberOfReturns;

77             m_ytdValueOfReturns = ytdValueOfReturns;

78         }

79     }

80 }

81 

 

Our grid will always display the following columns:

 

The Product Code (property ProductCode). The Number of Returns for the current quarter (property NumberOfReturnsThisQuarter).

 

Our RuleSet will determine which additional columns will be shown, based on a set of properties of the currently logged-in user. We will use the RMACustomColumnType enumeration to represent these custom columns. This enumeration is shown below:

 

 1 using System;

 2

 3 namespace SampleApplication

 4 {

 5     /// <summary>

 6     /// These are the different types of columns

 7     /// that can optionally be displayed in

 8     /// our Grid

 9     /// </summary>

10     public enum RMACustomColumnType

11     {

12         ValueOfReturnsThisQuarter,

13         NumberOfReturnsLastQuarter,

14         ValueOfReturnsLastQuarter,

15         YtdNumberOfReturns,

16         YtdValueOfReturns

17     }

18 }

19 

 

We also need a class to represent a user. In a real-world scenario, we would probably use Activity Directory or some other LDAP provider. To keep it simple, we will use a class called BusinessUser.cs, as shown below:

 

 1 using System;

 2

 3 namespace SampleApplication

 4 {

 5     /// <summary>

 6     /// This class represents our users

 7     /// </summary>

 8     public class BusinessUser

 9     {

10         private string m_name;

11         private bool m_isAccountant;

12         private bool m_isManager;

13

14         public string Name

15         {

16             get { return m_name; }

17         }

18

19         public bool IsAccountant

20         {

21             get { return m_isAccountant; }

22         }

23

24         public bool IsManager

25         {

26             get { return m_isManager; }

27         }

28

29         public BusinessUser(string name, bool isAccountant, bool isManager)

30         {

31             m_name = name;

32             m_isAccountant = isAccountant;

33             m_isManager = isManager;

34         }

35     }

36 }

37 

 

Note the two properties:

 

  • bool IsAccountant: This property indicates that the user is an accountant.
  • bool IsManager: This property indicates that the user is a manager.

These properties will indirectly be used by our Ruleset to determine which custom columns should be shown in the grid.

 

Defining the Extensibility Interface:

 

Next, we will define the extensibility interface that will be exposed by our application to the WF Rules. Below is a summary of the requirements for this interface:

 

The interface should have access to the IsAccountant and IsManager Boolean properties of the current user.

 

The interface should enable the Rules to add a custom column and a custom header text to the grid view.

 

Add a new interface to the project, and name it IGridExtensibility.cs.The code for this class is shown below:

 

 1 using System;

 2

 3 namespace SampleApplication

 4 {

 5     /// <summary>

 6     /// Thi is our extensibility interface

 7     /// </summary>

 8     public interface IGridExtensibility

 9     {

10         bool IsCurrentUserAccountant { get; }

11         bool IsCurrentUserManager { get; }

12

13         void AddColumn(

14             RMACustomColumnType columnType,

15             string headerText);

16     }

17 }

18 

 

Implementing the Application's Main Form:

 

The design of the main form is shown below. We use a DataGridView to show the data, and a drop-down combo to allow for the selection of the current user:

 

The complete code for the main form is available in the downloads section, I will focus on the most important code sections below.

 

Our Form will implement the IGridExtensibility interface, as shown below:

 

1 namespace SampleApplication

2 {

3     public partial class MainForm : Form, IGridExtensibility

4     {

 

To simulate a number of users, we will create a List of BusinessUsers as a private member. Our drop-down combo will allow us to select a user:

 

1      private List<BusinessUser> m_users = new List<BusinessUser>();

2

3             // Create three different users

4             m_users.Add(new BusinessUser("Joe User", false, false));

5             m_users.Add(new BusinessUser("Fred Beancounter", true, false));

6             m_users.Add(new BusinessUser("Jack Sledgehammer", false, true));

 

Note that the first user ("Joe User") is neither an accountant nor a manager, while "Fred Beancounter" is an accountant. The final user "Jack Sledgehammer" is a manager (what else did you expect?).

 

The implementation of this interface is shown below:

 

 1   #region IGridExtensibility Members

 2  // This property is used by our Rules to determine if

 3  // the current user is an accountant

 4  public bool IsCurrentUserAccountant

 5  {

 6      get { return m_currentUser.IsAccountant; }

 7  }

 8

 9  // This property is used by our Rules to determine if

10  // the current user is a Manager

11  public bool IsCurrentUserManager

12  {

13      get { return m_currentUser.IsManager; }

14  }

15

16  // This method is used by our Rules to add a Custom Column

17  public void AddColumn(RMACustomColumnType columnType, string headerText)

18  {

19      DataGridViewTextBoxColumn column = new DataGridViewTextBoxColumn();

20      column.HeaderText = headerText;

21      column.DataPropertyName = columnType.ToString();

22      column.Name = columnType.ToString();

23

24      m_userColumns.Add(column);

25  }

26

27 #endregion IGridExtensibility Members

 

In lines 4 through 14, we implement the properties that will be used by the RuleSet to access the properties of the current user. The application maintains the current user in the m_currentUser private field.

 

The AddColumn method is the method that will be called by the Rule to add a custom column to the grid. The code simply creates a new DataGridViewTextBoxColumn instance, and adds it to the m_userColumns list. This list will then be used by the application to create the custom columns in the grid (see next section).

 

The showColumns method is the method that contains our extensibility point. In this method we load, validate and execute our RuleSet:

 

 1  // This method shows both the standard and custom columns

 2  // for our grid

 3  private void showColumns()

 4  {

 5      // First, show the standard columns

 6      showStandardColumns();

 7      m_userColumns.Clear();

 8

 9      // Use the RuleSetService to retrieve our RuleSet

10     RuleSetService ruleSvc = new RuleSetService();

11     RuleSet ruleSet =

12     ruleSvc.GetRuleSet(new RuleSetInfo("RmaRules"));

13

14     // Setup a RuleValidation instance

15     RuleValidation validation = new RuleValidation(this.GetType(), null);

16

17     // Validate the RuleSet

18     if (ruleSet.Validate(validation))

19     {

20          RuleExecution execution = new RuleExecution(validation, this);

21          ruleSet.Execute(execution);

22

23          // Show each custom column added by the rule

24          foreach (DataGridViewColumn column in m_userColumns)

25          {

26              RMAGrid.Columns.Add(column);

27          }

28      }

29  }

 

In lines 10-12, we instantiate the RuleSetService, and load our "RmaRules" RuleSet from the Rules database.

 

Before we can execute our RuleSet, we need to validate it. We do this by creating a RuleValidation instance, passing in our type, which in this case is our extensibility interface type. Optionally, you can pass in a custom ITypeProvider interface as the second argument, which can further restrict which types are available to the RuleSet.

 

In line 15 we call the Validate method on the RuleSet, passing in our RuleValidation instance. The Validate method will ensure that all Rules in our RuleSet are accessing valid fields, properties and methods of the type passed in as the first argument to the RuleValidation constructor.

 

To execute the RuleSet, we first need to instantiate a RuleExecution object. The constructor takes the following arguments:

 

Our previously instantiated RuleValidation instance. Passing in a RuleValidation instance guarantees that its contained rules are defined correctly.

 

Our Form instance, which is the object on which the RuleSet will operate. All access to this object will be performed through the extensibility interface, since this is the type that was specified when we create the RuleSet in the RuleEditor (see next section).

 

Finally, we can execute the RuleSet by invoking its Execute method, passing in our RuleExecution instance. This will trigger our Rules to fire, invoking the methods in our extensibility interface. Note that this execution will be performed on our current thread, so we have no need for thread synchronization primitives here.

 

After our Rule execution is complete, we simply add the custom columns to our Grid (lines 24-26).

 

Creating the RuleSet:

 

To create the RuleSet, execute the RuleSet Editor RuleSetTool.exe (found in the

ExternalRuleSetSample dialog). Create a new RuleSet named RmaRules with version 1.0. Associate the RuleSet with the SampleApplication.IGridExtensibility interface, as shown below:

 

We will create two rules:

 

1. ColumnsForBeanCounter: The condition of this rule checks if the current user's IsAccountant flag is set:

 

 this.IsCurrentUserAccountant

   The "then action" adds the following custom columns to the grid:

    ValueOfReturnsThisQuarter

    ValueOfReturnsLastQuarter

    YtdValueOfReturns

 

this.AddColumn(SampleApplication.RMACustomColumnType.ValueOfReturnsThisQuarter, "Money Lost this Quarter")

this.AddColumn(SampleApplication.RMACustomColumnType.ValueOfReturnsLastQuarter, "Money Lost last Quarter")

this.AddColumn(SampleApplication.RMACustomColumnType.YtdValueOfReturns, "Money Lost last Year")

 

2. ColumnsForManager: The condition of this rule check if the current user's

  IsManager flag is set:

 

this.IsCurrentUserManager

  The "then action" adds the following custom columns to the grid:

    YtdTotalNumberOfReturns

    YtdValueOfReturns

 

this.AddColumn(SampleApplication.RMACustomColumnType.YtdNumberOfReturns, "Total Returns this Year")

this.AddColumn(SampleApplication.RMACustomColumnType.YtdValueOfReturns, "Values of Returns for this year")The Rules editor with the completed RuleSet is shown below:

 

Running the Application:

 

When you run the application, and you select the first user ("Joe User"), you will notice that only the standard columns are displayed:

 

When you select "Fred Beancounter", you will see the "accountant" custom columns:

     

An finally, our friend Sledgehammer will see the manager columns:

Conclusion:

 

In this example, you have seen that the power of WF RuleSets can be leveraged outside of the realm of Workflow. Because a RuleSet can be associated and operate on any .NET type, it can be applied to any number of application domains. In the next post, we will look at another interesting application of WF Rules: providing complex validation of WinForms and Web forms.


Similar Articles