Introduction
A rules engine is a software component that allows non-programmers to add or change business logic in a business process management (BPM) system. A rule is a statement that describes a business policy or procedure. Business logic describes the sequence of operations that is associated with data in a database to carry out the rule.
A business rules engine works by separating execution code for business rules from the rest of the business process management system. This allows the end user to change business rules without having to ask a programmer for help. When a change is made, the engine will evaluate the change's effect on other rules in the system and flag the user if there is a conflict.
Generic UI engine is a UI components that are common to various kinds of User Interfaces. That means: having generic UI Components that can be accessed using common Interfaces. Generic UI engine can be mapped to custom rule engine.
Background
Using the code
The Rule Engine with generic UI consist of following parts;-
- UI Configuration language (XML)
- Rule Configuration language (XML)
- UI Engine/Parser (XSLT transformation)
- Rule Parser (C#)
Here is UI Configuration xml file sample :
<FORM>
<PAGES>
<PAGE title="GENERIC RULE ENGINE CONFIGURATION 1" id="page_1">
<FIELDS>
<FIELD type="DropDownList" label="Catagory" cType="USERCONTROL">
<PROPERTIES>
<PROPERTY name="ID">DropDownList1</PROPERTY>
<PROPERTY name="BINDING_TYPE">pre</PROPERTY>
<PROPERTY name="BINDING">candidate.cat</PROPERTY>
<PROPERTY name="PRE_RULEID">2</PROPERTY>
<PROPERTY name="AutopostBack">true</PROPERTY>
</PROPERTIES>
</FIELD>
<FIELD type="DropDownList" label="Sub-Catagory" cType="USERCONTROL">
<PROPERTIES>
<PROPERTY name="ID">DropDownList2</PROPERTY>
<PROPERTY name="BINDING_TYPE">pre</PROPERTY>
<PROPERTY name="BINDING">candidate.subcat</PROPERTY>
<PROPERTY name="PRE_RULEID">3</PROPERTY>
</PROPERTIES>
</FIELD></FIELDS>
</PAGE>
;</PAGES>
</FORM>
This UI configuration file is transformed into HTML by XSLS (Extensible Stylesheet Language Transformation (XSLT) lets you transform the content
of a source XML document into another document that is different in format in structure)
Here is sample XSLT file
="1.0" ="UTF-8"
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:asp="remove">
<xsl:output method="html" indent="yes" encoding="utf-8"
omit-xml-declaration="yes" version="4.0"></xsl:output>
<xsl:param name="pageid">page_1</xsl:param>
<xsl:template match="/">
-->
<table id="dyfrm" cellpadding="0" cellspacing="5">
-->
<tr>
<td colspan="3" align="center" style="font-size:25px">
<xsl:value-of select="FORM/PAGES/PAGE[@id=$pageid]/@title" />
</td>
</tr>
<tr><td colspan="3" style="height:20px"></td></tr>
-->
<xsl:for-each select="FORM/PAGES/PAGE[@id=$pageid]/FIELDS/FIELD">
<xsl:element name="tr">
<xsl:attribute name="id">TR_<xsl:value-of select=
"PROPERTIES/PROPERTY[@name='ID']"></xsl:value-of></xsl:attribute>
-->
<xsl:if test="@display='none'">
<xsl:attribute name="style">display:none;</xsl:attribute>
</xsl:if>
<xsl:choose>
-->
<xsl:when test="@type='HTML'">
<td colspan="3">
<!-- #include file="<xsl:value-of select="@src"></xsl:value-of>" -->;
</td>
</xsl:when>
<xsl:when test="@type='GridView'">
<td colspan="3" align="center">
-->
<xsl:element name="asp:{@type}">
<xsl:attribute name="runat">server</xsl:attribute>
<xsl:for-each select="./PROPERTIES/PROPERTY">
<xsl:attribute name="{@name}">
<xsl:value-of select="current()"></xsl:value-of>
</xsl:attribute>
</xsl:for-each>
<xsl:for-each select="./LISTITEMS/LISTITEM">
<asp:ListItem value="{@value}">
<xsl:value-of select="current()"></xsl:value-of>
</asp:ListItem>
</xsl:for-each>
</xsl:element>
</td>
</xsl:when>
-->
<xsl:otherwise>
-->
<td valign="top">
<xsl:value-of select="@label" />
</td>
-->
<td>
-->
<xsl:element name="asp:{@type}">
<xsl:attribute name="runat">server</xsl:attribute>
<xsl:for-each select="./PROPERTIES/PROPERTY">
<xsl:attribute name="{@name}"><xsl:value-of select=
"current()"></xsl:value-of></xsl:attribute>
</xsl:for-each>
<xsl:for-each select="./LISTITEMS/LISTITEM">
<asp:ListItem value="{@value}"><xsl:value-of select=
"current()"></xsl:value-of></asp:ListItem>
</xsl:for-each>
</xsl:element>
</td>
-->
<td>
<xsl:if test="@required='true'">
<asp:RequiredFieldValidator ErrorMessage="Required" runat="server"
ControlToValidate="{PROPERTIES/PROPERTY[@name='ID']}" />
</xsl:if>
<xsl:if test="@validation='Date'">
<asp:CompareValidator ErrorMessage="Dates Only" runat="server"
Operator="DataTypeCheck" Type="Date"
ControlToValidate="{PROPERTIES/PROPERTY[@name='ID']}" />
</xsl:if>
<xsl:if test="@validation='Number'">
<asp:CompareValidator ErrorMessage="Numbers Only" runat="server"
Operator="DataTypeCheck"
Type="Integer" ControlToValidate="{PROPERTIES/PROPERTY[@name='ID']}" />
</xsl:if>
<xsl:if test="@validation='Currency'">
<asp:CompareValidator ErrorMessage="Currency Only" runat="server"
Operator="DataTypeCheck" Type="Currency"
ControlToValidate="{PROPERTIES/PROPERTY[@name='ID']}" />
</xsl:if>
</td>
</xsl:otherwise>
</xsl:choose>
</xsl:element>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>
Rule Engine Configuration Sample:
<Rule id="4">
<PROPERTIES type="selectquery">
<PROPERTY name="DataTextField">#</PROPERTY>
<PROPERTY name="tablename">TABLE1</PROPERTY>
<PROPERTY name="condition">FIELD1=?</PROPERTY>
<PARAMETERS>
-->
-->
-->
-->
-->
-->
<PARAMETER type="string">100001</PARAMETER>
</PARAMETERS>
<BUSINESS>
<CONDITION ID="1">
-->
-->
-->
-->
-->
-->
<IF leftTerm="DropDownList5.Value" op="=="
rightTerm="2" type="System.Int32">
-->
<THEN TYPE="Text">FIELD2</THEN>
<ELSE>
<THEN TYPE="Text">FIELD3</THEN>
</ELSE>
</IF>
</CONDITION>
</BUSINESS>
</PROPERTIES>
</Rule>
Explanation:
Here # is replaced with compiled BUSINESS tag and ? is replaced with PARAMETER tag and type.
Each Parsed control is saved as a structure with following properties.
public struct UControls
{
public string ID;
public string ControlType;
public string BindingFields;
public string pre_Rulesid;
public string post_Rulesid;
public string DependingControl;
};
Following code is used to parse UI engine configuration file and custom UControl structure is initialized for each control and the properties pre_Rulesid and post_Rulesid defines rule id in Rule Configuration file. Here pre_Ruleid rule means the rule which loaded at the time of page load e.g., Dropdown.
var DynControls = from c in q.Descendants("FIELD")
where (string)c.Attribute("cType") == "USERCONTROL"
select c;
foreach (XElement control in DynControls.Descendants("PROPERTIES"))
{
var DynProperties = from c in control.Descendants("PROPERTY")
select c;
UControls tempUC = new UControls();
tempUC.ControlType = control.Parent.Attribute("type").Value;
if (control.Parent.Attribute("DependingControl") != null)
{
tempUC.DependingControl = control.Parent.Attribute("DependingControl").Value;
}
foreach (XElement properties in DynProperties)
{
if (properties.Attribute("name").Value == "ID")
{
tempUC.ID = properties.Value;
}
if (properties.Attribute("name").Value == "BINDING")
{
tempUC.BindingFields = properties.Value;
}
if (properties.Attribute("name").Value == "POST_RULEID")
{
tempUC.post_Rulesid = properties.Value;
}
if (properties.Attribute("name").Value == "PRE_RULEID")
{
tempUC.pre_Rulesid = properties.Value;
}
}
Now we have control structure array and rule. here is the rule parser which generates database query by rule configuration file and business condition evaluation function.
public List<string> QuerySelectionRuleByControl(
IEnumerable<XElement> SelectRule, object gb,DataSet master)
{
var R_properties = from c in SelectRule.Descendants("PROPERTY")
select c;
GlobalData gd = (GlobalData)gb;
StringBuilder sb = new StringBuilder();
List<string> li = new List<string>();
string dataTextField = "";
string dataValueField = "";
string ruletype = "";
sb.Append("Select * ");
foreach (XElement x in R_properties)
{
ruletype = x.Parent.Attribute("type").Value;
if (x.Attribute("name").Value == "DataTextField")
{
dataTextField = ApplyLogic(x.Value, SelectRule, master, gd);
}
if (x.Attribute("name").Value == "DataValueField")
{
dataValueField = ApplyLogic(x.Value, SelectRule, master, gd);
}
if (x.Attribute("name").Value == "tablename")
{
sb.Append("from " + x.Value);
}
if (x.Attribute("name").Value == "condition")
{
string t = x.Value;
if (x.Value.Contains("?"))
{
var R_Parameters = from c in SelectRule.Descendants(
"PARAMETERS").Descendants("PARAMETER")
select c;
foreach (XElement xe in R_Parameters)
{
Control ParameterValueControl=null;
foreach (Control c in gd.xsl.ControlHolder.Controls[1].Controls)
{
if (c.ID == xe.Value.Split('.')[0].ToString())
{
ParameterValueControl = c;
}
}
if (ParameterValueControl != null)
{
if (ParameterValueControl.GetType().Name == "DropDownList")
{
string[] tempstr = xe.Value.Split('.');
DropDownList dTEmp = (DropDownList)ParameterValueControl;
if (tempstr[1] == "Value")
{
string pData = dTEmp.SelectedItem.Value;
t = x.Value.Insert(x.Value.IndexOf('?'), "'" + pData + "'");
}
if (tempstr[1] == "Text")
{
string pData = dTEmp.SelectedItem.Text;
t = x.Value.Insert(x.Value.IndexOf('?'), "'" + pData + "'");
}
}
}
if (xe.Value.Contains("RadioButtonList"))
{
string[] tempstr = xe.Value.Split('.');
RadioButtonList dTEmp = (RadioButtonList)ParameterValueControl;
if (tempstr[1] == "Value")
{
string pData = dTEmp.SelectedItem.Value;
t = x.Value.Insert(x.Value.IndexOf('?'), "'" + pData + "'");
}
if (tempstr[1] == "Text")
{
string pData = dTEmp.SelectedItem.Text;
t = x.Value.Insert(x.Value.IndexOf('?'), "'" + pData + "'");
}
}
if (xe.Value.Contains("CheckBoxList"))
{
string[] tempstr = xe.Value.Split('.');
CheckBoxList dTEmp = (CheckBoxList)ParameterValueControl;
if (tempstr[1] == "Value")
{
string pData = dTEmp.SelectedItem.Value;
t = x.Value.Insert(x.Value.IndexOf('?'), "'" + pData + "'");
}
if (tempstr[1] == "Text")
{
string pData = dTEmp.SelectedItem.Text;
t = x.Value.Insert(x.Value.IndexOf('?'), "'" + pData + "'");
}
}
if (xe.Value.Contains("TextBox"))
{
string[] tempstr = xe.Value.Split('.');
TextBox dTEmp = (TextBox)ParameterValueControl;
if (tempstr[1] == "Text")
{
string pData = dTEmp.Text;
t = x.Value.Insert(x.Value.IndexOf('?'), "'" + pData + "'");
}
}
if (xe.Value.Contains("ListBox"))
{
string[] tempstr = xe.Value.Split('.');
ListBox dTEmp = (ListBox)ParameterValueControl;
if (tempstr[1] == "Value")
{
string pData = dTEmp.SelectedItem.Value;
t = x.Value.Insert(x.Value.IndexOf('?'), "'" + pData + "'");
}
if (tempstr[1] == "Text")
{
string pData = dTEmp.SelectedItem.Text;
t = x.Value.Insert(x.Value.IndexOf('?'), "'" + pData + "'");
}
} if (xe.Value.Contains("GridView"))
{
string[] tempstr = xe.Value.Split('.');
GridView dTEmp = (GridView)ParameterValueControl;
if (tempstr[2] == "Text")
{
DataRow dr = ((DataRowView)dTEmp.SelectedRow.DataItem).Row;
string pData = dr[tempstr[1]].ToString();
t = x.Value.Insert(x.Value.IndexOf('?'), "'" + pData + "'");
}
}
if (xe.Value.Contains("Image"))
{
} if (xe.Value.Contains("BulletedList"))
{
}
if (xe.Value.Contains("HiddenField"))
{
}
if (xe.Value.Contains("Literal"))
{
}
if (xe.Value.Contains("Calender"))
{
}
if (xe.Value.Contains("master"))
{
string[] tempstr = xe.Value.Split('.');
string pData = master.Tables[tempstr[1]].Rows[0][tempstr[2]].ToString();
if (xe.Attribute("type").Value == "string")
{
t = x.Value.Insert(x.Value.IndexOf('?'), "'" + pData + "'");
t = t.Remove(t.IndexOf('?'), 1);
}
if (xe.Attribute("type").Value == "int")
{
t = x.Value.Insert(x.Value.IndexOf('?'), pData);
t = t.Remove(t.IndexOf('?'), 1);
}
}
}
}
sb.Append(" where " + ApplyLogic(t.Replace("?", ""), SelectRule, master, gd));
}
}
li.Add(sb.ToString());
li.Add(dataTextField);
li.Add(dataValueField);
li.Add(ruletype);
return li;
}
private string ApplyLogic(string str_blogic,
IEnumerable<XElement> SelectRule, DataSet master,GlobalData gd)
{
if(str_blogic.Contains("#"))
{
var R_Business = from c in SelectRule.Descendants("BUSINESS").Descendants("CONDITION")
select c;
foreach(XElement b_Condition in R_Business)
{
if (b_Condition.Descendants().First().Name.ToString() == "IF")
{
string leftterm = b_Condition.Descendants().First().Attribute("leftTerm").Value;
string rightterm = b_Condition.Descendants().First().Attribute("rightTerm").Value;
string op = b_Condition.Descendants().First().Attribute("op").Value;
string ConditionType = b_Condition.Descendants().First().Attribute("type").Value;
Control ParameterValueControlleft=null;
Control ParameterValueControlright=null;
foreach (Control c in gd.xsl.ControlHolder.Controls[1].Controls)
{
if (c.ID == leftterm.Split('.')[0].ToString())
{
ParameterValueControlleft = c;
}
if (c.ID == rightterm.Split('.')[0].ToString())
{
ParameterValueControlright = c;
}
}
if (ParameterValueControlleft != null)
{
if (ParameterValueControlleft.GetType().Name == "DropDownList")
{
string[] tempstr = leftterm.Split('.');
DropDownList dTEmp = (DropDownList)ParameterValueControlleft;
if (tempstr[1] == "Value")
{
leftterm = dTEmp.SelectedItem.Value;
}
if (tempstr[1] == "Text")
{
leftterm = dTEmp.SelectedItem.Text;
}
}
}
if (ParameterValueControlright != null)
{
if (ParameterValueControlright.GetType().Name == "DropDownList")
{
string[] tempstr = rightterm.Split('.');
DropDownList dTEmp = (DropDownList)ParameterValueControlright;
if (tempstr[1] == "Value")
{
rightterm = dTEmp.SelectedItem.Value;
}
if (tempstr[1] == "Text")
{
rightterm = dTEmp.SelectedItem.Text;
}
}
}
if (leftterm.Contains("master"))
{
string[] tempstr = leftterm.Split('.');
leftterm = master.Tables[tempstr[1]].Rows[0][tempstr[2]].ToString();
}
if (rightterm.Contains("master"))
{
string[] tempstr = rightterm.Split('.');
rightterm = master.Tables[tempstr[1]].Rows[0][tempstr[2]].ToString();
}
bool testResult = TestCondition(leftterm, rightterm,ConditionType, op);
if (testResult)
{
if (b_Condition.Descendants().First().Descendants().First().Attribute(
"TYPE").Value == "Text")
{
string str1 = b_Condition.Descendants().First().Descendants().First().Value;
str_blogic = str_blogic.Insert(str_blogic.IndexOf('#'), str1);
str_blogic= str_blogic.Remove(str_blogic.IndexOf('#'), 1);
}
}
else
{
XElement x= b_Condition.Descendants().Descendants("ELSE").First();
if (b_Condition.Descendants().Descendants("ELSE").First(
).Descendants().First().Attribute("TYPE").Value == "Text")
{
string str1 = b_Condition.Descendants().Descendants(
"ELSE").First().Descendants().First().Value;
str_blogic = str_blogic.Insert(str_blogic.IndexOf('#'), str1);
str_blogic = str_blogic.Remove(str_blogic.IndexOf('#'), 1);
}
}
}
}
return str_blogic;
}
else
{
return str_blogic;
}
}
private bool TestCondition(string obj1, string obj2, string objType, string op)
{
Type tt = Type.GetType(objType);
object lvalue = Convert.ChangeType(obj1, Type.GetType(objType));
object rvalue = Convert.ChangeType(obj2, Type.GetType(objType));
switch (op)
{
case "==":
bool temp = (lvalue.ToString() == rvalue.ToString());
return temp;
case ">":
if (lvalue.GetType().Name == "Int32")
return int.Parse(lvalue.ToString()) > int.Parse(rvalue.ToString());
else if (lvalue.GetType().Name == "Double")
return double.Parse(lvalue.ToString()) > double.Parse(rvalue.ToString());
else if (lvalue.GetType().Name == "Char")
return char.Parse(lvalue.ToString()) > char.Parse(rvalue.ToString());
else
return false;
case "<":
if (lvalue.GetType().Name == "Int32")
return int.Parse(lvalue.ToString()) < int.Parse(rvalue.ToString());
else if (lvalue.GetType().Name == "Double")
return double.Parse(lvalue.ToString()) < double.Parse(rvalue.ToString());
else if (lvalue.GetType().Name == "Char")
return char.Parse(lvalue.ToString()) < char.Parse(rvalue.ToString());
else
return false;
case "<=":
if (lvalue.GetType().Name == "Int32")
return int.Parse(lvalue.ToString()) <= int.Parse(rvalue.ToString());
else if (lvalue.GetType().Name == "Double")
return double.Parse(lvalue.ToString()) <= double.Parse(rvalue.ToString());
else if (lvalue.GetType().Name == "Char")
return char.Parse(lvalue.ToString()) <= char.Parse(rvalue.ToString());
else
return false;
case ">=":
if (lvalue.GetType().Name == "Int32")
return int.Parse(lvalue.ToString()) >= int.Parse(rvalue.ToString());
else if (lvalue.GetType().Name == "Double")
return double.Parse(lvalue.ToString()) >= double.Parse(rvalue.ToString());
else if (lvalue.GetType().Name == "Char")
return char.Parse(lvalue.ToString()) >= char.Parse(rvalue.ToString());
else
return false;
default:
return false;
}
}
Points of Interest
There is lot of scope to improve this concept to generate generic web pages for different users.