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.

Sunday 30 May 2010

Neat Routing Technique for Restricting Access to Tables

In answering this question “Table specific routing for insert not working” on the Dynamic Data Forum I came across a  way to restrict which tables the catch all routes in Dynamic Data processes, allowing you to specify routes that only allows List or Edit etc.

// Customers route
routes.Add("Customers", new DynamicDataRoute("{table}/{action}.aspx")
{
    Constraints = new RouteValueDictionary(new
    {
        action = "List",
        table = "Customers"
    }),
    Model = DefaultModel,
});

// Employees route
routes.Add("Employees", new DynamicDataRoute("{table}/{action}.aspx")
{
    Constraints = new RouteValueDictionary(new
    {
        action = "List|Edit",
        table = "Employees"
    }),
    Model = DefaultModel,
});

// Categories route
routes.Add("Categories", new DynamicDataRoute("{table}/{action}.aspx")
{
    Constraints = new RouteValueDictionary(new
    {
        action = "List|Details|Insert",
        table = "Categories"
    }),
    Model = DefaultModel,
});

// catch all route
routes.Add("CatchAll", new DynamicDataRoute("{table}/{action}.aspx")
{
    Constraints = new RouteValueDictionary(new
    {
        action = "List|Details|Edit|Insert",
        table = "^((?!Customers|Employees|Categories).)*$"
    }),
    Model = DefaultModel
});

Listing 1 – Filtering tables routes

In this sample I am specifying three custom routes for:

  1. “Customer” table to allow List action.
  2. “Employees” table to allow List and Edit actions;
  3. “Categories” table to allow List, Details and Insert actions;

To show that you can restrict any individual action or combination of actins.

Note: If you do not have a List action in the actions constraint then the table will not appear on the Default.aspx as there will be no list action and it will appear to be unscaffolded, this is not the case if you try a route that you have allowed i.e. Details or Insert then the specified routes will work, it will just not appear on the Default.aspx page.

But without filtering these tables out of the the “Catch All” route (Dynamic Data default route) the missing function will be available via through the “Catch All” route.

The solution is to add a Constraint that says don’t generate a route for these tables, we do this with a regular expression:

      "^((?!Customers|Employees|Categories).)*$"

the route value dictionary actually puts the ^ and $ at the beginning and end of the expression but I have left them there for clarity of what the expression does (for those who really know what regular expressions mean Winking)

Disabled Edit and Details links

Figure 1 - Disabled Edit and Details links

In Figure 1 we see the Edit and Details links are disabled, but the Delete is still enabled this is because there is no route for Delete and so it is not controlled by routing.

!Important: In Visual Studio 2010 and .Net 4 the links still appear to be there i.e. they are not disabled but there is no URL assigned so the links do not work. so there will be a little more work to make the links disabled.

Happy coding Happy Wizzard

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.