- Part 1 – The Read-Only Hierarchical Field Template
- Part 2 – The Edit Hierarchical Field Template
- Part 3 – The Basic Hierarchical Filter
This would have been the final part of the series of articles, however I realised that we will need a final article that covers the more complex action of the CascadingHierarchicalFilter, this where at each level of selection we filter the list.
Figure 1 – Cascading Hierarchical Filter.
e.g. in Figure 1 the CascadingHierarchicalFilter (the basic version) no selection occurs until we select a value from the final list control, however in the complex CascadingHierarchicalFilter we will filter at each level and so we will hold that over until we have completed the basic version of the CascadingHierarchicalFilter.
And so to the basic version, first we will need a starting place so copy the ForeignKey filter and rename it to CascadingHierarchical and the class name to CascadingHierarchicalFilter. Next we remove the content of the aspx page so it looks like Listing 1.
<%@ Control Language="C#" CodeBehind="CascadeHierarchical.ascx.cs" Inherits="CascadeHierarchicalFieldTemplate.CascadeHierarchicalFilter" %>
Listing 1 – CascadingHierarchical.aspx page
Then we start on the code behind by adding our member variables, adjusting the two properties and replacing the default Page_Init with ours, which is very similar to the Edit templates Page_Init see Listing 2.
#region member variables private const string NullValueString = "[null]"; // hold the current data context to access the model private object context; // hold the list of filters private SortedList<int, HierachicalListControl> filters = new SortedList<int, HierachicalListControl>(); // hold the attribute private CascadeHierarchicalAttribute cascadeHierarchicalAttribute; #endregion private new MetaForeignKeyColumn Column { get { return (MetaForeignKeyColumn)base.Column; } } public override Control FilterControl { get { if (filters[0] != null && filters[0].ListControl != null) return filters[0].ListControl; else return null; } } protected void Page_Init(object sender, EventArgs e) { //if (!Column.IsRequired) // check we have a cascade hierarchical attribute if not throw error cascadeHierarchicalAttribute = Column.GetAttribute<CascadeHierarchicalAttribute>(); if (cascadeHierarchicalAttribute == null) throw new InvalidOperationException("Was expecting a CascadeFilterAttribute."); // check we have correct column type if not throw error if (!(Column is MetaForeignKeyColumn)) throw new InvalidOperationException(String.Format("Column {0} must be a foreign key column navigation property", Column.Name)); // get current context context = Column.Table.CreateContext(); // get hierarchical cascade columns var parentColumns = new SortedList<int, String>(); for (int i = 0; i < cascadeHierarchicalAttribute.Parameters.Length; i++) parentColumns.Add(i, cascadeHierarchicalAttribute.Parameters[i]); // add extra column to represent this column itself parentColumns.Add(cascadeHierarchicalAttribute.Parameters.Length, String.Empty); //get current column into a local variable MetaForeignKeyColumn currentColumn = Column; // setup list of filter definitions for (int i = 0; i < parentColumns.Count; i++) { // get parent column name var parentColumnName = parentColumns[i]; // create dropdown list var ddl = new DropDownList() { ID = String.Format("ListControl{0}", i), Enabled = false, AutoPostBack = true }; // create filter var filter = new HierachicalListControl(ddl) { Column = currentColumn }; // check for last parent filter if (!String.IsNullOrEmpty(parentColumnName)) { // set parent column from parent table filter.ParentColumn = (MetaForeignKeyColumn)currentColumn.ParentTable.GetColumn(parentColumnName); // set current column to parent column currentColumn = filter.ParentColumn; } else { // this is the last parent and has // no parent itself so set to null filter.ParentColumn = null; currentColumn = null; } // add filter to list of filters filters.Add(i, filter); } // add dropdown list to page in correct order 2, 1, 0 // last parent, parent<N>, child for (int i = parentColumns.Count - 1; i >= 0; i--) { // setup dropdown list filters[i].ListControl.Items.Clear(); filters[i].ListControl.Items.Add(new ListItem("------", String.Empty)); // add parent list controls event handler if (i > 0) filters[i].ListControl.SelectedIndexChanged += ListControls_SelectedIndexChanged; else filters[i].ListControl.SelectedIndexChanged += ListControl0_SelectedIndexChanged; // add control to place holder this.Controls.Add(filters[i].ListControl); } if (DefaultValue != null) PopulateAllListControls(DefaultValue); else { // fill last parent filter var lastParentIndex = filters.Count - 1; var parentTable = filters[lastParentIndex].Column.ParentTable; var parentQuery = parentTable.GetQuery(context); // set next descendant list control PopulateListControl(lastParentIndex, parentQuery); } }
Listing 2 – Member variables and Page_Init
Now we add the following Listings 3 & 4 these are virtually the same as from the Edit template and so I am thinking in the next revision they may all be removed to the class library.
/// <summary> /// Sets the default values. /// </summary> /// <param name="fieldValue">The value.</param> private void PopulateAllListControls(object fieldValue) { var displayStrings = new SortedList<int, String>(); #region Get list of propert values // get property values var propertyValues = new SortedList<int, Object>(); propertyValues.Add(0, fieldValue); for (int i = 0; i < filters.Count - 1; i++) { var parentName = filters[i].ParentColumn.Name; object pv = propertyValues[i].GetPropertyValue(parentName); propertyValues.Add(i + 1, pv); } #endregion // stating at the first filter and work way up to the last filter for (int i = 0; i < filters.Count; i++) { var parentTable = filters[i].Column.ParentTable; var parentQuery = parentTable.GetQuery(context); IQueryable listItemsQuery; if (i == cascadeHierarchicalAttribute.Parameters.Length) { listItemsQuery = parentQuery.GetQueryOrdered(parentTable); } else { var pcol = filters[i + 1].Column; var selectedValue = filters[i].ParentColumn.GetForeignKeyString(propertyValues[i]); listItemsQuery = parentQuery.GetQueryFilteredFkColumn(pcol, selectedValue); } // set next descendant list control PopulateListControl(i, listItemsQuery); // set initial values var selectedValueString = filters[i].Column.Table.GetPrimaryKeyString(propertyValues[i]); ListItem item = filters[i].ListControl.Items.FindByValue(selectedValueString); if (item != null) filters[i].ListControl.SelectedValue = selectedValueString; } }
Listing 3 – PopulateAllListControls
/// <summary> /// Handles the SelectedIndexChanged event of the parentListControls control. /// </summary> /// <param name="sender">The source of the event.</param> /// <param name="e"> /// The <see cref="System.EventArgs"/> instance containing the event data. /// </param> /// <summary> /// Setups the parent list control. /// </summary> /// <param name="table">The table.</param> /// <param name="filterIndex">The parent id.</param> /// <param name="items">The items.</param> public void PopulateListControl(int filterIndex, IQueryable items) { // clear the list controls list property filters[filterIndex].ListControl.Items.Clear(); // enable list control filters[filterIndex].ListControl.Enabled = true; // add unselected value showing the column name // [Styles] filters[filterIndex].ListControl.Items.Add( new ListItem(String.Format("[{0}]", filters[filterIndex].Column.DisplayName), NullValueString)); foreach (var row in items) { // populate each item with the display string and key value filters[filterIndex].ListControl.Items.Add( new ListItem(filters[filterIndex].Column.ParentTable.GetDisplayString(row), filters[filterIndex].Column.ParentTable.GetPrimaryKeyString(row))); } } /// <summary> /// Resets all descendant list controls. /// </summary> /// <param name="startFrom">The start from.</param> private void ResetAllDescendantListControls(int startFrom) { for (int i = startFrom - 1; i >= 0; i--) { filters[i].ListControl.Items.Clear(); filters[i].ListControl.Items.Add(new ListItem("----", String.Empty)); filters[i].ListControl.Enabled = false; } }
Listing 4 – PopulateListControl and ResetAllDescendantListControls
Listing 5 is again virtually the same as the Edit templates but I kept it separate as it is an event handler and may make more sense to keep it in the template.
/// <summary> /// Handles the SelectedIndexChanged event for each List control, /// and populates the next list control in the hierarchy. /// </summary> /// <param name="sender">The source of the event.</param> /// <param name="e"> /// The <see cref="System.EventArgs"/> instance containing the event data. /// </param> void ListControls_SelectedIndexChanged(object sender, EventArgs e) { // get list control var listControl = (ListControl)sender; // get the sending list controls id as an int var id = ((Control)sender).ID; // use regular expression to find list control index var regEx = new Regex(@"\d+"); var parentIndex = int.Parse(regEx.Match(id).Value); if (listControl.SelectedValue != NullValueString) { if (parentIndex > 0) { // set child index var childIndex = parentIndex - 1; // get parent table var parentTable = filters[childIndex].Column.ParentTable; // get query from table var query = parentTable.GetQuery(context); // get items for list control var itemQuery = query.GetQueryFilteredFkColumn( filters[parentIndex].Column, listControl.SelectedValue); // populate list control PopulateListControl(childIndex, itemQuery); // reset all descendant list controls ResetAllDescendantListControls(childIndex); } } else { // reset all descendant list controls ResetAllDescendantListControls(parentIndex); } }
Listing 5 – ListControls_SelectedIndexChanged
Next we replace the DropDownList1_SelectedIndexChanged with ListControl0_SelectedIndexChanged Listing 6 really only a name change to make the code read clearly.
protected void ListControl0_SelectedIndexChanged(object sender, EventArgs e) { OnFilterChanged(); }
Listing 6 – ListControl0_SelectedIndexChanged
Finally we need to change the GetQueryable method to replace DropDownList1 with filters[0].ListControl.
Download
Happy coding
12 comments:
Hi
It is a great example, but can you or how can i do following in reverse order. means if some one select '5 Door' then it show which show models which contains '5 Door', if some one select model then it show vehicle type which contains selected model, if some one select vehicle type than show manufacture list.
Are you asking that when displaying the Models table to have a filter that filters the column on styles?
I'm not sure how that would be done and have never had a requirement for that. But I think a filter could be written, I would not bother with a generic filter in that case but just create a custom filter for that spcific requirement.
Steve :D
Hi Steve
exactly i need that at each level of selection i can filter the list.
Lara
Thanks Lara
Steve :)
Hi Steve,
" however I realised that we will need a final article that covers the more complex action of the CascadingHierarchicalFilter, this where at each level of selection we filter the list"...
When do you think is possible to you to complete this?
Thanks a lot in advance
Paolo
To get that last part working will require some clever expression tree stuff and I've not had an idea as yet, I may try and work on it over Christmas :)
Steve
Hi Steve, did you have any luck with creating the filter GetQueryable method? I'm working on something similar and saw this article:
http://www.olegsych.com/2010/08/extending-aspnet-dynamic-data-filtering-with-joins/
It works pretty well, but only goes up one level - so you can filter entities via their parent entity type, but not via their grand parent entity type.
Sorry not done any more on it yet, I will eventually get around to it though.
Steve
some new about filtering also with father and grandfather?
Hi Matteo, not sure what you are after?
Steve
Hi Steve. Thanks for the great work. Have you noticed the error that occurs in the CascadeHierarchicalFilter when a default value is provided? For example, when selecting "View Models" from the Vehicle Types.
Sorry not seen the error, but then my live cod eis much futher on than what is published here.
I am working on an open source project that will contain the latest and greatest bits. These will also be available via NuGet :)
Steve
Post a Comment