Showing posts with label Cascading FieldTemplates. Show all posts
Showing posts with label Cascading FieldTemplates. Show all posts

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.

Tuesday, 25 May 2010

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

In the past I have done several articles on Cascading field templates and filters, these articles will cover a single cascading hierarchical field template that filters by it's parent fields without having to have those tables related directly related to the columns table.

ScreenShot258

Figure 1 – Old Cascading Relationships

So in the diagram above to have cascade from Builder->Developer->HouseType on the plot you must have an foreign key relationship with each of the Parent tables on the Plots tables. In the above Requires Plots table we have an FK relationship with Builder, Developer and HouseType to get the cascade to work.

There was however a Cascade filter in the old Dynamic Data Futures VS2008 SP1 RTM project on Codeplex. This did offered the hope of something better, which in this article we will build. First the field template then with a few changes a Filter.

So the aim of this sample if to have a single foreign key field on the table and have the selection cascade over each parent field.

The Attribute

We will need a simple attribute for this to pass in the list of parent columns

Cascade Relationship EF

Figure 2 – New Cascading Relationships

So we will need to pass the field template a list of parent foreign key navigation properties like this:

Cascade Relationship EF attributes

Figure 3 – Foreign Key columns

In Figure 3 the normal foreign key column Product already in known to use but we will need to supply the name of the next foreign key navigation property which will be  Category.

[UIHint("CascadeHierarchical")]
[CascadeHierarchical("Category")]
public Product Product { get; set; }

Listing 1 – Attribute applied

