This is an article is a follow on from my previous article Cascading or Dependant Field Templates for ASP.Net 4.0 Preview and is the start of a series of three articles which describe Cascading Filters for:
- Dynamic Data (v1.0) .Net 3.5 SP1
- Dynamic Data Futures (Futures on Codeplex)
- Dynamic Data Preview 3 (Preview 3 on Codeplex)
This first version is pretty similar to the previous article on cascading FieldTemplates here we will adapt the CascadingFieldTemplate class to facilitate our need for cascading filters.
Again there will be a new event that will be used for the cascade allowing us to fire the event other than when the dropdown list in changes (i.e. then the parent is changed and we need to pass on the cascade to the next child)
Figure 1 – Filters that need to be cascaded
In Figure 1 we want Developer to be filtered by Builder and HouseType to in turn be filtered by Development so we see in Figure 2 the desired result, you cannot select a Developer or HouseType until it’s parent as been selected.
Figure 2 – Cascading Filters showing cascade in default
I’m not going to go into a deep explanation here as the code is well commented, I will point out were the differences between the cascading file and filter are.
using System; using System.Collections.Generic; using System.Linq; using System.Web; /// <summary> /// Attribute to identify which column to use as a /// parent column for the child column to depend upon /// </summary> public class CascadeAttribute : Attribute { /// <summary> /// Name of the parent column /// </summary> public String ParentColumn { get; private set; } /// <summary> /// Default Constructor sets ParentColumn /// to an empty string /// </summary> public CascadeAttribute() { ParentColumn = ""; } /// <summary> /// Constructor to use when /// setting up a cascade column /// </summary> /// <param name="parentColumn">Name of column to use in cascade</param> public CascadeAttribute(string parentColumn) { ParentColumn = parentColumn; } }
Listing 1 – CascadeAttribute this is the same as in the Filter example
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web.DynamicData; using System.Web.UI.WebControls; using System.Linq.Expressions; using System.Web.UI; /// <summary> /// Event Arguments for Category Changed Event /// </summary> public class SelectionChangedEventArgs : EventArgs { /// <summary> /// Custom event arguments for SelectionChanged /// event of the CascadingFieldTemplate control /// </summary> /// <param name="value"> /// The value of the currently selected /// value of the parent control /// </param> public SelectionChangedEventArgs(String value) { Value = value; } /// <summary> /// The values from the control of the dependee control /// </summary> public String Value { get; set; } } /// <summary> /// Modifies the standard FilterUserControlBase /// to support cascading of selected values. /// </summary> public class CascadingFilterTemplate : System.Web.DynamicData.FilterUserControlBase { #region Properties /// <summary> /// Data context /// </summary> private object DC; /// <summary> /// This controls list control /// </summary> public ListControl ListControl { get; private set; } /// <summary> /// Parent column of this column named in metadata /// </summary> public MetaForeignKeyColumn ParentColumn { get; private set; } /// <summary> /// This FieldTemplates column as MetaForeignKeyColumn /// </summary> public MetaForeignKeyColumn ChildColumn { get; private set; } /// <summary> /// Parent control acquired from ParentColumn /// </summary> public CascadingFilterTemplate ParentControl { get; set; } #endregion protected virtual void Page_Init(object sender, EventArgs e) { DC = Column.Table.CreateContext(); // get the parent column var parentColumn = Column.GetAttributeOrDefault<CascadeAttribute>().ParentColumn; if (!String.IsNullOrEmpty(parentColumn)) ParentColumn = Column.Table.GetColumn(parentColumn) as MetaForeignKeyColumn; // cast Column as MetaForeignKeyColumn ChildColumn = Column as MetaForeignKeyColumn; //TODO: find a way of determining which the parent control is DetailsView or FormView // get dependee field (note you must specify the // container control type in <DetailsView> or <FormView> ParentControl = GetParentControl(); } /// <summary> /// Delegate for the Interface /// </summary> /// <param name="sender"> /// A parent control also implementing the /// ISelectionChangedEvent interface /// </param> /// <param name="e"> /// An instance of the SelectionChangedEventArgs /// </param> public delegate void SelectionChangedEventHandler( object sender, SelectionChangedEventArgs e); //publish event public event SelectionChangedEventHandler SelectionChanged; /// <summary> /// Raises the event checking first that an event if hooked up /// </summary> /// <param name="value">The value of the currently selected item</param> public void RaiseSelectedIndexChanged(String value) { // make sure we have a handler attached if (SelectionChanged != null) { //raise event SelectionChanged(this, new SelectionChangedEventArgs(value)); } } // advanced populate list control protected void PopulateListControl(ListControl listControl, String filterValue) { //get the parent column if (ParentColumn == null) { // if no parent column then just call // the base to populate the control PopulateListControl(listControl); // make sure control is enabled listControl.Enabled = true; } else if (String.IsNullOrEmpty(filterValue)) { // if there is a parent column but no filter value // then make sure control is empty and disabled listControl.Items.Clear(); listControl.Items.Add(new ListItem("[Not Set]", "")); // make sure control is disabled listControl.Enabled = false; } else { // get the child columns parent table var childTable = ChildColumn.ParentTable; // get parent FiledTeamlate string[] parentColumnPKV = filterValue.Split(','); // this is where I use that file from Dynamic Data Futures var parentFieldTemplate = GetSelectedParent( parentColumnPKV, ParentColumn.ParentTable); // get list of values filters by the parent's selected entity var itemlist = GetQueryFilteredByParent( childTable, ParentColumn, parentFieldTemplate); // clear list controls items collection before adding new items listControl.Items.Clear(); listControl.Items.Add(new ListItem("[Not Set]", "")); // add returned values to list control foreach (var row in itemlist) listControl.Items.Add( new ListItem( childTable.GetDisplayString(row), childTable.GetPrimaryKeyString(row))); // make sure control is enabled listControl.Enabled = true; } } /// <summary> /// Get the entity value of the selected /// value of the parent column /// </summary> /// <param name="primaryKeyValues"> /// An array of primary key values /// </param> /// <param name="parentTable"> /// Parent columns FK table /// </param> /// <returns> /// Returns the currently selected entity /// from the parent list as an object /// </returns> private object GetSelectedParent( string[] primaryKeyValues, MetaTable parentTable) { var query = parentTable.GetQuery(DC); // Items.Where(row => row.ID == 1).Single() var singleWhereCall = LinqExpressionHelper.BuildSingleItemQuery( query, parentTable, primaryKeyValues); return query.Provider.Execute(singleWhereCall); } /// <summary> /// Returns an IQueryable of the current FK table filtered by the /// currently selected value from the parent filed template /// </summary> /// <param name="childTable"> /// This columns FK table /// </param> /// <param name="parentColumn"> /// Column to filter this column by /// </param> /// <param name="selectedParent"> /// Value to filter this column by /// </param> /// <returns> /// An IQueryable result filtered by the parent columns current value /// </returns> private IQueryable GetQueryFilteredByParent (MetaTable childTable, MetaForeignKeyColumn parentColumn, object selectedParent) { // get query {Table(Developer)} var query = ChildColumn.ParentTable.GetQuery(DC); // {Developers} var parameter = Expression.Parameter(childTable.EntityType, childTable.Name); // {Developers.Builder} var property = Expression.Property(parameter, parentColumn.Name); // {value(Builder)} var constant = Expression.Constant(selectedParent); // {(Developers.Builder = value(Builder))} var predicate = Expression.Equal(property, constant); // {Developers => (Developers.Builder = value(Builder))} var lambda = Expression.Lambda(predicate, parameter); // {Table(Developer).Where(Developers => (Developers.Builder = value(Builder)))} var whereCall = Expression.Call(typeof(Queryable), "Where", new Type[] { childTable.EntityType }, query.Expression, lambda); // generate the query and return it return query.Provider.CreateQuery(whereCall); } /// <summary> /// Gets the Parent control in a cascade of controls /// </summary> /// <param name="column"></param> /// <returns></returns> private CascadingFilterTemplate GetParentControl() { // get the parent container var parentDataControl = GetContainerControl(); if (ParentColumn != null) { // Get Parent FieldTemplate return parentDataControl.FindFilterControlRecursive(ParentColumn.Name) as CascadingFilterTemplate; } return null; } /// <summary> /// Get the Data Control containing the FiledTemplate /// usually a DetailsView or FormView /// </summary> /// <param name="control"> /// Use the current field template as a starting point /// </param> /// <returns> /// A FilterRepeater the control that /// contains the current control /// </returns> private FilterRepeater GetContainerControl() { var parentControl = this.Parent; while (parentControl != null) { var p = parentControl as FilterRepeater; if (p != null) return p; else parentControl = parentControl.Parent; } return null; } }
Listing 2 – the CascadingFilterTemplate
The main differences here we are no longer testing for Insert or Edit mode as they don't apply to this control and in the area of acquiring the parent control we now search for a different control type both for the container control and the actual template control. Also not that in the GetParentControl method we don’t have to extract the control from a container this is because it’s the same user control each time.
/// <summary> /// Get the DynamicControl by searching recursively for it by DataField. /// </summary> /// <param name="Root">The control to start the search at.</param> /// <param name="Id">The DataField of the control to find</param> /// <returns>The found control or NULL if not found</returns> public static Control FindFilterControlRecursive(this Control root, string dataField) { var dc = root as CascadingFilterTemplate; //Category if (dc != null) { if (String.Compare(dc.DataField, dataField, true) == 0) return dc; } foreach (Control Ctl in root.Controls) { Control FoundCtl = FindFilterControlRecursive(Ctl, dataField); if (FoundCtl != null) return FoundCtl; } return null; }
Listing – 3 FindFilterControlRecursive
Listing 3 show the extension method which if I combined both the cascading FieldTemplates and FilterTemplates I would make this a generic extension method:
public static Control FindDynamicControlRecursive<T>(this Control root, string dataField)
where T : Control
So I could reuse the method for either.
Download (UPDATED)
And here’s the sample website with a script to create the database and an excel worksheet with table data in it.
Next we will cover getting this working with the old Dynamic Data Futures. |
9 comments:
Hi,
There are very useful articles about cascading filters, great work.
But there are some issues with navigating from one entity page to another with filters. For example, from Developers page to RequieredPlots.
There is exception
DropDownList1' has a SelectedValue which is invalid because it does not exist in the list of items.
Parameter name: value
[ArgumentOutOfRangeException: 'DropDownList1' has a SelectedValue which is invalid because it does not exist in the list of items.
Parameter name: value]
System.Web.UI.WebControls.ListControl.PerformDataBinding(IEnumerable dataSource) +2713781
System.Web.UI.WebControls.ListControl.PerformSelect() +49
System.Web.UI.Control.DataBindChildren() +11024287
System.Web.UI.Control.DataBind(Boolean raiseOnDataBinding) +182
System.Web.UI.Control.DataBindChildren() +11024287
System.Web.UI.Control.DataBind(Boolean raiseOnDataBinding) +182
System.Web.UI.WebControls.Repeater.CreateControlHierarchy(Boolean useDataSource) +674
System.Web.UI.WebControls.Repeater.OnDataBinding(EventArgs e) +68
System.Web.DynamicData.FilterRepeater.DataBind() +31
System.Web.DynamicData.FilterRepeater.Page_InitComplete(Object sender, EventArgs e) +47
System.EventHandler.Invoke(Object sender, EventArgs e) +0
System.Web.UI.Page.OnInitComplete(EventArgs e) +11035598
System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +1674
Have you thought about this issues?
Hi Азамат, you are right I will look into this but I will deal with it in the latest version.
http://csharpbits.notaclue.net/2009/04/cascading-filters-and-fields-dynamic.html as this is EF compatible
And I will post updates there :D
Steve :D
Hi Азамат, you will need to add this to the line in the Page_Init where it tests the InitialValue the DropDownList1.Items.Count > 1
I'm going to update the downloads now
Thanks for your vigilance :D
Steve
This bug was due to keeping the DDL's empty until the partent was populated :D
Hi Азамат, I've fixed all the downloads now :D
Remember you can message me no via Digby on my blog when I'm online, just ping me
Steve :D
Hi Steve
I have a question in relation to your "GetQueryFilteredByParent" function. Correct me if I'm wrong, but as far as I understand it, it doesn't work for entity data modell where I would like to use the navigation properties to get a result?!
A short example (1:n relation)
table Customers
---------------
CustomerID (uniqueIdentifier)
Name (text)
Area (navigation property)
table Areas
-----------
AreaID (uniqueIdentifier)
Name (text)
Customers (navigation property)
What I would like to do now, is to show only customers that belong to a certain area using your cascading filter
The problem is now, that something like:
var customers = (From c In ctx.Customers Where c.Area == SelectedArea Select c);
won't work. What should work is something like:
var customers = (From c In ctx.Customers Where c.Area.AreaID == SelectedArea.AreaID Select c);
But for this situtation I'm unable to build a proper "GetQueryFilteredByParent" function.
It would be really fantastic, if you could help me out. I'm loosing my hair...
Thank you very much
Yes you are correct this has been updated by this article (as this version was not EF compatible) http://csharpbits.notaclue.net/2009/04/cascading-filters-and-fields-dynamic.html the new uses feature I found in the Preview 3 filter template ForeignKey.aspx.cs
Steve :D
Thanks!! Your sample was a big help.
I haven't actually tried to implement the cascading filters yet but I had it bookmarked to try later when I ran into a situation where I needed an Expression to return a row. I'm having a difficult time understanding Expressions but your GetSelectedParent function does exactly what I needed.
Now I just need to find a plausable place to insert cascading filters into the project :-)
Hi Wes, yes Expression are hard :( but the old Futures project still on codeplex has some good examples, ;-) that's where most of my code comes from :D
Steve
Post a Comment