Monday, 19 January 2009

Dynamic Data – Cascading FieldTemplates

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.

Cascading ForeignKey_Edit like FieldTemplates

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:

  1. A way to get the parent DetailsView from inside a FieldTemplate.
  2. Some way of finding the FieldTemplate from the parent control tree of the dependant FileTemplate.
  3. Some way of getting the dependee FieldTemplate to fire an event on the dependant FieldTemplate when a change is made on dependee.
  4. 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 smile_teeth.

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:

  1. The properties that allow a the control’s OnSelectedIndexChanges event to be captured.
  2. 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).

Diagram of changes to Order_Details table

Figure 2 – Diagram of changes to Order_Details table

Added Relationship in the Model

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 HappyWizard

Oh and the Download

30 comments:

Anonymous said...

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?

Stephen J. Naughton said...

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

Anonymous said...

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

Stephen J. Naughton said...

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

Deepesh said...

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,

Anonymous said...

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?

Stephen J. Naughton said...

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

Anonymous said...

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

Stephen J. Naughton said...

It's probably a syntax error it's easy with generics :D

Steve

Anonymous said...

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.

Stephen J. Naughton said...

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

Anonymous said...

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;}..

Stephen J. Naughton said...

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

Paweł Szewczyk said...

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!

Stephen J. Naughton said...

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

Anonymous said...

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

Stephen J. Naughton said...

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

GVF said...

Is it possible to find this code in VB?

Stephen J. Naughton said...

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

GVF said...

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.

Stephen J. Naughton said...

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

Anonymous said...

Hi Stephen, You're the best man. Thanks for sharing your knowledge.

Giri said...

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.

Stephen J. Naughton said...

It sound like you have a property in your metadata that does not match aproperty in your model.

Steve

safknw said...

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.

kapil kshirsagar said...

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 ?

Stephen J. Naughton said...

Hi kapil, this is a very old post try my http://nuget.org/List/Packages/NotAClue.DynamicData.CustomFieldTemplates on http://nuget.org

Steve

Stephen J. Naughton said...

sorry wrong Nuget package try this one http://nuget.org/List/Packages/NotAClue.DynamicData.Cascade

Steve

kapil kshirsagar said...

thanx steve. :)

Unknown said...

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