Note in Listing 1 we only need to supply the next navigation property and if there was another level up we could supply that also and so on.

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class CascadeHierarchicalAttribute : Attribute
{
    public String[] Parameters { get; private set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="CascadeHierarchicalAttribute"/> class.
    /// </summary>
    /// <param name="parameters">
    /// The parameters are the parent columns in
    /// order of  hierarchy and must be Foreign Key
    /// navigation columns for the hierarchy for car manufacturers
    /// e.g. Manufacturer, VehicleType, Model and Style on the 
    /// Styles table the parameters would be:
    /// [CascadeHierarchical("Manufacturer", "VehicleType", "Model")]
    /// as Style would already be known via foreign key navigation.
    /// </param>
    public CascadeHierarchicalAttribute(params String[] parameters)
    {
        Parameters = parameters;
    }
}

Listing 2 – CascadeHierarchicalAttribute

The only parameter takes the param attribute and takes an array of type String.

Read Only Field Template

So first thing is to create the read only version of the field template, so what will be required, if we look at the standard foreign key field template the only methods we will need to replace is the GetDisplayString() method. In here we will return a string of the form:

Manufacturer > Vehicle Type > Model > Style

this would gives us with cars something like:

“Ford > Car > Mondeo > 5 Door Hatch”

// value to use to separate values in the Hierarchy
private const string DISPLAY_SEPERATOR = " > ";

protected String GetDisplayString()
{
    // make sure this is a navigation property (ForeignKey column navigation property)
    if (!(Column is MetaForeignKeyColumn))
        throw new InvalidOperationException(String.Format("Column {0} must be a foreign key column navigation property", Column.Name));

    // get cascade hierarchical attribute
    var cascadeHierarchicalAttribute = Column.GetAttribute<CascadeHierarchicalAttribute>();
    if (cascadeHierarchicalAttribute == null)
        throw new InvalidOperationException("Was expecting a CascadeFilterAttribute.");

    // check for null value
    if (FieldValue != null)
    {
        //get parent table
        var parentTable = ForeignKeyColumn.ParentTable;

        // add this foreign key navigation property to the soreted list
        var fieldValues = new SortedList<int, String>();
        fieldValues.Add(0, ForeignKeyColumn.ParentTable.GetDisplayString(FieldValue));

        // get current value as an object
        Object currentValue = FieldValue;

        // loop through each navigation property to build values
        for (int i = 0; i < cascadeHierarchicalAttribute.Parameters.Length; i++)
        {
            // set name of parent column
            var parentColumnName = cascadeHierarchicalAttribute.Parameters[i];

            // get parent column from name
            var parentColumn = (MetaForeignKeyColumn)parentTable.GetColumn(parentColumnName);

            // check for valid column
            if (parentColumn == null && !(parentColumn is MetaForeignKeyColumn))
                throw new InvalidOperationException(String.Format("Invalid parent column {0}", parentColumnName));

            // get next value
            currentValue = currentValue.GetPropertyValue(parentColumnName);

            // extract current value as string using the parent table's display string
            var currentValueString = FormatFieldValue(parentColumn.ParentTable.GetDisplayString(currentValue));

            // add current value as string to the sorted field values list
            fieldValues.Add(i + 1, currentValueString);

            // set next parent table
            parentTable = parentColumn.ParentTable;
        }

        // build display string
        var displayString = new StringBuilder();
        for (int i = cascadeHierarchicalAttribute.Parameters.Length; i >= 0; i--)
            displayString.Append(fieldValues[i] + DISPLAY_SEPERATOR);

        // return the built string
        return FormatFieldValue(displayString.ToString().Substring(0, displayString.ToString().Length - 2));
    }
    else
        // same as standard foreign key field
        return FormatFieldValue(ForeignKeyColumn.ParentTable.GetDisplayString(FieldValue));
}

Listing 2 – new GetDisplayString method

now in Details and List pages we will get output like this:

25-05-2010 15-06-46

Figure 4 – new output from read only CascadeHierarchical field template.

The only clever piece of code in here is the GetPropValue() method which gets the actual value from the parent column using some basic reflection.

/// <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 3 -  GetPropertyValue

So in the next part we will create the CascadeHierarchical_Edit field template that has multiple dropdown lists populated one for each parent with the furthest grandparent filtering the next and so on to the child.

Note: I will add a project download in the next article.

Friday, 8 May 2009

Communicating Between FieldTemplates in Dynamic Data (UPDATED)

A question that is asked a lot on the Dynamic Data Forum is how can I get a reference to a FieldTemplate, the reason people ask this is because they are used to doing things this was from classic ASP.Net code; the problem with this is that it leads to specialised code in the page, which means custom page and I always go for custom FieldTemplate over custom page.

The problem with most of the custom FieldTemplates I’ve written for production code is that they are not generic which can be ok, but I tend to find myself writing the same sort of things again and again. So with question on the Dynamic Data Forum and on this blog I thought I’d tackle one of these types of problem in a more generic reusable way. This solution come from the previous cascading articles I’ve culminating with Cascading Filters and Fields – Dynamic Data Entity Framework Version which allows fields and filters to cascade. Here I’m going to use the same event model so one control can alert other controls to a change in it’s state thus facilitating say a checkbox hiding or disabling other field on the form depending upon its state. In this article I’m going to look at Checkbox as parent controls enabling other controls to change there status.

What we will need to Build This.

  • Event Interface
  • Event Delegate
  • EventArgs
  • Implementations
    • Parent
    • Child

The Code

Here I will quickly layout the code (each listing is fully commented) we are going to use it is not majorly different form the Cascading FieldTemplate mentioned  here

/// <summary>
/// The interface for parent controls to implement.
/// </summary>
public interface IChangeNotifyingFieldTemplate
{
    /// <summary>
    /// Gets the parent column.
    /// </summary>
    /// <value>The parent column.</value>
    MetaColumn ParentColumn { get;}

    /// <summary>
    /// Gets the state.
    /// </summary>
    /// <value>The state.</value>
    String State { get; }

    /// <summary>
    /// Occurs when [state changed].
    /// </summary>
    event ChangingAwareEventHandler StateChanged;
}

Listing 1 – the IChangingAware event interface

In Listing 1 we have our interface which has an event and three properties we will need top implement in our FieldTemplates. Now we will need a way of sending the current status of the parent control to the child control for this will will use an EventArgs class.

/// <summary>
/// Event Arguments for Changing Aware Event
/// </summary>
public class ChangingAwareEventArgs : EventArgs
{
    /// <summary>
    /// Custom event arguments for SelectionChanged 
    /// event of the ParentChangingAwareFieldTemplate control
    /// </summary>
    /// <param name="value">
    /// The value of the currently selected 
    /// value of the parent control
    /// </param>
    public ChangingAwareEventArgs(String state)
    {
        State = state;
    }
    /// <summary>
    /// The values from the control of the parent control
    /// </summary>
    public String State { get; set; }
}

Listing 2 – Changing Aware EventArgs

As you can see in Listing 2 Changing Aware EventArgs has only one property which is a string for simplicity. We will use Value to pass the current value of the parent control to the child.

/// <summary>
/// Delegate for the changing aware Interface
/// </summary>
/// <param name="sender">Parent Control</param>
/// <param name="e">An instance of the ChangingAwareEventArgs</param>
public delegate void ChangingAwareEventHandler(
    object sender,
    ChangingAwareEventArgs e);

Listing 3 – The delegate for our parent and child controls

In Listing 3 you can see the delegate for our controls event.

public class ParentChangeNotifyingFieldTemplate 
: FieldTemplateUserControl, IChangeNotifyingFieldTemplate { /// <summary> /// Gets or sets the value. /// </summary> /// <value>The state.</value> public virtual String State { get; private set; } /// <summary> /// Gets or sets the parent column. /// </summary> /// <value>The parent column.</value> public MetaColumn ParentColumn { get; private set; } /// <summary> /// publish event. /// </summary> public event ChangingAwareEventHandler StateChanged; /// <summary> /// Raises the <see cref="E:System.Web.UI.Control.Init"/> event. /// </summary> /// <param name="e"> /// An <see cref="T:System.EventArgs"/> /// object that contains the event data. /// </param> protected override void OnInit(EventArgs e) { ParentColumn = Column; base.OnInit(e); } /// <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 RaiseStatusChanged(String value) { // make sure we have a handler attached if (StateChanged != null) { //raise event StateChanged(this, new ChangingAwareEventArgs(value)); } } }

Listing 4 – Parent Change Notifying FieldTemplate

In Listing 4 the control that parent FieldTemplate will inherit so that they can generate events for the child control to subscribe to.

public class ChildChangingAwareFieldTemplate : FieldTemplateUserControl
{
    /// <summary>
    /// Gets or sets the parent column.
    /// </summary>
    /// <value>The parent column.</value>
    public MetaColumn ParentColumn { get; private set; }

    /// <summary>
    /// Gets or sets the parent control.
    /// </summary>
    /// <value>The parent control.</value>
    public IChangeNotifyingFieldTemplate ParentControl { get; set; }

    /// <summary>
    /// Raises the <see cref="E:System.Web.UI.Control.Init"/> event.
    /// </summary>
    /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
    protected override void OnInit(EventArgs e)
    {
        // get the parent column
        var parentColumn = Column.GetAttributeOrDefault<ChangingAwareAttribute>().ParentColumn;
        if (!String.IsNullOrEmpty(parentColumn))
            ParentColumn = Column.Table.GetColumn(parentColumn) as MetaColumn;

        // get parent field (note you must specify the container control type in
        // DetailsView and FormView = CompositeDataBoundControl : DataBoundControl
        // ListView = DataBoundControl
        if (ParentColumn != null)
            ParentControl = GetParentControl();

        // finally call base
        base.OnInit(e);
    }

    /// <summary>
    /// Gets the Parent control in a cascade of controls
    /// </summary>
    /// <param name="column"></param>
    /// <returns></returns>
    private IChangeNotifyingFieldTemplate GetParentControl()
    {
        if (ParentColumn != null)
        {
            // get value of dev ddl (Community)
            var parentDataControl = this.GetContainerControl<DataBoundControl>();

            // Get Parent FieldTemplate
            var parentDynamicControl = parentDataControl
                .FindDynamicControlRecursive(ParentColumn.Name)
                as DynamicControl;

            // extract the parent control from the DynamicControl
            IChangeNotifyingFieldTemplate parentControl = null;
            if (parentDynamicControl != null)
                parentControl = parentDynamicControl.Controls[0] as IChangeNotifyingFieldTemplate;

            return parentControl;
        }
        return null;
    }
}

Listing 5 – Child Changing Aware FieldTemplate

And Listing 5 is the control that child FieldTemplates will inherit, so it can subscribe to events from the parent control. It contains the logic to find the parent control and a couple of properties to hold the parent column and controls in.

/// <summary>
/// Get the attribute or a default instance of the attribute
/// if the Column attribute do not contain the attribute
/// </summary>
/// <typeparam name="T">
/// Attribute type
/// </typeparam>
/// <param name="table">
/// Column to search for the attribute on.
/// </param>
/// <returns>
/// The found attribute or a default 
/// instance of the attribute of type T
/// </returns>
public static T GetAttributeOrDefault<T>(this MetaColumn column) where T : Attribute, new()
{
    return column.Attributes.OfType<T>().DefaultIfEmpty(new T()).FirstOrDefault();
}

Listing 6 – Get attribute extension method

The extension method in Listing 6 is there to simplify the code for getting an attribute which we do a lot in Dynamic Data.

/// <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 FindDynamicControlRecursive<T>(this Control root, string dataField) where T : Control
public static Control FindDynamicControlRecursive(this Control root, string dataField)
{
    var dc = root as DynamicControl; //Category
    if (dc != null)
    {
        if (String.Compare(dc.DataField, dataField, true) == 0)
            return dc;
    }

    foreach (Control Ctl in root.Controls)
    {
        Control FoundCtl = FindDynamicControlRecursive(Ctl, dataField);

        if (FoundCtl != null)
            return FoundCtl;
    }
    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>
public static T GetContainerControl<T>(this Control control) where T : Control
{
    var parentControl = control.Parent;
    while (parentControl != null)
    {
        var p = parentControl as T;
        if (p != null)
            return p;
        else
            parentControl = parentControl.Parent;
    }
    return null;
}

Listing 7 – A group of extension methods to get the parent control

Listing 7 is the two extension methods used by the child control to find the parent, by first using GetContainerControl to find the DetailsView, FormView or GridView etc.

!Important:

ALL previous Cascading examples have a minor flaw/bug/feature. The issue occurs when the parent control appears in the list of controls after the child control, which means in the controls OnInit event all following controls are not in the list. There are two options here

  1. Force the order of columns shown in the data control
  2. let each child control capture the OnDataBound event of the container DataControl and then find the parent control there, which may be too late to hookup the event
In this article we are going to use the first method and so I will introduce a field generator and an attribute to set the column order.
/// <summary>
/// Allows to specify the ordering of columns. Columns are will
///
be sorted in increasing order based on the Order value. Columns without /// this attribute have a default Order of 0. Negative values are /// allowed and can be used to place a column before all other columns. /// unashamedly nicked from the DD Futures project :D /// </summary> [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field,
Inherited = true,
AllowMultiple = false)] public class ColumnOrderAttribute : Attribute, IComparable { public static ColumnOrderAttribute Default = new ColumnOrderAttribute(0); public ColumnOrderAttribute(int order) { Order = order; } /// <summary> /// The ordering of a column. Can be negative. /// </summary> public int Order { get; private set; } public int ListOrder { get; set; } #region IComparable Members
public int CompareTo(object obj) { return Order - ((ColumnOrderAttribute)obj).Order; } #endregion } public static partial class HelperExtansionMethods { public static ColumnOrderAttribute GetColumnOrdering(this MetaColumn column) { return column.Attributes.OfType<ColumnOrderAttribute>()
.DefaultIfEmpty(ColumnOrderAttribute.Default).First(); } }

