So before I dive into an explanation of this feature I’d better explain what I’m doing here. Firstly have a look at Figure 1.
Figure 1 – Cascading ForeignKey_Edit like FieldTemplates
In Figure 1 you can see two DropDownLists Category and Products is this form you are only allowed to choose a product the matches the selected category. So what is required, is when Category is changes then the contents of Product should be regenerated to be filtered by the chosen Category. I hope that make some sense.
The requirements for the above to work are:
- A way to get the parent DetailsView from inside a FieldTemplate.
- Some way of finding the FieldTemplate from the parent control tree of the dependant FileTemplate.
- Some way of getting the dependee FieldTemplate to fire an event on the dependant FieldTemplate when a change is made on dependee.
- An Attribute to tell the control which control to use as the dependee control.
Note: I am using the words dependant for the field that depends upon another for filtering and dependee for the field that is depended upon.
1. Getting the parent control
The first step would be to climb the parent tree using a generic extension method.
/// <summary>
/// Get a parent control of T from the parent control tree of the control
/// </summary>
/// <typeparam name="T">Control of type T</typeparam>
/// <param name="control">Control to search in for control type T</param>
/// <returns>The found control of type T or null if not found</returns>
public static T GetParent<T>(this Control control) where T : Control
{
var parentControl = control.Parent;
while (parentControl != null)
{
var formView = parentControl as T;
if (formView != null)
return formView;
else
parentControl = parentControl.Parent;
}
return null;
}
Listing 1 – Get Parent generic extension method
I decided that a generic method was required as I had not only the DetailsView but also the FormView to deal with. The logic is simple here all that happens is that the parent of the current cointrol is cast as the T type and if it is not null then we have the control we are looking for, if it is null then the loop continues and the parent of the parent is tested and so on until a match is found or a parent is null. This get’s us the hosting control of the type we want.
2. Finding the Dependee FieldTemplate
At first I thought I could use the handy extension method FindFieldTemplate but it always returned null. So I had to go my own way and came up with this:
/// <summary>
/// Get the control by searching recursively for it.
/// </summary>
/// <param name="Root">The control to start the search at.</param>
/// <param name="Id">The ID of the control to find</param>
/// <returns>The control the was found or NULL if not found</returns>
public static Control FindDynamicControlRecursive(this Control root, string dataField)
{
var dc = root as DynamicControl;
if (dc != null)
{
if (dc.DataField == dataField)
return dc;
}
foreach (Control Ctl in root.Controls)
{
Control FoundCtl = FindDynamicControlRecursive(Ctl, dataField);
if (FoundCtl != null)
return FoundCtl;
}
return null;
}
Listing 2 – FindDynamicControlRecusive
This is based on a function I found here How to find a control when using Master Pages the change is simple all I do is cast the control as DynamicControl and then test for null if not null then I can test the DataField which I know had the column name in it.
This however only gets us the DynamicControl next we have to extract the field which is held in the DynamicControl’s Controls collection
// Get Parent FieldTemplate
var dependeeDynamicControl = detailsView.FindDynamicControlRecursive(dependeeColumn.ColumnName) as DynamicControl;
AdvancedFieldTemplate dependeeField = null;
// setup the selection event
if (dependeeDynamicControl != null)
dependeeField = dependeeDynamicControl.Controls[0] as AdvancedFieldTemplate;
Listing 3 – Fragment: Extracting the FieldTEmplate from the DynamicControl
Now providing that the control we are after in in slot [0] of the DynamicControl’s Controls collection we are away .
Note: proved to be the most difficult due to differences between DetailsView and FormView which was what my original was working with.
3. Getting an event fired on the Dependant FieldTemplate when the Dependee’s DropDownList changes
Ok for this to work we need to expose the DropDownList’s OnSelectedIndexChanged event and also the SelectedValue property, here’s the code for that in Listing 4.
public override event EventHandler SelectionChanged
{
add
{
DropDownList1.SelectedIndexChanged += value;
}
remove
{
DropDownList1.SelectedIndexChanged -= value;
}
}
public override string SelectedValue
{
get
{
return DropDownList1.SelectedValue;
}
}
Listing 4 – Exposing the OnSelectedIndexChanged event and the SelectedValue property
This is fine but when I get a copy of the control from the earlier code I need to get access to this exposed event and property. So here's how we will do that we’ll create a class that inherits
/// <summary>
/// A class to add some extra features to the standard
/// FieldTemplateUserControl
/// </summary>
public class AdvancedFieldTemplate : FieldTemplateUserControl
{
/// <summary>
/// Handles the adding events to the drop down list
/// </summary>
public virtual event EventHandler SelectionChanged
{
add { }
remove { }
}
/// <summary>
/// Returns the selected value of the drop down list
/// </summary>
public virtual string SelectedValue
{
get
{
return null;
}
}
}
Listing 5 – AdvancedFieldTemplate class
As you can see from Listing 5 this class inherits the FieldTemplateUserControl class the a FieldTemplate inherits, so we inherit that and then on the custom FieldTemplates we want to cascade we set them to inherit the AdvancedFieldTemplate.
Next we need an event handler to handle the event passed to this the dependant control.
protected void SelectedIndexChanged(object sender, EventArgs e)
{
var ddl = sender as DropDownList;
if(ddl != null)
PopulateListControl(DropDownList1, ddl.SelectedValue);
}
Listing 6 – Event handler
4. The Attribute
Attribute have been cover a lot on this site so I’m just going to post the code and comment a very little.
[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public class DependeeColumnAttribute : Attribute
{
public static DependeeColumnAttribute Default = new DependeeColumnAttribute();
public DependeeColumnAttribute() : this("") { }
public DependeeColumnAttribute(String columnName)
{
ColumnName = columnName;
}
public String ColumnName { get; set; }
}
Listing 7 – DependeeColumnAttribute
I could of course hard coded the dependee column in to each custom FieldTemplate but of course that assumes that every time you use the FieldTEmplate the dependee column will have the same name, which is not always the case.
5. Putting it All Together
Firstly we need the extension method that get us the dependee control:
public static AdvancedFieldTemplate GetDependeeField<T>(this Control control, MetaColumn column) where T : Control
{
// get value of dev ddl (Community)
var detailsView = control.GetParent<T>();
// get parent column attribute
var dependeeColumn = column.GetAttribute<DependeeColumnAttribute>();
if (dependeeColumn != null)
{
// Get Parent FieldTemplate
var dependeeDynamicControl = detailsView.FindDynamicControlRecursive(dependeeColumn.ColumnName) as DynamicControl;
AdvancedFieldTemplate dependeeField = null;
// setup the selection event
if (dependeeDynamicControl != null)
dependeeField = dependeeDynamicControl.Controls[0] as AdvancedFieldTemplate;
return dependeeField;
}
return null;
}
Listing 7 – GetDependeeField
In Listing 7 we return the FieldTemplate extracted from the found DynamicControl so in the Page_Load event we get the dependee control and assign it the event handler so that we can capture the SelectedIndexChanged event of the dependee DropDpownList.
protected void Page_Load(object sender, EventArgs e)
{
if (DropDownList1.Items.Count == 0)
{
if (!Column.IsRequired)
DropDownList1.Items.Add(new ListItem("[Not Set]", ""));
PopulateListControl(DropDownList1, "");
}
// get dependee field
var dependeeField = this.GetDependeeField<DetailsView>(Column);
// add event handler if dependee exists
if (dependeeField != null)
dependeeField.SelectionChanged += SelectedIndexChanged;
}
Listing 8 – Page_Load event
The lines in BOLD ITALIC are the added lines and demonstrates the use of GetDependeeField extension method.
protected override void OnDataBinding(EventArgs e)
{
base.OnDataBinding(e);
if (Mode == DataBoundControlMode.Edit)
{
var dependeeField = this.GetDependeeField<DetailsView>(Column);
if (dependeeField != null)
PopulateListControl(DropDownList1, dependeeField.SelectedValue);
string foreignkey = ForeignKeyColumn.GetForeignKeyString(Row);
ListItem item = DropDownList1.Items.FindByValue(foreignkey);
if (item != null)
{
DropDownList1.SelectedValue = foreignkey;
}
}
}
Listing 9 – OnDataBinding event handler
Again in BOLD ITALIC are line are are added.
An finally some metadata
[MetadataType(typeof(Order_DetailMD))]
public partial class Order_Detail
{
public class Order_DetailMD
{
public object OrderID {get;set;}
public object ProductID {get;set;}
public object UnitPrice {get;set;}
public object Quantity {get;set;}
public object Discount {get;set;}
public object CategoryID {get;set;}
// EntityRefs
[DependeeColumn("Category")]
[UIHint("Product")]
[ColumnOrder(2)]
public object Product {get;set;}
[UIHint("Category")]
[ColumnOrder(1)]
public object Category {get;set;}
}
}
Listing 10 – Metadata
In the attached project I've also implemented some column ordering to arrange the columns in the order that makes sense if they cascade.
There are two part to this solution which I didn’t make clear:
- The properties that allow a the control’s OnSelectedIndexChanges event to be captured.
- Finding the dependee control and capturing it’s OnSelectedIndexChanges event and reading it’s SelectedValue.
It must be noted that if both controls implement all the features there won’t be a problem but if you do it in only the dependant control the you won’t be able to capture the OnSelectedIndexChanges event of the dependee control or read it’s SelectedValue. So you must at least implement 1. in the dependee control and 2. in the dependant control.
I hope this clarifies things.
Setting up Northwind to work with the sample
Here’s a diagram showing the changes I made to Northwind to facilitate this example (as there were no columns that fitted this scenario).
Figure 2 – Diagram of changes to Order_Details table
Figure 3 – Added Relationship in the Model
And here’s the T-SQL to update the CategoryID column in Order_Details once you've added it.
USE [Northwind]
GO
UPDATE [Order Details]
SET [Order Details].[CategoryID] = p.[CategoryID]
FROM [Products] p
WHERE p.[ProductID] = [Order Details].[ProductID]
SELECT * FROM [Order Details]
Listing 11 – SQL to update Oder_Details
Note: This listing is just to make changes to Northwind so it fits this scenario.
And that's it
Oh and the Download