Showing posts with label Expression Trees. Show all posts
Showing posts with label Expression Trees. Show all posts

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

Monday, 23 March 2009

Cascading or Dependant Field Templates for ASP.Net 4.0 Preview

Introduction

So Here’s the Idea (which is a continuation of Dynamic Data – Cascading FieldTemplates) you have a field that is a dropdown list of one thing and another that is a subset of the previous. So to have a real world example (I’ve been working in the House Building Industry in the UK for the last 5 years so I have a ready example.

A very simple House Sales system

Figure 1 – A very simple House Sales system

In Figure 1 we have five tables, each Builder would have a number of Developments and each Development would have a series of House Types. Now a Plot would exist on a Development of a single Builder and have a House Type, in my example I have two plots tables RequiredPlot and NotRequiredPlot the difference is simple in one all the FK fields are require i.e. not nullable and in the other they are nullable. This is to show both faces of the cascading FieldTemplate. When you insert a plot in Dynamic Data then you must choose the Builder the Development and the House Type.

Insert New Plot NotRequired  (Note the [Not Set] in the NotRequired) Insert New Plot Required

Figure 2 – Insert New Plot NotRequired and Required (Note the [Not Set] in the NotRequired)

Here in Figure 2 Inserting a new Plot we have a problem at the moment I can choose a development of a different builder than the plot and the same with house type.

So my idea here is to provide a generic way of saying this field depends upon that filed (i.e. House Type depends upon Development and Development depends upon Builder).

Bug Undocumented Feature

And also having done this before I have found a nasty bug (sorry) undocumented feature my software never has bugs only undocumented features. This err Feature shows it’s self when you have a list where one say in our case Builder has only one Development and you are not using the [not set] and only showing values if you select Builder A and her has two developments and you select development 2 this will filter the contents of the House Types dropdown list, if you then change the Builder to a builder that only have one development then you will not get the corrects house types showing in the House Types dropdown list, because you cannot change the Development because there is only one.

OK I hope that last paragraph made sense because it took me a while to see why the problem exists, which is the cascade does not ripple through the cascading controls. This version of cascading Field Template will rectify this problem.

So explanation over, to the code.

Building the Extension the the FieldTemplateUserControl

The two main requirements of this implementation are:

  1. Have a ripple down the chain of controls when a selection at the top is made.
  2. A generic means of cascade.

In the previous version I simply hooked up the SelectionChanged event of the DropDownList but that only fired when the dropdown list is changed manually. So this time we will implement our own event in the UserControl which we can fire on DropDownList SelectionChanged event and when we receive a SelectionChange event from the parent control.

Before we dive into the main body of code we need to understand the event structure so here’s a basic intro to events. I’m going to use some terminology so here are my definitions how I understand it.

Term Explanation Where
Delegate Think of a Delegate as the declaration of the method pattern you must use to implement or consume this event Publisher
Publisher This is the code that has the event and wants to let other code know about the event  
Client This is the code that wants to know when the event happens in the publisher  
X This is the event  
Publish Is saying I have an event that can inform you when X happens Publisher 
Raise The publisher announces that X has happened Publisher
Subscribe Tell me when X happens Client
Consume Is actually deal with the results of the event X Client
EventArgs information passed to the event from the Publisher relating to event X own class

Table 1 – Event Terms and Descriptions

I personally find most explanations of event handling confusing so I cobbled together the above from what I’ve read, to help stop me getting confused when need to work with events.

Cascade Attribute and Associated Extension Methods

We need an attribute

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

Below are the standard extension methods I use to find attributes I use these so I don’t have to test for null as I know I will get an attribute back, for a detailed explanation of these see Writing Attributes and Extension Methods for Dynamic Data.

public static partial class HelperExtansionMethods
{
    /// <summary>
    /// Get the attribute or a default instance of the attribute
    /// if the Table attribute do not contain the attribute
    /// </summary>
    /// <typeparam name="T">Attribute type</typeparam>
    /// <param name="table">Table 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 MetaTable table) where T : Attribute, new()
    {
        return table.Attributes.OfType<T>().DefaultIfEmpty(new T()).FirstOrDefault();
    }

    /// <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 – Extension methods for finding attributes

Custom EventArgs Type

So the first thing we need is an event argument to pass the value of the selected item to the child field, since we are going to the trouble of having our own event we may as well do away with the whole looking for the control and extracting it’s value that we did before this will greatly simplify the code and make it more efficient as we will not be searching the control tree which takes time.

/// <summary>
/// Event Arguments for Category Changed Event
/// </summary>
public class SelectionChangedEventArgs : EventArgs
{
    /// <summary>
    /// Initializes a new category changed event
    /// </summary>
    /// <param name="categoryId">
    /// The categoryId of the currently selected category
    /// </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 – Selection Changes Event Arguments

CascadingFieldTemplate Class

This part of this article involves creating the cascade class to apply to the ForeignKey_Edit FieldTEmplate

Class diagram for CascadingFieldTemplate

Figutre 3 -  Class diagram for CascadingFieldTemplate

I will be using a few bits from the old Dynamic Data Futures project which you can find here Dynamic Data on Codeplex the file I will be using is the LinqExpressionHelper file as what the point of writing what already bee written. I’ll point out this code when we get to it but it’s always worth crediting the author of the code, so as usual all the credit goes to the ASP.Net team for the really clever bit of code here.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.DynamicData;
using System.Web.UI.WebControls;
using System.Linq.Expressions;
using System.Web.UI;

/// <summary>
/// Event Arguments for Category Changed Event
/// </summary>
public class SelectionChangedEventArgs : EventArgs
{
    /// <summary>
    /// Custom event arguments for SelectionChanged 
    /// event of the CascadingFieldTemplate control
    /// </summary>
    /// <param name="value">
    /// The value of the currently selected 
    /// value of the parent control
    /// </param>
    public SelectionChangedEventArgs(String value)
    {
        Value = value;
    }
    /// <summary>
    /// The values from the control of the parent control
    /// </summary>
    public String Value { get; set; }
}

/// <summary>
/// Modifies the standard FieldTemplateUserControl 
/// to support cascading of selected values.
/// </summary>
public class CascadingFieldTemplate : FieldTemplateUserControl
{
    /// <summary>
    /// Data context
    /// </summary>
    private object DC;

    /// <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)
    {
        DC = Table.CreateContext();

        // get the parent column
        var parentColumn = Column.GetAttributeOrDefault<CascadeAttribute>().ParentColumn;
        if (!String.IsNullOrEmpty(parentColumn))
        {
            ParentColumn = Column.Table.GetColumn(parentColumn) as MetaForeignKeyColumn;
        }

        // cast Column as MetaForeignKeyColumn
        ChildColumn = Column as MetaForeignKeyColumn;

        //TODO: find a way of determining which the parent control is DetailsView or FormView

        // get dependee field (note you must specify the
        // container control type in <DetailsView> or <FormView>
        ParentControl = GetParentControl();
    }

    /// <summary>
    /// Delegate for the Interface
    /// </summary>
    /// <param name="sender">
    /// A parent control also implementing the 
    /// ISelectionChangedEvent interface
    /// </param>
    /// <param name="e">
    /// An instance of the SelectionChangedEventArgs
    /// </param>
    public delegate void SelectionChangedEventHandler(
        object sender, 
        SelectionChangedEventArgs e);

    //publish event
    public event SelectionChangedEventHandler SelectionChanged;

    /// <summary>
    /// Raises the event checking first that an event if hooked up
    /// </summary>
    /// <param name="value">The value of the currently selected item</param>
    public void RaiseSelectedIndexChanged(String value) 
    {
        // make sure we have a handler attached
        if (SelectionChanged != null)
        {
            //raise event
            SelectionChanged(this, new SelectionChangedEventArgs(value));
        }
    }

    // advanced populate list control
    protected void PopulateListControl(ListControl listControl, String filterValue)
    {
        //get the parent column
        if (ParentColumn == null)
        {
            // if no parent column then just call
            // the base to populate the control
            PopulateListControl(listControl);
            // make sure control is enabled
            listControl.Enabled = true;
        }
        else if (String.IsNullOrEmpty(filterValue))
        {
            // if there is a parent column but no filter value
            // then make sure control is empty and disabled
            listControl.Items.Clear();

            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 parent FiledTeamlate
            string[] parentColumnPKV = filterValue.Split(',');
            var parentFieldTemplate = GetSelectedParent(
                parentColumnPKV, 
                ParentColumn.ParentTable);

            // get list of values filteres by the parent's selected entity
            var itemlist = GetQueryFilteredByParent(
                childTable, 
                ParentColumn, 
                parentFieldTemplate);

            // 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>
    /// Get the entity value of the selected 
    /// value of the parent column
    /// </summary>
    /// <param name="primaryKeyValues">
    /// An array of primary key values
    /// </param>
    /// <param name="parentTable">
    /// Parent columns FK table
    /// </param>
    /// <returns>
    /// Returns the currently selected entity
    /// from the parent list as an object
    /// </returns>
    private object GetSelectedParent(
        string[] primaryKeyValues, 
        MetaTable parentTable)
    {
        var query = parentTable.GetQuery(DC);

        // Items.Where(row => row.ID == 1).Single()
// this is where I use that file from Dynamic Data Futures
var singleWhereCall = LinqExpressionHelper.BuildSingleItemQuery( query, parentTable, primaryKeyValues); return query.Provider.Execute(singleWhereCall); } /// <summary> /// Returns an IQueryable of the current FK table filtered by the /// currently selected value from the parent filed template /// </summary> /// <param name="childTable"> /// This columns FK table /// </param> /// <param name="parentColumn"> /// Column to filter this column by /// </param> /// <param name="selectedParent"> /// Value to filter this column by /// </param> /// <returns> /// An IQueryable result filtered by the parent columns current value /// </returns> private IQueryable GetQueryFilteredByParent (MetaTable childTable, MetaForeignKeyColumn parentColumn, object selectedParent) { // get query {Table(Developer)} var query = ChildColumn.ParentTable.GetQuery(DC); // {Developers} var parameter = Expression.Parameter(childTable.EntityType, childTable.Name); // {Developers.Builder} var property = Expression.Property(parameter, parentColumn.Name); // {value(Builder)} var constant = Expression.Constant(selectedParent); // {(Developers.Builder = value(Builder))} var predicate = Expression.Equal(property, constant); // {Developers => (Developers.Builder = value(Builder))} var lambda = Expression.Lambda(predicate, parameter); // {Table(Developer).Where(Developers => (Developers.Builder = value(Builder)))} var whereCall = Expression.Call(typeof(Queryable), "Where", new Type[] { childTable.EntityType }, query.Expression, lambda); // generate the query and return it return query.Provider.CreateQuery(whereCall); } /// <summary> /// Gets the Parent control in a cascade of controls /// </summary> /// <param name="column"></param> /// <returns></returns> private 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 – SelectionChangedEventArgs Class

You will note this class inherits the FieldTemplateUserControl class so we get all the magic of the underlying class. We declare a delegate:

public delegate void SelectionChangedEventHandler(object sender, SelectionChangedEventArgs e);

Note the SelectionChangedEventArgs are not just empty standard EventArgs we now get the value of the parent passed in from the parent of the currently selected value.

And then the event:

public event SelectionChangedEventHandler SelectionChanged;

The we have the guts of the class PopulateListControl which takes the selected value pass in from the parent FieldTemplate where we get a list of items for this column filtered by the parent value if it exists and has a value.

Then we have GetQueryFilteredByParent where we actually get the list of items filtered by the parent FieldTemplate.

And I did say I’d point out where I was using that helper class from Dynamic Data Futures:

var singleWhereCall = LinqExpressionHelper.BuildSingleItemQuery(query, parentTable, primaryKeyValues);

Also here it’s worth talking about:

private CompositeDataBoundControl GetContainerControl()

and in particular this line of code:

var p = parentControl as CompositeDataBoundControl;

the CompositeDataBoundControl is the base type of the DetailsView and the FormView as will know if you’ve looked at the previews (I’ve only just got into it been too busy with work, but that’s stopped for a little while) to facilitate EntityTemplates Details, Edit and Insert now use the FormView control so I’ve written this modification to handle either version, the release with .Net 3.5 SP1 or the Preview. Note however that if you use ListView with inline editing as in my article:

Custom PageTemplates Part 3 - Dynamic/Templated Grid with Insert (Using ListView)

then you will need to modify the code to search for the ListView also.

Modifying the Standard ForeignKey_Edit FieldTemplate

Here we will wire up the SelectionChanged event so a change in FieldTemplate will ripple down the list

parent –> child(parent) –> child etc.

Here again is the code for the FieldTemplate as it has been modified:

using System;
using System.Collections.Specialized;
using System.ComponentModel.DataAnnotations;
using System.Web.DynamicData;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class ForeignKey_EditField : CascadingFieldTemplate //System.Web.DynamicData.FieldTemplateUserControl
{
    protected override void Page_Init(object sender, EventArgs e)
    {
        // remember to call the base class
        base.Page_Init(sender, e);

        // add event handler if dependee exists
        if (ParentControl != null)
        {
            // subscribe to event
            ParentControl.SelectionChanged += SelectedIndexChanged;
        }
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        if (DropDownList1.Items.Count == 0)
        {
            if (Mode == DataBoundControlMode.Insert || !Column.IsRequired)
                DropDownList1.Items.Add(new ListItem("[Not Set]", ""));

            PopulateListControl(DropDownList1);
        }

        SetUpValidator(RequiredFieldValidator1);
        SetUpValidator(DynamicValidator1);
    }

    #region Event
    // raise event
    protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
    {
        RaiseSelectedIndexChanged(DropDownList1.SelectedValue);
    }

    // consume event
    protected void SelectedIndexChanged(object sender, SelectionChangedEventArgs e)
    {
        PopulateListControl(DropDownList1, e.Value);

        if (Mode == DataBoundControlMode.Insert || !Column.IsRequired)
            RaiseSelectedIndexChanged("");
        else
        {
            RaiseSelectedIndexChanged(DropDownList1.Items[0].Value);
        }
    }
    #endregion

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

        if (Mode == DataBoundControlMode.Edit)
        {
            string selectedValueString = GetSelectedValueString();
            ListItem item = DropDownList1.Items.FindByValue(selectedValueString);
            if (item != null)
            {
                // if there is a default value cascade it
                RaiseSelectedIndexChanged(item.Value);
                DropDownList1.SelectedValue = selectedValueString;
             }
        }
        else if (Mode == DataBoundControlMode.Insert)
        {
            // if child field has hook up for cascade
            RaiseSelectedIndexChanged("");
        }
    }

    protected override void ExtractValues(IOrderedDictionary dictionary)
    {
        // If it's an empty string, change it to null
        string value = DropDownList1.SelectedValue;
        if (String.IsNullOrEmpty(value))
        {
            value = null;
        }

        ExtractForeignKey(dictionary, value);
    }

    public override Control DataControl
    {
        get
        {
            return DropDownList1;
        }
    }
}

Listing 4 - ForeignKey_Edit FieldTemplate

I’ve highlighted all the changes in BOLD ITALIC to emphasise  the content the main thins to look for are:

In the Page_Init here wee hook up the event if there is a parent control, and then the section that is surrounded with a region called event here we react to the SelectedIndexChanged event of the DropDownList1 and also consume the event from the parent if it exists.

<asp:DropDownList 
    ID="DropDownList1" 
    runat="server" 
    CssClass="DDDropDown" 
    AutoPostBack="True" 
    onselectedindexchanged="DropDownList1_SelectedIndexChanged">
</asp:DropDownList>

Listing 5 – changes to the DropDownList control in the page

Here again in Listing 5 I’ve highlighted the changed content, firstly we have enabled AutoPostBack and wired up the OnSelectedIndexChanged event to the event handler DropDownList1_SelectedIndexChanged in the code behind inside the event region.

Sample Metadata

[MetadataType(typeof(RequiredPlotMD))]
public partial class RequiredPlot
{
    public partial class RequiredPlotMD
    {
        public object Id { get; set; }
        public object BuilderId { get; set; }
        public object DeveloperId { get; set; }
        public object HouseTypeId { get; set; }
        public object No { get; set; }

        public object Builder { get; set; }
        [Cascade("Builder")]
        public object Developer { get; set; }
        [Cascade("Developer")]
        public object HouseType { get; set; }
    }
}

[MetadataType(typeof(NotRequiredPlotMD))]
public partial class NotRequiredPlot
{
    public partial class NotRequiredPlotMD
    {
        public object Id { get; set; }
        public object BuilderId { get; set; }
        public object DeveloperId { get; set; }
        public object HouseTypeId { get; set; }
        public object No { get; set; }

        public object Builder { get; set; }
        [Cascade("Builder")]
        public object Developer { get; set; }
        [Cascade("Developer")]
        public object HouseType { get; set; }
    }
}

Listing 6 – Sample metadata

In the attached file is a copy of the website zipped a script for creating the database and an excel spreadsheet with all the data which you can import into the DB which is fiddly but it saves me having to have several different version of the DB

I would go into more detail breaking it down line by line but I like to have the full listing with lots of comments myself, but if you think I should be more detailed let me know.

Note: This also works with Dynamic Data from .Net 3.5 SP1

Happy coding