Listing 8 – Column Order attribute

/// <summary>
/// Implements the IAutoFieldGenerator interface and 
/// supports advanced scenarios such as declarative 
/// column ordering, workaround for attribute 
/// localization issues.
/// Again mostly swiped from DD Futures
/// </summary>
public class AdvancedFieldGenerator : IAutoFieldGenerator
{

    private MetaTable _table;
    private bool _multiItemMode;

    /// <summary>
    /// Allows to explicitly declare which columns should be skipped
    /// </summary>
    public List<MetaColumn> SkipList
    {
        get;
        set;
    }

    /// <summary>
    /// Creates a new AdvancedFieldGenerator.
    /// </summary>
    /// <param name="table">The table this class generates fields for.</param>
    /// <param name="multiItemMode"><value>true</value> to indicate a multi-item control such as GridView, <value>false</value> for a single-item control such as DetailsView.</param>
    public AdvancedFieldGenerator(MetaTable table, bool multiItemMode)
    {
        if (table == null)
        {
            throw new ArgumentNullException("table");
        }

        _table = table;
        _multiItemMode = multiItemMode;
        SkipList = new List<MetaColumn>();
    }

    private bool IncludeField(MetaColumn column)
    {
        // Skip columns that should not be scaffolded
        if (!column.GetScaffold())
            return false;

        // Don't display long strings in controls that show multiple items
        if (column.IsLongString && _multiItemMode)
            return false;

        // Skip columns that are on the skip list
        if (SkipList.Contains(column))
            return false;

        return true;
    }

