Showing posts with label IQueryable. Show all posts
Showing posts with label IQueryable. Show all posts

Sunday, 6 June 2010

Part 3 – A Cascading Hierarchical Field Template & Filter for Dynamic Data

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.

Cascading Hierarchical Filter

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

Note: With a few changes around the last two listings you could adapt this to either the old Dynamic Data Futures VS2008 SP1 RTM filters on CodePlex or Josh Heyes’s Dynamic Data Filtering.

Monday, 31 May 2010

Part 2 – A Cascading Hierarchical Field Template & Filter for Dynamic Data

In this second part we are going to complete the field templates by adding the CascadeHierarchical_Edit field template (see Figure 1) and in the next adding a CascadeHierarchical filter.

Starting with a copy of the ForeignKey_Edit field template as our base we will end up with a field template that looks like Figure 1 allowing you to have a hierarchy ‘n’ levels deep.

Cascade Hierarchical Field Template

Figure 1 – Cascade Hierarchical Field Template in action.

Note: It turns out the the Product field on the Order is only available in insert not edit so I have moved away from Northwind and have created a very construed Vehicles sample DB, I have included as a script in the download at the end of this article.

The first we must make the copy of the ForeignKey_Edit field template, and then make sure we rename it to CascadeHierarchical_Edit, also we must change the class name all it’s files; this sample being a Web Application project there are three files:

  • CascadeHierarchical_Edit.ascx
  • CascadeHierarchical_Edit.ascx.cs
  • CascadeHierarchical_Edit.ascx.designer.cs

Each must be modified as follows (I generally follow the naming convention use in the other standard field templates i.e. Text_Edit’s class name is Text_EditField), so we will set our field templates class name to CascadeHierarchical_EditField.

Note: if you change CascadeHierarchical_Edit.ascx first then you will not need to change the CascadeHierarchical_Edit.ascx.designer.cs will happen automatically. (I don’t know why the CascadeHierarchical_Edit.ascx.cs is not also changed automatically when the ascx file’s Inherits property is changed).

In the .cs file change the class name to CascadeHierarchical_EditField and in the CascadeHierarchical_Edit.ascx change the Inherits property to  be namespace.CascadeHierarchical_EditField, namespace is usually the name of the project, in any case if will already be filled in in the template unless you are copying from one project to another.

Now we have a basis to begin, lets first start with the Page_Init and some explanation.

<%@ Control 
    Language="C#" 
    CodeBehind="CascadeHierarchical_Edit.ascx.cs" 
    Inherits="CascadeHierarchicalFieldTemplate.CascadeHierarchical_EditField" %>

<asp:RequiredFieldValidator 
    runat="server" 
    ID="RequiredFieldValidator1" 
    CssClass="DDControl DDValidator" 
    Display="Static" 
    Enabled="false"/>
<asp:DynamicValidator 
    runat="server" 
    ID="DynamicValidator1" 
    CssClass="DDControl DDValidator" 
    Display="Static"/>

Listing 1 – the aspx page.

Here in Listing 1 you can see all we have are the required and dynamic validators, this is all we need at the dropdown lists will all be created dynamically.

#region member variables
// 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

