in

Foo Theory

Partners in Community - serving up some ice cold Kool-Aid!
Welcome to footheory.com.  The bloggers and contributing members on this site are consultants, project/program managers and software architects working across the US.  Our community will focus on Microsoft technologies, .NET architecture, software patterns & practices and just plain stream of consciousness.

Bennie's Weblog

Simple Application Extensibility with WF Rules

Overview

In a previous article, we studied how we can externalize Windows Workflow Rules into a separate database, allowing the rules to be modified without a need to rebuild or re-deploy the application.

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 architected 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.

An overview of how such a concept can be implemented is shown in the following figure:

Extensibility

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

  1. The application defines an extensibility point. This extensibility point contains code which leverages the RuleSetService to load, validate and execute a RuleSet.
  2. The RuleSetService loads the RuleSet from the Rules database.
  3. 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
  4. 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.
  5. 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:

  1. System.Workflow.Activities.dll
  2. System.Workflow.ComponentModel.dll
  3. System.Workflow.Runtime.dll
  4. RuleSetServices.dll: This is our RuleSetService implementation mentioned in the "external rules" article.
  5. 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:

  1. The Product Code (property ProductCode).
  2. 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 simpe, 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:

  1. bool IsAccountant: This property indicates that the user is an accountant.
  2. 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:

  1. The interface should have access to the IsAccountant and IsManager boolean properties of the current user.
  2. 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:

FormDesign

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

1 // Create three different users 2 m_users.Add(new BusinessUser("Joe User", false, false)); 3 m_users.Add(new BusinessUser("Fred Beancounter", true, false)); 4 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:

 CreateRule

we will create two rules:

  • 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")

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

    ExtensibilityRulesFinished

    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:

    JoeUser

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

    FredBeancounter

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

    JackSledgeHammer

    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.

     

  • Comments

     

    Donn Felker said:

    Bennie,

    Mabye I overlooked it... I was not able to find the download for this code in your download section. (http://footheory.com/files/default.aspx)

    If its in a different location can you post the location?

    Thanks :)

    Donn

    May 31, 2007 11:27 AM
     

    Marco said:

    Bennie,

    First of all, good article. I was wondering if you were familiar with the Rule Manager from Acumen Business. Besides rule editors that aims to the business users, it also has automatic rule verification and rule validation to test a rule policy.

    Rulesets (or rule policies) can be deployed to a WF rules solution. The export wizard also creates the Domain Model (business terms).

    The Rule Manager has become a slightly more complex extension of the WFRules API :-)

    You can download it <a href="http://www.acumenbusiness.com/products.htm">here</a>

    Hope you like it

    Marco

    June 10, 2007 4:26 PM
     

    Rob said:

    Hi, I really wanted to have a play with this sample as the approach is pretty much what I had in mind for my new project. (Great minds must think alike ;-) I can't find the code in the downloads section, is it available anywhere? Thanks.

    June 29, 2007 2:00 AM
     

    bennie said:

    Hi Guys,

    I (finally) made the code of the sample available at: http://footheory.com/files/folders/wf/entry113.aspx.

    Sorry for the delay,

    Bennie

    June 30, 2007 11:07 AM
     

    WF Community Bloggers said:

    Hello, This file contains the code samples for the following articles: Simple Application Extensibility

    June 30, 2007 11:30 AM
     

    DevDays 2010 Presentation: Using the Workflow Foundation Rule Engine without using Workflow Foundation « Daniel van Wyk's Blog said:

    Pingback from  DevDays 2010 Presentation:  Using the Workflow Foundation Rule Engine without using Workflow Foundation &laquo; Daniel van Wyk&#039;s Blog

    March 30, 2010 7:40 AM

    Leave a Comment

    (required)  
    (optional)
    (required)  
    Add

    About bennie

    I work for a Microsoft Gold Partner, Statera SouthWest as a Strategic Partner and a Solutions Architect
    Copyright ASIQS Corporation © 2006, All rights reserved.
    Powered by Community Server (Commercial Edition), by Telligent Systems