    private ColumnOrderAttribute ColumnOrdering(MetaColumn column)
    {
        return column.Attributes.OfType<ColumnOrderAttribute>().DefaultIfEmpty(ColumnOrderAttribute.Default).First();
    }

    #region IAutoFieldGenerator Members

    public ICollection GenerateFields(Control control)
    {
        // Get all of table's columns, take only the ones that should be automatically included in a fields collection,
        // sort the result by the ColumnOrderAttribute, and for each column create a DynamicField
        var fields = from column in _table.Columns
                     where IncludeField(column)
                     orderby ColumnOrdering(column)
                     select new DynamicField()
                     {
                         DataField = column.Name,
                         HeaderText = column.DisplayName
                     };

        return fields.ToList();
    }

    #endregion
}

public static partial class HelperExtansionMethods
{
    /// <summary>
    /// Gets a value indicating if the column should be scaffolded. This honors the
    /// ScaffoldColumnAttribute as well as returns true if the column is an enumerated type.
    /// </summary>
    /// <param name="column"></param>
    /// <returns></returns>
    public static bool GetScaffold(this MetaColumn column)
    {
        // make sure we honor the ScaffoldColumnAttribute. The framework already does this
        // but we want to do this again as the first thing.
        var scaffoldAttribute = column.GetAttribute<ScaffoldColumnAttribute>();
        if (scaffoldAttribute != null)
            return scaffoldAttribute.Scaffold;

        // always return true for enumerated types
        return column.ColumnType.IsEnum || column.Scaffold;
    }
}

Listing 9 – The IAutoFieldGenerator

I have included Listing 8 & 9 for completeness they can both be found in the ASP.NET July 2007 Futures Source Code project on Codeplex and all I’m going to do is add [ColumnOrder(-1)] to the Discontinued column of the Products table (-1 is before zero and the default value is zero).

So now we are ready to setup some FieldTemplates to act as parents and children. Here we will create on parent control by modifying the default Boolean FieldTemplate Boolean_Edit.ascx.

Implementing the above classes in the FieldTemplates

Here we have a class for parent FieldTemplates to inherit and one for children, the parent exposes two properties and an event and the child class does the dirty business of finding the parent control.