protected void Page_Init(object sender, EventArgs e)
{
    // 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 = 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, "");

    //get current column into a local variable
    MetaForeignKeyColumn column = ForeignKeyColumn;

    // 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 = column };

        // check for last parent filter
        if (!String.IsNullOrEmpty(parentColumnName))
        {
            // set parent column from parent table
            filter.ParentColumn = (MetaForeignKeyColumn)column.ParentTable.GetColumn(parentColumnName);

            // set current column to parent column
            column = filter.ParentColumn;
        }
        else
        {
            // this is the last parent and has
            // no parent itself so set to null
            filter.ParentColumn = null;
            column = 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("------", ""));

        // add parent list controls event handler
        if (i > 0)
            filters[i].ListControl.SelectedIndexChanged += ListControls_SelectedIndexChanged;

        // add control to place holder
        this.Controls.Add(filters[i].ListControl);
    }

    if (Mode == DataBoundControlMode.Insert)
    {
        // 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 -  Page_Init

The reason for using Page_Init (see Listing 2) is that we are going to create the dropdown lists dynamically, these need to be instantiated in the OnInit event to be fully involved in post back.

The first thing we need to do in the Page_Init is to make sure we have the correct Column Type (MetaForeignKeyColumn) and that the column has a CascadeHierarchicalAttribute assigned, if either of these in not present then we throw an error.

There are three for loops in the Page_Init, the first for loop we are building a list of parent columns to help with the next for loop which builds the filters list (by having two loops we make it easy to put the list controls onto the page in the most logical order, last parent to the left and child to the right).

Note: We also set each dropdown list’s SelectedIndexChanged event to post back to the same ListControls_SelectedIndexChanged handler.

This list of  filters is used throughout the rest of the field template to build each list control and made up of a dropdown list the current column and it’s parent column (see Listing 3).

/// <summary>
/// Class to contains information about cascading dropdown lists
/// </summary>
protected internal class HierachicalListControl
{
    /// <summary>
    /// Returns a <see cref="System.String"/> that represents this instance.
    /// </summary>
    /// <returns>
    /// A <see cref="System.String"/> that represents this instance.
    /// </returns>
    public override string ToString()
    {
        var parentColumn = ParentColumn != null ? ParentColumn.Name : "null";
        return String.Format("{0}.{1}", Column.Name, parentColumn);
        //return this.Column.Name;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="HierachicalListControl"/> class.
    /// </summary>
    /// <param name="column">This column.</param>
    /// <param name="parentColumn">This column's parent column.</param>
    public HierachicalListControl(ListControl listControl)
    {
        ListControl = listControl;
    }

    /// <summary>
    /// Gets or sets the filter column.
    /// </summary>
    /// <value>The column.</value>
    public MetaForeignKeyColumn Column { get; set; }

    /// <summary>
    /// Gets or sets the filter column's parent column.
    /// </summary>
    /// <value>The parent column.</value>
    public MetaForeignKeyColumn ParentColumn { get; set; }

    /// <summary>
    /// Gets or sets the list control.
    /// </summary>
    /// <value>The list control.</value>
    public ListControl ListControl { get; set; }
}

Listing 3 – HierachicalListControl used in list of filters .

Note: I have overridden the ToString method to make the filters list more readable in debug mode, in Column.ParentColumn format, this is not required for the sample to function correctly.

The final for loop is use to initialise each dropdown list with it’s default item, hook-up the SelectionIndexChanged event and then add it to the page.

The final segment of code in Page_Init is the if statement for when we are in insert mode as the OnDataBound event will not fire, so we populate the last filter the (highest in the hierarchy).

protected void Page_Load(object sender, EventArgs e)
{
    if (filters[0] != null &&
        filters[0].ListControl != null)
    {
        RequiredFieldValidator1.ControlToValidate = "ListControl0";
        DynamicValidator1.ControlToValidate = "ListControl0";
        SetUpValidator(RequiredFieldValidator1);
        SetUpValidator(DynamicValidator1);
    }
}

Listing 4 – Page_Load event handler

The Page_Load event just makes sure that we have some filter[0] and then sets up the validators. The next event handler is the OnDataBinding handler here we make sure we are in Edit mode and that the current value is not null and then we call the main method for setting up the filters SetupListControls.

protected override void OnDataBinding(EventArgs e)
{
    base.OnDataBinding(e);

    // Set initial value
    if (Mode == DataBoundControlMode.Edit && FieldValue != null)
        PopulateAllListControls(FieldValue);
}

/// <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 5 – OnDataBinding and PopulateAllListControls

The first thing SetupListControls does is get a list of actual values for each filter in the cascade of filters using fieldValue as a starting point, working back from the passed in fieldValue, it calls the method GetPropertyValue which uses reflection to get the value of the next entity using the current value and the parent column’s name. These values are in turn used to fill each filter with appropriately filtered values. The last parent getting an unfiltered list of items as it has no parent itself.

/// <summary>
/// Gets the property value.
/// </summary>
/// <param name="sourceObject">The source object.</param>
/// <param name="propertyName">Name of the property.</param>
/// <returns>The named properties value.</returns>
public static Object GetPropertyValue(this Object sourceObject, 
    string propertyName)
{
    if (sourceObject != null)
        return sourceObject.GetType()
            .GetProperty(propertyName)
            .GetValue(sourceObject, null);
    else
        return null;
}

Listing 6 – GetPropertyValue

The two extension methods used to get the list of items are called GetQueryOrdered Listing 7 or GetQueryFilteredFkColumn Listing 8.

/// <summary>
/// Gets the query ordered.
/// </summary>
/// <param name="sourceQuery">The source query.</param>
/// <param name="table">The table.</param>
/// <returns></returns>
public static IQueryable GetQueryOrdered(this IQueryable sourceQuery, MetaTable table)
{
    // get display column attribute
    var displayColumnAttribute = table.GetAttribute<DisplayColumnAttribute>();

    // check to see if sort is assigned
    if (displayColumnAttribute == null || displayColumnAttribute.SortColumn == null)
        return sourceQuery;

    // {row.OrderBy(row => row.Name)}
    var orderByCall = GetOrderByCallExpression(
        sourceQuery, 
        table, 
        displayColumnAttribute.SortColumn, false);

    // create and return query
    return sourceQuery.Provider.CreateQuery(orderByCall);
}

Listing 7 – GetQueryOrdered

/// <summary>
/// Gets a list of entities from the source IQueryable 
/// filtered by the MetaForeignKeyColumn's selected value
/// </summary>
/// <param name="sourceQuery">The query to filter</param>
/// <param name="fkColumn">The column to filter the query on</param>
/// <param name="fkSelectedValue">The value to filter the query by</param>
/// <returns>
/// An IQueryable of the based on the source query 
/// filtered but the FK column and value passed in.
/// </returns>
public static IQueryable GetQueryFilteredFkColumn(this IQueryable sourceQuery, 
    MetaForeignKeyColumn fkColumn, String fkSelectedValue)
{
    // if no filter value return the query
    if (String.IsNullOrEmpty(fkSelectedValue))
        return sourceQuery;

    // order query
    sourceQuery = GetQueryOrdered(sourceQuery, fkColumn.Table);

    // {RequiredPlots}
    var parameterExpression = Expression.Parameter(sourceQuery.ElementType, fkColumn.Table.Name);

    // {(RequiredPlots.Builders.Id = 1)}
    var body = BuildWhereClause(fkColumn, parameterExpression, fkSelectedValue);

    // {RequiredPlots => (RequiredPlots.Builders.Id = 1)}
    var whereLambda = Expression.Lambda(body, parameterExpression);

    // {Developers.Where(RequiredPlots => (RequiredPlots.Builders.Id = 1))}
    var whereCall = Expression.Call(typeof(Queryable),
        "Where",
        new Type[] { sourceQuery.ElementType },
        sourceQuery.Expression,
        Expression.Quote(whereLambda));

    // create and return query
    return sourceQuery.Provider.CreateQuery(whereCall);
}

Listing 8 – GetQueryFilteredFkColumn

Listings 7 & 8 are form the IQueriableExtensionMethods.cs class file in the NotAClue.Web.DynamicData class library, and are a part of my dynamic Linq Expression extension methods that originally came from the ASP.NET July 2007 Futures Source Code sample on ASP.Net on CodePlex.com, I continue to expand and modify them as I need to; so I wont go into detail here as there better posts on Linq Expression than any I could write.

Note: There is a useful sample in the C# samples see Dynamic LINQ (Part 1: Using the LINQ Dynamic Query Library on Scott Guthrie's blog, “Dynamic Linq” allows you to easily build Linq queries on the fly.
/// <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), ""));

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

Listing 9 – PopulateListControl

Once we have the query filtered and ordered we then set the list control’s items up by calling PopulateListControl (Listing 9), then finally we set the initial value in of the list control.

/// <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 (!String.IsNullOrEmpty(listControl.SelectedValue))
    {
        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 10 – parentListControls_SelectedIndexChanged

The next thing we have to deal with is SelectedIndexChanged event on each dynamically created filter, we do this in the ListControls_SelectedIndexChanged handler (Listing 10) we first get the posting list control and then extract the filter index from it’s name. Remember all the filters are named ListControl{N} where N is the index into the filters list for the filter.

Note: We use a regular expression “\d+” to match one or more digits in the list control’s ID

Then we check to see if the list control has a SelectedValue if not we reset it and all it’s children to the default of "----" and Enabled to false. If it has a SelectedValue then we get a query on the child column filtered by the current SelectedValue and populate the next list control in line.

We then call SetupParentListControl to populate the control and finally ResetAllDescendantListControls to set any descendant controls to their default value of "----" and disabled.

The last two methods as two of the standard field template methods just slightly change to avoid exceptions seen here in Listings 11.

protected override void ExtractValues(IOrderedDictionary dictionary)
{
    // If it's an empty string, change it to null
    string value;
    if (filters[0] != null && filters[0].ListControl != null)
        value = filters[0].ListControl.SelectedValue;
    else
        value = String.Empty;

    if (String.IsNullOrEmpty(value))
        value = null;

    ExtractForeignKey(dictionary, value);
}

public override Control DataControl
{
    get
    {
        if (filters[0] != null && filters[0].ListControl != null)
            return filters[0].ListControl;
        else
            return null;
    }
}

Listing 11 – ExtractValues method and DataControl property.

These two methods merely check to see if there are nay controls before processing.

Download

The sample is a Visual Studio 2010 and .Net 4.0 sample but the code and field template should work with Visual Studio 2008 SP1 and .Net 3.5 SP1 DD Web Application.

Note: The script for creating the Vehicles database is in the zip file.