If you’re anything like me the thought of recursion can be quite scary and the fact that you still decided to read this post is impressive. Also before I start I want to point out that this is by far the most difficult thing I have done since I started programming. The problem I faced was that we needed to make a page that allowed the user to group rules together just like an if statement using AND and ORs to join the rules. The page needed to be able to support as many levels deep of grouping the user wished to go although in reality I find it hard to believe a user going more than two or three levels deep.
There was already a service in place that evaluated the rules and a data type that supported the recursive nature of the rules. There was just never a UI that allowed the rules to be grouped like this so really most of the difficult stuff was already taken care of. To understand the solution I will have to explain the type a little bit. I probably won’t be displaying a lot of code so I will do my best to explain all relevant details. To start we had a base type and two derived types, one for the ANDs and ORs and one for the criteria, like first name equals John. The ANDs and ORs had a list that was of the base type which allowed it to have lists of other ANDs and ORs which allowed for the recursion.
I thought a lot about how to implement page. I decided to go with user controls and repeaters not sure if it would work but i thought it offered the approach with the least code. A colleague had already given me the code to fill the data type so that was nice. I modified the base type to have a guid property for an id and a boolean property to indicate if the type was editable, yes if it wasn’t hard enough only some of the types would be editable at a time. The users would add and delete rules until the hit a save button which would then cause existing rules to not be editable and only allowed to be removed.
I started by created the first user control with a table with two rows. This control would be used to show the AND and OR criteria. The first row had a label the prompted them for the operand they wanted to add or the current AND or OR. It also had a drop down populated with AND and OR along with the properties they would be able to compare such as first and last name. Finally there was a button to add the Type to the existing types. The second column simply contained a repeater with a placeholder control in it. Next I created another user control, this one would be used to display the criteria that would be the values we were comparing.
Repeater’s onItemCreated method:
protected void dlRules_ItemCreated(object sender, DataListItemEventArgs e)
{
PlaceHolder holder = (PlaceHolder)e.Item.FindControl("phControl");
if (e.Item.DataItem is AndCriteria || e.Item.DataItem is OrCriteria)
{
Private_PolicyManagement_Controls_TestControl ctl = (Private_PolicyManagement_Controls_TestControl)LoadControl("~/Private/PolicyManagement/Controls/TestControl.ascx");
ctl.ID = ((Criteria)e.Item.DataItem).ID.ToString();// "ctl1";
ctl.Init += new EventHandler(ctl_Init);
ctl.RemoveCriteria += new EventHandler<CustomEventArgs>(ctl_RemoveCriteria);
ctl.Criteria = (Criteria)e.Item.DataItem;
ctl.AddNewCriteria += new EventHandler<CustomEventArgs>(ctl_AddNewCriteria);
holder.Controls.Add(ctl);
}
else
{
Private_PolicyManagement_Controls_TestControl2 ctl = (Private_PolicyManagement_Controls_TestControl2)LoadControl("~/Private/PolicyManagement/Controls/TestControl2.ascx");
ctl.ID = ((Criteria)e.Item.DataItem).ID.ToString();
ctl.RemoveCriteria += new EventHandler<CustomEventArgs>(ctl_RemoveCriteria);
ctl.DisplayData = (OrderCharacteristicCriterion)e.Item.DataItem;
holder.Controls.Add(ctl);
}
}
code from control2’s set property
public OrderCharacteristicCriterion DisplayData
{
set
{
if (value != null)
{
lbRemove.CommandArgument = value.ID.ToString();
if (value.IsEdit)
{
lblOperand.Visible = lblValue.Visible = false;
ddlComparer.Visible = ddlValue.Visible = true;
lblCharacteristic.Text = value.FriendlyName;
BindValueDropDown(value.Name);
if (value.Comparison == PricingEngineLib.Logic.Comparer.Comparison.Eq)
ddlComparer.SelectedIndex = 0;
else
ddlComparer.SelectedIndex = 1;
if (!string.IsNullOrEmpty(value.Value) && !string.IsNullOrEmpty(value.FriendlyValue))
{
ddlValue.SelectedValue = string.Format("{0}||{1}", value.Value, value.FriendlyValue);
}
}
else
{
lblCharacteristic.Text = value.FriendlyName;
lblOperand.Text = value.Comparison.ToString();
lblValue.Text = value.FriendlyValue;
}
}
}
}
Still with me? Good cause here is where the fun starts. Since I needed to store the data until the user clicked save I decided to use ViewState. Since some of the drop downs were going to have a lot of values in them I only stored the recursive type in ViewState and not the user controls. The first load was easy, in the init method I got the type and bound the controls. I made a property on the first user control of the base type. I then set the data source of the repeater to this property. In the ItemCreated method of the repeater I used reflection to get the type of object that was being bound to it. If it was an AND or OR I loaded the same user control set the same property and stuck it in the placeholder. Otherwise I loaded the second user control and set its property to display the comparison values. I wasn’t sure you could use the same user control inside of itself which was why I was hesitant with the method I chose but it worked and that’s all that mattered to me. I added a left margin on the first control’s table to get an indent and show the nesting.
Normally that feat would be enough for me but we also needed to capture input when the user added or changed values. This gets pretty hairy so bear with me. I wasn’t sure how to do this since I created the control tree every post back and after ViewState loaded for that matter. I thought this might be a problem since I was raising events in the users controls and wasn’t sure the page would catch the events. This is where those extra properties I added came in handy. In the set method of the second controls property I changed the ID of the user control to include the guid of the data type I was setting it to. Also in the ItemCreated method of the repeater I needed to bind the controls to events on the controls to raise events when an item was added or deleted. If the user clicked the add or delete button the page caught the event and ran a function that recursively went thru the type and found the appropriate criteria to add under or to remove. To do this I created a custom EventArg that I used to pass in the guid of the type i needed to act on. For the events caused by the auto postback of the drop downs of the values being edited i had a similar function that went thru the type but this function also went thru all of the values in the Request.Form object and split the values based on the ids of the drop downs that I changed looking for the appropriate values to act on.
Unfortunately after all that work we decided to go with a coworkers implementation that instead of using user controls dynamically added controls to the page while loading the type. It worked the same and just goes to show that there is always more than one way to accomplish things. I apologize if this was hard to follow and for the lack of source. If something is unclear or you want to see more source let me know. Hope you enjoyed.