Here we are going to use the Boolean_Edit.ascx for as out parent, you could use any theoretically but I thought Boolean made for a good sample.

#region Changing Aware code
/// <summary>
/// override the Value property and 
/// return the controls curretn state
/// </summary>
public override string State
{
    get
    {
        return CheckBox1.Checked.ToString();
    }
}

public MetaColumn ChildColumn { get { return Column; } }

protected void CheckBox1_CheckedChanged(object sender, EventArgs e)
{
    RaiseStatusChanged(this.CheckBox1.Checked.ToString());
}
#endregion

Listing 10 – code to add to the parent control (Boolean_Edit.ascx)

You just need to add the code from Listing 10 to the Boolean_Edit.ascx.cs file and then change the classes inheritance to ParentChangeNotifyingFieldTemplate now Boolean_Edit FieldTemplate is publishing its ChangingAware event.

#region Changing Aware Control
// added page init to hookup the event handler
protected override void OnDataBinding(EventArgs e)
{
    // get the parent column
    var parentColumn = Column.GetAttributeOrDefault<ChangingAwareAttribute>().ParentColumn;

    if (!String.IsNullOrEmpty(parentColumn))
    {
        //TODO: get the value from Row of the ParentColumn
        Object value = DataBinder.GetPropertyValue(Row, parentColumn);
        if (String.Compare(value.ToString(), "true", true) == 0)
            this.Visible = false;
        else
            this.Visible = true;
    }

    base.OnDataBinding(e);
}
#endregion

Listing 11 – this is the code for the Text.ascx.cs file

All you need to do is add the above code Listing 11 the to ReadOnly FieldTemplates that you want to hide in response to the parent in our case its just the Text.ascx.cs file.

#region Event
// added page init to hook-up the event handler
protected void Page_Init(object sender, EventArgs e)
{
    if (ParentColumn != null && ParentControl != null)
    {
        // regiter for the event
        ParentControl.StateChanged += StateChanged;
    }
}

// consume event
protected void StateChanged(object sender, ChangingAwareEventArgs e)
{
    if (ParentColumn != null && ParentControl != null)
    {
        // show or hide depending on current state of parent
        if (String.Compare(e.State, "true", true) == 0)
            this.Visible = false;
        else
            this.Visible = true;
    }
}

// added data binding to allow field to be hidden on load
protected override void OnDataBinding(EventArgs e)
{
    if (ParentControl != null)
        this.Visible = ParentControl.State == "True" ? false : true;
    base.OnDataBinding(e);
}

Listing 12 – this is the code for the Text_Edit.ascx and Integer_Edit.ascx files (UPDATED)

Listing 12 code is added to both the Text_Edit.ascx.cs and Integer_Edit.ascx.cs files

Updated: I’ve update the code in the OnDataBinding event handler to fix a bug during insert where there would be no value in the parent filed.
[MetadataType(typeof(ProductMD))]
public partial class Product
{
    public class ProductMD
    {
        public object ProductID {get;set;}
        public object ProductName {get;set;}
        public object SupplierID {get;set;}
        public object CategoryID {get;set;}

        [ChangingAware("Discontinued")]
        public object QuantityPerUnit {get;set;}

        public object UnitPrice {get;set;}
        public object UnitsInStock {get;set;}

        [ChangingAware("Discontinued")]
        public object UnitsOnOrder {get;set;}

        [ChangingAware("Discontinued")]
        public object ReorderLevel {get;set;}

        [ColumnOrder(-1)]
        public object Discontinued {get;set;}
        // EntitySet
        public object Order_Details {get;set;}
        // EntityRef
        public object Category {get;set;}
        public object Supplier {get;set;}

    }
}

Listing 13 – the Metadata

As you can see in Listing 13 of the metadata I’ve added the ChangeAware attribute to several columns these will be hidden id the row is discontinued. And to make sure that the children can see the parent when looking for it I’ve added a ColumnOrder attribute to the Discontinued column with a value of –1 for force it to be the first field in the row see Figure 1, 2 and 3 .

Figure 1 – As you can see the bottom row is discontinued and some field are hidden appropriately

Figure 1 – As you can see the bottom row is discontinued and some field are hidden appropriately.

 

Figure 2 - normal Figure 3 - Discontinued
Figure 2 - normal Figure 3 - Discontinued

 

So there we have it

Download (UPDATED)

Happy coding HappyWizard

Saturday, 4 April 2009

Cascading Filters and Fields – Dynamic Data Entity Framework Version (UPDATED)

Well I wanted to do this in EF and Preview 3 at the same time but I having an issue with that so I’m combining both the Cascading Fields and Filters together and when the bugs are ironed out of the Preview I’ll do it there also.

Firstly the issue with the Preview.

  1. No filters support in the DefaultEFProject
  2. Errors when saving using the DefaultDomainServiceProject

