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.
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 .
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; }
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;} } }
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
Oh and the Download
30 comments:
Thanks. Great work.
But one thing I dont understand. We should fix all tables where we want to use cascading with script like in listing 11? I think it is not very good.
Also can we use it in Dynamic Data EF projects?
Sorry no Listing 11 is just for the sample as I'd messed about with Northwind. I added a column and I then populated it with data from the Products table. This is a paticular case where you have two or more columns in a table that would filter each other i.e. say you have a Products table that has two FK columns say Categories and Manufacurers where you would only want to show manufacurers who mached a category.
Hope that makes sense I'll update the article to say so regardign the use of the script
Hello,
thanks for this blog, exactly what I need at the moment :-)
But I have a problem: When I want to do an insert or edit, it is never executed. I can see in SQL Profiler, that there is a NULL value (which is not allowed in DB) instead of the category ID. Moreover the category ID and the product ID change place:
HTML:
...
categoryID = 11
productID = 19
...
SQL:
...
@p2 = 19
@p3 = NULL
...
In the HTML page I can see the correct values in the category dropdown, but it gets somehow lost while executing the INSERT statement.
Can you give e a hint?
Thanks a lot,
Alex
I recommend you post a question on the Dynamic Data Forum here http://forums.asp.net/1145.aspx Details you should give are Model Linq to SQL or Entity Framework, and try and pose you question from a well known DB lkke Northwind because thisway we will be abvle to repro it ourselves give as much detail as you can see this post by David Ebbo on wht to include http://blogs.msdn.com/davidebb/archive/2009/01/11/tips-on-getting-your-asp-net-dynamic-data-questions-answered-quickly.aspx
Steve :D
Hello,
I have a dropdownlist in the DetailView of the List page. Which is bind by the values of other table. We need to have an selectIndexchange event on the dynamicdatacontrol.
Can you please tell us, how we can have this?
Thanks,
Hello Stephen,
thanks for this blog, i read your job and i implement this solution on my project..but this idea dosn't work for me. When i build the solution i receive this error :"Error 11 No overload for method 'GetDependeeField' takes '1' arguments". The method in class ExtensionMethods take two arguments..like [public static AdvancedFieldTemplate GetDependeeField 'T' (this Control control, MetaColumn column) where T : Control] but in the Page_Load method we pass only one Arguments..maybe i'm missing anything?
Have you downloaded the project and tested that?
Note: with Northwind you need to hack the relationships and primary keys I can supply a sample if you like.
Steve
Yes i downloaded zip file..i create a new project for test your job after that i tested the code on my DynamicData Web Application, but building error is also relieved. I don't understand where is my error...i don't find GetDependeeField overload extension method that accepts only 1 arguments like you used on "Listing 8 – Page_Load event". The only implemantion that i found is in "Listing 8 – Page_Load event" and we pass two arguments. Tanks for your help..and sorry for my english.. :-D
Nicola
It's probably a syntax error it's easy with generics :D
Steve
How we can use this example for another scenario, because this we can use only in particular case where you have two or more columns in a table that would filter each other.
I want to develop cascading data from another tables, for example Category, Subcategory and Products tables.
I tried to use your another article for this case http://csharpbits.notaclue.net/2008/08/dynamic-data-and-field-templates-second.html#comment-form
.
But there is some bugs in Edit mode.
Thanks.
Hi Azamat, I would for the time being create a custom FieldTemplate for this which is what I've had to do, I just havent had the time to look at the Expression syntax that would be needed to show the edit value in cascade.
Sorry Steve :D
Hi stephen, i'm Nicola, i re-test your code on my project and now it's work fine with 3 DropDownList (i've Category---> Type ---> SubType), but there is only a problem. When i change the Category the event handler not have effect on my third Dropdown (that is called SubType) but works fine on the Types DropDown. Do Yuo have any Idea how i can handled the event on Category changing that is submitted to sutype DropDownList? Is possible make two DependeeColum on the same property? Like this..
[UIHint("Type")]
[DependeeColumn("Category")]
public object Type {get;set;}
[UIHint("subType")]
[DependeeColumn("Category")]
[DependeeColumn("Type")]
public object SubType {get;set;}
[UIHint("Category")]
public object Category {get;set;}..
It looks to me like you have two DependeeColumn attribute on one column here:
[DependeeColumn("Category")]
[DependeeColumn("Type")]
public object SubType {get;set;}
What I've found is the cascade only ripples one level at a time, so:
1st level a value is selected and then 2nd level is reset to be limited to the selected value of the 1st level,
2nd level a value is selected and so the third level is limited to the value selected in the second level.
And so on...
What I would like to happen is until a value is selected in the dependee column for the DDL to be disabled. thus forcing you to select the 1st, then 2nd, then 3rd etc.
I will probably get to this in the next release of DD or atleast the next preview of that release.
Steve :D
Hello,
Thanks for all your work and examples. Could you perhaps check whether it would be possible to use this solution with .NET 4.0? I tried and get null reference exception when setting dependeeField around line 60 of Product_Edit.ascx.cs. Currently I'm using it with .NET 3.5 SP1, but would like to migrate to 4.0 when it becomes RTM.
Let me know if you need some more details!
Hi Pawel, I'm waiting for Beta 2 which should have more of DD that will ship in .net 4.0. but at the moment it should work as there are no real differences between DD v1.0 and whats in VS2010 Beta 1.
When I get a moment I will run the sample up in VS2010 Beta 1 and see what happens.
As I side note I will be converting all my sample to VS2010 when we get to Beta 2 and will put some on codeplex.com
Steve :D
P.S. feel free to e-mail me :D
Hey, thanks a lot for a great blog.
I have been trying to ajust you samle to work with listview but without success.
In the module ExtensionMethodes the
// setup the selection event
if (dependeeDynamicControl != null)
dependeeField = dependeeDynamicControl.Controls[0] as AdvancedFieldTemplate;
dependeeField always get set to null...
Do you have any sugestion how i can do this ?
thanks in advance
/peter
Hi Peter, the reasone is that in a gridview you could perhaps search the Row for the fieldTemplate but in a ListView you have no knowledge of the structure that the FieldTemplate is in.
Steve :D
Is it possible to find this code in VB?
I do have a sample in VB just send me an e-mail an I'll dig it out, I did it for a guy in Hong Kong
But you are on your own with VB :( I get a head ache every time I go back to VB.
Steve
I didn't knew c# was so popular (I'm begining with .net) I think I'm going to migrate my project to c# and use DD Futures too.
Yes C# is cool im my opinion not because it is any better than VB but because it is so like JavaScript, Java, ActionScript etc.
Steve :D
Hi Stephen, You're the best man. Thanks for sharing your knowledge.
i am getting the following error when i run the application.
The associated metadata type for type 'DynamicDataFutures.Order_Detail' contains the following unknown properties or fields: Category. Please make sure that the names of these members match the names of the properties on the main type.
It sound like you have a property in your metadata that does not match aproperty in your model.
Steve
I'm getting NullReferenceException in function FindDynamicControlRecursive
a line (no 93) foreach (Control Ctl in root.Controls).
My project is in asp.net 4.0, can u tell me what I'm doing wrong.
Hi,
I have implemented this code for
country and state dropdownlist,
the build is successful but on running it it giving
"Could not find key member 'ID' of key 'ID' on type 'State'. The key may be wrong or the field or property on 'State' has changed names."
why this error occurring ?
Hi kapil, this is a very old post try my http://nuget.org/List/Packages/NotAClue.DynamicData.CustomFieldTemplates on http://nuget.org
Steve
sorry wrong Nuget package try this one http://nuget.org/List/Packages/NotAClue.DynamicData.Cascade
Steve
thanx steve. :)
Thanks, I found this really helpful. Not understanding this that well at first, it took me a while to figure out that I had to change the parameter to match the structure in my page -- in my case a . Worked great for me after that!
Mark
Post a Comment