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.

3 comments:

matteo ghetti said...

It works also with LinqtoSql DynamicData?

Steve said...

Good to hear I thought it would but had'nt had the time to test.

Thanks Steve :)

edgarin said...

Thanks a lot. This helped me a lot and saved me a lot of time.

One small bug, when there are special characters, they are being html-encoded twice, so their html entities are shown to the user.