Well I’m going to build a new Dynamic Data Entities Web Application for this project and create a separate project to keep the CascadeExtensions in.

I’ll add the projects zipped to the end of the article.

Lets create the class file first. (I’m assuming you know how to operate VS :D )

Create a new Class Library project Class Library project and delete the Class.cs file and give the project a namespace like DyanmicData.CascadeExtensions

ScreenShot285

Figure 1 – Adding Assembly name and Default namespace

using System;

namespace
DynamicData.CascadeExtensions { /// <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

You will need one of my extension methods to extract the attribute later on:

/// <summary>
/// Get the attribute or a default instance of the attribute
/// if the Column attribute do not contain the attribute
/// </summary>
/// <typeparam name="T">Attribute type</typeparam>
/// <param name="table">Column to search for the attribute on.</param>
/// <returns>The found attribute or a default instance of the attribute of type T</returns>
public static T GetAttributeOrDefault<T>(this MetaColumn column) where T : Attribute, new()
{
    return column.Attributes.OfType<T>().DefaultIfEmpty(new T()).FirstOrDefault();
}

Listing 2 – GetAttributeOrDefault extension method.

I have some more extension methods to add later that will be used by both the CascadeFieldTemplate and CascadeFilterTemplates.

using System;

namespace DynamicData.CascadeExtensions
{
    /// <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 parent control
        /// </summary>
        public String Value { get; set; }
    }
}

Listing 3 - SelectionChangedEventArgs

Next we create two new classes called CascadeFieldTemplate and CascadeFilterTemplate.

using System;
using System.Web.DynamicData;
using System.Web.UI.WebControls;

namespace DynamicData.CascadeExtensions
{
    /// <summary>
    /// Modifies the standard FieldTEmplateUserControl 
    /// to support cascading of selected values.
    /// </summary>
    public class CascadingFieldTemplate : FieldTemplateUserControl
    {
        /// <summary>
        /// Controls selected value
        /// </summary>
        public String SelectedValue { get; private set; }

        /// <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 CascadingFieldTemplate ParentControl { get; set; }

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


            // get parent 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();

                if (Mode == DataBoundControlMode.Insert || !Column.IsRequired)
                    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 query {Table(Developer).OrderBy(d => d.Name)}
                var query = ChildColumn.ParentTable.GetQuery(Column.Table.CreateContext());

                // get list of values filtered by the parent's selected entity
                var itemlist = query.GetQueryFilteredByParent(ParentColumn, filterValue);

                // clear list controls items collection before adding new items
                listControl.Items.Clear();

                // only add [Not Set] if in insert mode or column is not required
                if (Mode == DataBoundControlMode.Insert || !Column.IsRequired)
                    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>
        /// Gets the Parent control in a cascade of controls
        /// </summary>
        /// <param name="column"></param>
        /// <returns></returns>
        private CascadingFieldTemplate GetParentControl()
        {
            // get value of dev ddl (Community)
            var parentDataControl = GetContainerControl();

            if (ParentColumn != null)
            {
                // Get Parent FieldTemplate
                var parentDynamicControl = parentDataControl
                    .FindDynamicControlRecursive(ParentColumn.Name)
                    as DynamicControl;

                // extract the parent control from the DynamicControl
                CascadingFieldTemplate parentControl = null;
                if (parentDynamicControl != null)
                    parentControl = parentDynamicControl.Controls[0] 
as
CascadingFieldTemplate; return parentControl; } 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 CompositeDataBoundControl the base class for FormView and DetailsView /// </returns> private CompositeDataBoundControl GetContainerControl() { var parentControl = this.Parent; while (parentControl != null) { // NOTE: this will not work if used in // inline editing in a list view as // ListView is a DataBoundControl. var p = parentControl as CompositeDataBoundControl; if (p != null) return p; else parentControl = parentControl.Parent; } return null; } } }

Listing 4 – CascadeFieldTemplate

using System;
using System.Web.DynamicData;
using System.Web.UI.WebControls;

namespace DynamicData.CascadeExtensions
{
    /// <summary>
    /// Modifies the standard FieldTEmplateUserControl 
    /// to support cascading of selected values.
    /// </summary>
    public class CascadingFilterTemplate : FilterUserControlBase
    {
        #region Properties
        /// <summary>
        /// This controls list control 
        /// </summary>
        public ListControl ListControl { get; private set; }

        /// <summary>
        /// Paretn 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

        //public override IQueryable GetQueryable(IQueryable source)
        //{
        //    return source;
        //}

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

            // get dependee field (note you must specify the
            // container control type in <DetailsView> or <VormView>
            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("[All]", ""));

                // make sure control is disabled
                listControl.Enabled = false;
            }
            else
            {
                // get the child columns parent table
                var childTable = ChildColumn.ParentTable;

                // get query {Table(Developer).OrderBy(d => d.Name)}
                var query = ChildColumn.ParentTable.GetQuery(Column.Table.CreateContext());

                // filter the query by the parent
                var itemlist = query.GetQueryFilteredByParent(ParentColumn, filterValue);

                // clear list controls items collection before adding new items
                listControl.Items.Clear();
                listControl.Items.Add(new ListItem("[All]", ""));

                // 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>
        /// Gets the Parent control in a cascade of controls
        /// </summary>
        /// <returns>An the parent control or null</returns>
        private CascadingFilterTemplate GetParentControl()
        {
            if (ParentColumn != null)
            {
                // get the parent container
                var parentDataControl = GetContainerControl();

                // get the parent container
                if (parentDataControl != null)
                    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 5 – CadcadingFilterTemplate

You may note that I have removed GetQueryFilteredByParent and some other local methods from both CascadeFieldTemplate and CadcadingFilterTemplate, they will be placed in the extension methods class file later.

Now we come the the differences between the Linq to SQL implementation and this the Entity Framework implementation.

The issue I had when trying to make this work with EF was that

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

Listing 6 – Old GetQueryFilteredByParent method.

In here I passed in the selectedParent which contained the entity I was filtering on anyway what I found was the EF did not like that at all it said basically I want a simple value like int, String, double etc.

This is the expression I was faced with:

Table(Developer).Where(Developers => (Developers.Builder = value(Builder)))

here I was leaving the join up to L2S but EF wanted me to be more specific

Table(Developer).Where(Developers => (Developers.BuilderId = 2))

But I knew that wouldn't work because EF does not have the FK fields in it’s entities, well not if it can help it :) so I surmised that it would want something like this:

Table(Developer).Where(Developers => (Developers.Builder.Id = 2))

Where Id is the PK of the Builder entity.

And then I was lucky enough to be trying this with Preview 3 and the DefaultDomainServiceProject which if you have a look at the ForeignKey filter you will see some nice Expression creating code:

private Expression BuildQueryBody(
    ParameterExpression parameterExpression, 
    string selectedValue)
{
    IDictionary dict = new Hashtable();
    Column.ExtractForeignKey(dict, selectedValue);

    int i = 0;
    ArrayList andFragments = new ArrayList();
    foreach (DictionaryEntry entry in dict)
    {
        string fieldName = Column.ParentTable.Name + "." 
            + Column.ParentTable.PrimaryKeyColumns[i++].Name;

        Expression propertyExpression = 
            CreatePropertyExpression(parameterExpression, fieldName);

        object value = ChangeType(entry.Value, propertyExpression.Type);
        Expression equalsExpression = Expression.Equal(
            propertyExpression, 
            Expression.Constant(value, propertyExpression.Type));

        andFragments.Add(equalsExpression);
    }

    Expression result = null;
    foreach (Expression e in andFragments)
    {
        if (result == null)
        {
            result = e;
        }
        else
        {
            result = Expression.AndAlso(result, e);
        }
    }
    return result;
}

Listing 7 – BuildQuery from the ForeignKey filter.

This helps build something like this (item.Categories.CategoryID = 1) which eventually produces this where expression and it would deal with composite keys.

Where(item => (item.Categories.CategoryID = 2))

So I swiped that from the ForeignKey filter and added it to my Extension methods, and built the missing bits from what I could glean with Reflector.

So we have the following in the extension methods class

#region IQueryable methods
/// <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 GetQueryFilteredByParent(this IQueryable sourceQuery, MetaForeignKeyColumn fkColumn, String fkSelectedValue)
{
    // if no filter value return the query
    if (String.IsNullOrEmpty(fkSelectedValue))
        return sourceQuery;

    // {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 lambda = Expression.Lambda(body, parameterExpression);

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

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

/// <summary>
/// This builds the and where clause taking
/// into account composite keys
/// </summary>
/// <param name="fkColumn">The column to filter the query on</param>
/// <param name="fkSelectedValue">The value to filter the query by</param>
/// <param name="parameterExpression">Parameter expression</param>
/// <returns>
/// Returns the expression for the where clause 
/// i.e. ((x = 1) && (Y = 2)) or (x = 1) etc.
/// </returns>
private static Expression BuildWhereClause(
    MetaForeignKeyColumn fkColumn, 
    ParameterExpression parameterExpression, 
    string fkSelectedValue)
{
    // get the FK's and value into dictionary
    IDictionary dict = new OrderedDictionary();
    fkColumn.ExtractForeignKey(dict, fkSelectedValue);

    // setup index into dictionary
    int i = 0;

    // setup array list to hold each AND fragment
    ArrayList andFragments = new ArrayList();
    foreach (DictionaryEntry entry in dict)
    {
        // get fk name 'Builders.Id'
        string keyName = fkColumn.Name
            + "." + fkColumn.ParentTable.PrimaryKeyColumns[i++].Name;

        // Build property expression 
        // i.e. {RequiredPlots.Builders.Id}
        Expression propertyExpression 
            = BuildPropertyExpression(parameterExpression, keyName);

        // sets the type based on the propertyExpression's type
        // i.e. all the values returned from the DDL are of type string
        // so the type on the expression needs setting to the correct type
        object value = ChangeType(entry.Value, propertyExpression.Type);

        // join the property expression and value in an
        // equals expression i.e. (RequiredPlots.Builders.Id = 1)
        Expression equalsExpression 
            = Expression.Equal(propertyExpression, 
            Expression.Constant(value, propertyExpression.Type));

        // add a fragment to array list
        andFragments.Add(equalsExpression);
    }

    // initialise result
    Expression result = null;
    // join add fragments of composite keys 
    // together together
    foreach (Expression e in andFragments)
    {
        if (result == null)
            result = e;
        else
            result = Expression.AndAlso(result, e);
    }
    // joined fragments look something like:
    // (RequiredPlots.Developer.Id = 1) && (RequiredPlots.HouseType.Id = 1)
    return result;
}

/// <summary>
/// Builds a property expression from the parts it joins
/// the parameterExpression and the propertyName together.
/// i.e. {RequiredPlots}  and "Builders.Id"
/// becomes: {RequiredPlots.Developers.Id}
/// </summary>
/// <param name="parameterExpression">
/// The parameter expression.
/// </param>
/// <param name="propertyName">
/// Name of the property.
/// </param>
/// <returns>
/// A property expression
/// </returns>
public static Expression BuildPropertyExpression(
    Expression parameterExpression, 
    string propertyName)
{
    Expression expression = null;
    // split the propertyName into each part to 
    // be build into a property expression
    string[] strArray = propertyName.Split(new char[] { '.' });
    foreach (string str in strArray)
    {
        if (expression == null)
            expression 
                = Expression.PropertyOrField(parameterExpression, str);
        else
            expression 
                = Expression.PropertyOrField(expression, str);
    }
    // {RequiredPlots.Developer.Id}
    return expression;
}

/// <summary>
/// Changes the type.
/// </summary>
/// <param name="value">The value to convert.</param>
/// <param name="type">The type to convert to.</param>
/// <returns>The value converted to the type.</returns>
public static object ChangeType(object value, Type type)
{
    // if type is null throw exception can't
    // carry on nothing to convert to.
    if (type == null)
        throw new ArgumentNullException("type");

    if (value == null)
    {
        // test for nullable type 
        // (i.e. if Nullable.GetUnderlyingType(type)
        // is not null then it is a nullable type 
        // OR if it is a reference type
        if ((Nullable.GetUnderlyingType(type) != null) 
            || !type.IsValueType)
            return null;
        else // for 'not nullable value types' return the default value.
            return Convert.ChangeType(value, type);
    }

    // ==== Here we are guaranteed to have a type and value ====

    // get the type either the underlying type or 
    // the type if there is no underlying type.
    type = Nullable.GetUnderlyingType(type) ?? type;

    // Convert using the type
    TypeConverter converter 
        = TypeDescriptor.GetConverter(type);
    if (converter.CanConvertFrom(value.GetType()))
    {
        // return the converted value
        return converter.ConvertFrom(value);
    }

    // Convert using the values type
    TypeConverter converter2 
        = TypeDescriptor.GetConverter(value.GetType());
    if (!converter2.CanConvertTo(type))
    {
        // if the type cannot be converted throw an error
        throw new InvalidOperationException(
            String.Format("Unable to convert type '{0}' to '{1}'", 
            new object[] { value.GetType(), type }));
    }
    // return the converted value
    return converter2.ConvertTo(value, type);
}
#endregion

Listing 8 – IQueryable extension methods

Updated: 

In the BuildWhereClause method in Listing 8 I have made a minor change that resolves a major bug:

    string keyName = fkColumn.ParentTable.Name + "." + fkColumn.ParentTable.PrimaryKeyColumns[i++].Name;

has been changed to:

         string keyName = fkColumn.Name + "." + fkColumn.ParentTable.PrimaryKeyColumns[i++].Name;

The issue here was that you would get {RequiredPlots.Developers.Id} instead of {RequiredPlots.Developer.Id} (note the plural Developers) this was fine in Entity Framework where the entity was left as it cane out of the DB but no good for Linq to SQL which uses pluralisation.

So there you have it, there’s a lot more we could do with this to streamline the code make some of the extension methods more generic etc but I think I will leave it there.

Download (UPDATED)

The download is a Web Application Project for EF bit the CascadeExtensions classes, FieldTemplate and FilterUserControl are compatible with EF and L2S.

Note: Also included are the script to create the DB and some data to import in excel format.
Updated: I’ve now added filter ordering via an extension to the FilterRepeater called SortedFilterRepeater and am mapping it in web.config, also added a general sort vi IAutoFieldGenerator on all pages