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

Friday, 27 March 2009

Cascading Filters – Dynamic Data Futures (Futures on Codeplex) (UPDATED)

This is the second article is the series following on from Cascading Filters – for Dynamic Data v1.0 all we are going to do is apply the same CascadingFilterTemplate to the DefaultFilter in the futures project.

  1. Dynamic Data (v1.0) .Net 3.5 SP1
  2. Dynamic Data Futures (Futures on Codeplex)
  3. Dynamic Data Preview 3 (Preview 3 on Codeplex)

This is the same class as in the previous article just added to a Dynamic Data Futures enabled website.

using System;
using System.Web.DynamicData;
using System.Web.UI;
using Microsoft.Web.DynamicData;
public partial class Default_Filter : CascadingFilterTemplate, ISelectionChangedAware
{
 public event EventHandler SelectionChanged
 {
 add
 {
 DropDownList1.SelectedIndexChanged += value;
 }
 remove
 {
 DropDownList1.SelectedIndexChanged -= value;
 }
 }
 public override string SelectedValue
 {
 get
 {
 return DropDownList1.SelectedValue;
 }
 }
  protected override void Page_Init(object sender, EventArgs e)
 {
 // remember to call the base class
 base.Page_Init(sender, e);
 // add event handler if parent exists
 if (ParentControl != null)
 {
 // subscribe to event
 ParentControl.SelectionChanged += ListControlSelectionChanged;
 }
 if (!Page.IsPostBack)
 {
 if (ParentControl != null)
 PopulateListControl(DropDownList1, ParentControl.SelectedValue);
 else
 PopulateListControl(DropDownList1);
 // Set the initial value if there is one
 if (DropDownList1.Items.Count > 1 && !String.IsNullOrEmpty(InitialValue))
 {
 DropDownList1.SelectedValue = InitialValue;
 RaiseSelectedIndexChanged(InitialValue);
 }
 }
 var c = Column;
 }
// raise event
 protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
 {
 RaiseSelectedIndexChanged(DropDownList1.SelectedValue);
 }
 // consume event
 protected void ListControlSelectionChanged(object sender, SelectionChangedEventArgs e)
 {
 PopulateListControl(DropDownList1, e.Value);
 RaiseSelectedIndexChanged(DropDownList1.Items[0].Value);
 }
}

Listing 1 – Default.ascx.cs from Futures project filters

UPDATED: I’ve added this DropDownList1.Items.Count > 1 above to fix error mentioned

In Listing 1 I’ve marked up the changes in the Default.ascx.cs file.

<%@ Control 
 Language="C#" 
 CodeFile="Default.ascx.cs" 
 Inherits="Default_Filter" %>
<asp:DropDownList 
 ID="DropDownList1" 
 runat="server" 
 AutoPostBack="true" 
 EnableViewState="true" 
 CssClass="droplist" 
 onselectedindexchanged="DropDownList1_SelectedIndexChanged">
 <asp:ListItem Text="All" Value="" />
</asp:DropDownList>

Listing 2 – only one change here the event handler for the dropdown list

Again in Listing 2 the only change is marked in BOLD ITELIC OnSelectedIndexChanged="DropDownList1_SelectedIndexChanged" this just allows us to fire the event when the selection changes as well as when notification of a change occurs.

I’ve used the same DB as in the previous article and the same metadata so you should get the same effect.

Next we will be jumping to the ASP.Net 4.0 Preview using DomainService and adding filtering to the new Filters.

Download (UPDATED)

[フレーム]

Thursday, 26 March 2009

Cascading Filters – for Dynamic Data v1.0 (UPDATED)

This is an article is a follow on from my previous article Cascading or Dependant Field Templates for ASP.Net 4.0 Preview and is the start of a series of three articles which describe Cascading Filters for:

  1. Dynamic Data (v1.0) .Net 3.5 SP1
  2. Dynamic Data Futures (Futures on Codeplex)
  3. Dynamic Data Preview 3 (Preview 3 on Codeplex)

This first version is pretty similar to the previous article on cascading FieldTemplates here we will adapt the CascadingFieldTemplate class to facilitate our need for cascading filters.

Again there will be a new event that will be used for the cascade allowing us to fire the event other than when the dropdown list in changes (i.e. then the parent is changed and we need to pass on the cascade to the next child)

[画像: Filters that need to be cascaded]

Figure 1 – Filters that need to be cascaded

In Figure 1 we want Developer to be filtered by Builder and HouseType to in turn be filtered by Development so we see in Figure 2 the desired result, you cannot select a Developer or HouseType until it’s parent as been selected.

Cascading Filters showing cascade in default

Figure 2 – Cascading Filters showing cascade in default

I’m not going to go into a deep explanation here as the code is well commented, I will point out were the differences between the cascading file and filter are.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
/// <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 this is the same as in the Filter example

 

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 dependee control
 /// </summary>
 public String Value { get; set; }
}
/// <summary>
/// Modifies the standard FilterUserControlBase
/// to support cascading of selected values.
/// </summary>
public class CascadingFilterTemplate : System.Web.DynamicData.FilterUserControlBase
{
 #region Properties
 /// <summary>
 /// Data context
 /// </summary>
 private object DC;
 /// <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 CascadingFilterTemplate ParentControl { get; set; }
 #endregion
 protected virtual void Page_Init(object sender, EventArgs e)
 {
 DC = Column.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();
 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(',');
 // this is where I use that file from Dynamic Data Futures
 var parentFieldTemplate = GetSelectedParent(
 parentColumnPKV,
 ParentColumn.ParentTable);
 // get list of values filters by the parent's selected entity
 var itemlist = GetQueryFilteredByParent(
 childTable,
 ParentColumn,
 parentFieldTemplate);
 // clear list controls items collection before adding new items
 listControl.Items.Clear();
 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()
 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 CascadingFilterTemplate GetParentControl()
 {
 // get the parent container
 var parentDataControl = GetContainerControl();
 if (ParentColumn != null)
 {
 // Get Parent FieldTemplate
 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 2 – the CascadingFilterTemplate

The main differences here we are no longer testing for Insert or Edit mode as they don't apply to this control and in the area of acquiring the parent control we now search for a different control type both for the container control and the actual template control. Also not that in the GetParentControl method we don’t have to extract the control from a container this is because it’s the same user control each time.

/// <summary>
/// Get the DynamicControl by searching recursively for it by DataField.
/// </summary>
/// <param name="Root">The control to start the search at.</param>
/// <param name="Id">The DataField of the control to find</param>
/// <returns>The found control or NULL if not found</returns>
public static Control FindFilterControlRecursive(this Control root, string dataField)
{
 var dc = root as CascadingFilterTemplate; //Category
 if (dc != null)
 {
 if (String.Compare(dc.DataField, dataField, true) == 0)
 return dc;
 }
 foreach (Control Ctl in root.Controls)
 {
 Control FoundCtl = FindFilterControlRecursive(Ctl, dataField);
 if (FoundCtl != null)
 return FoundCtl;
 }
 return null;
}

Listing – 3 FindFilterControlRecursive

Listing 3 show the extension method which if I combined both the cascading FieldTemplates and FilterTemplates I would make this a generic extension method:

public static Control FindDynamicControlRecursive<T>(this Control root, string dataField)
where T : Control

So I could reuse the method for either.

Note: For both the CascadingFieldTemplate and the CascadingFilterTemplate to work together there would need to be some changes as to where the delegate is declared as it’s the same delegate for each.

Download (UPDATED)

And here’s the sample website with a script to create the database and an excel worksheet with table data in it.

[フレーム]

Next we will cover getting this working with the old Dynamic Data Futures.

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 

Monday, 19 January 2009

Dynamic Data – Cascading FieldTemplates

So before I dive into an explanation of this feature I’d better explain what I’m doing here. Firstly have a look at Figure 1.

Cascading ForeignKey_Edit like FieldTemplates

Figure 1 – Cascading ForeignKey_Edit like FieldTemplates

In Figure 1 you can see two DropDownLists Category and Products is this form you are only allowed to choose a product the matches the selected category. So what is required, is when Category is changes then the contents of  Product should be regenerated to be filtered by the chosen Category. I hope that make some sense.

The requirements for the above to work are:

  1. A way to get the parent DetailsView from inside a FieldTemplate.
  2. Some way of finding the FieldTemplate from the parent control tree of the dependant FileTemplate.
  3. Some way of getting the dependee FieldTemplate to fire an event on the dependant FieldTemplate when a change is made on dependee.
  4. An Attribute to tell the control which control to use as the dependee control.
Note: I am using the words dependant for the field that depends upon another for filtering and dependee for the field that is depended upon.

1. Getting the parent control

The first step would be to climb the parent tree using a generic extension method.

/// <summary>
/// Get a parent control of T from the parent control tree of the control
/// </summary>
/// <typeparam name="T">Control of type T</typeparam>
/// <param name="control">Control to search in for control type T</param>
/// <returns>The found control of type T or null if not found</returns>
public static T GetParent<T>(this Control control) where T : Control
{
 var parentControl = control.Parent;
 while (parentControl != null)
 {
 var formView = parentControl as T;
 if (formView != null)
 return formView;
 else
 parentControl = parentControl.Parent;
 }
 return null;
}

Listing 1 – Get Parent generic extension method

I decided that a generic method was required as I had not only the DetailsView but also the FormView to deal with. The logic is simple here all that happens is that the parent of the current cointrol is cast as the T type and if it is not null then we have the control we are looking for, if it is null then the loop continues and the parent of the parent is tested and so on until a match is found or a parent is null. This get’s us the hosting control of the type we want.

2. Finding the Dependee FieldTemplate

At first I thought I could use the handy extension method FindFieldTemplate but it always returned null. So I had to go my own way and came up with this:

/// <summary>
/// Get the control by searching recursively for it.
/// </summary>
/// <param name="Root">The control to start the search at.</param>
/// <param name="Id">The ID of the control to find</param>
/// <returns>The control the was found or NULL if not found</returns>
public static Control FindDynamicControlRecursive(this Control root, string dataField)
{
 var dc = root as DynamicControl;
 if (dc != null)
 {
 if (dc.DataField == dataField)
 return dc;
 }
 foreach (Control Ctl in root.Controls)
 {
 Control FoundCtl = FindDynamicControlRecursive(Ctl, dataField);
 if (FoundCtl != null)
 return FoundCtl;
 }
 return null;
}

Listing 2 – FindDynamicControlRecusive

This is based on a function I found here How to find a control when using Master Pages the change is simple all I do is cast the control as DynamicControl and then test for null if not null then I can test the DataField which I know had the column name in it.

This however only gets us the DynamicControl next we have to extract the field which is held in the DynamicControl’s Controls collection

// Get Parent FieldTemplate
var dependeeDynamicControl = detailsView.FindDynamicControlRecursive(dependeeColumn.ColumnName) as DynamicControl;
AdvancedFieldTemplate dependeeField = null;
// setup the selection event
if (dependeeDynamicControl != null)
 dependeeField = dependeeDynamicControl.Controls[0] as AdvancedFieldTemplate;

Listing 3 – Fragment: Extracting the FieldTEmplate from the DynamicControl

Now providing that the control we are after in in slot [0] of the DynamicControl’s Controls collection we are away smile_teeth.

Note: proved to be the most difficult due to differences between DetailsView and FormView which was what my original was working with.

3. Getting an event fired on the Dependant FieldTemplate when the Dependee’s DropDownList changes

Ok for this to work we need to expose the DropDownList’s OnSelectedIndexChanged event and also the SelectedValue property, here’s the code for that in Listing 4.

public override event EventHandler SelectionChanged
{
 add
 {
 DropDownList1.SelectedIndexChanged += value;
 }
 remove
 {
 DropDownList1.SelectedIndexChanged -= value;
 }
}
public override string SelectedValue
{
 get
 {
 return DropDownList1.SelectedValue;
 }
}

Listing 4 – Exposing the OnSelectedIndexChanged event and the SelectedValue property

This is fine but when I get a copy of the control from the earlier code I need to get access to this exposed event and property. So here's how we will do that we’ll create a class that inherits

/// <summary>
/// A class to add some extra features to the standard
/// FieldTemplateUserControl
/// </summary>
public class AdvancedFieldTemplate : FieldTemplateUserControl
{
 /// <summary>
 /// Handles the adding events to the drop down list
 /// </summary>
 public virtual event EventHandler SelectionChanged
 {
 add { }
 remove { }
 }
 /// <summary>
 /// Returns the selected value of the drop down list
 /// </summary>
 public virtual string SelectedValue
 {
 get
 {
 return null;
 }
 }
}

Listing 5 – AdvancedFieldTemplate class

As you can see from Listing 5 this class inherits the FieldTemplateUserControl class the a FieldTemplate inherits, so we inherit that and then on the custom FieldTemplates we want to cascade we set them to inherit the AdvancedFieldTemplate.

Next we need an event handler to handle the event passed to this the dependant control.

protected void SelectedIndexChanged(object sender, EventArgs e)
{
 var ddl = sender as DropDownList;
 if(ddl != null)
 PopulateListControl(DropDownList1, ddl.SelectedValue);
}

Listing 6 – Event handler

4. The Attribute

Attribute have been cover a lot on this site so I’m just going to post the code and comment a very little.

[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public class DependeeColumnAttribute : Attribute
{
 public static DependeeColumnAttribute Default = new DependeeColumnAttribute();
 public DependeeColumnAttribute() : this("") { }
 public DependeeColumnAttribute(String columnName)
 {
 ColumnName = columnName;
 }
 public String ColumnName { get; set; }
}

Listing 7 – DependeeColumnAttribute

I could of course hard coded the dependee column in to each custom FieldTemplate but of course that assumes that every time you use the FieldTEmplate the dependee column will have the same name, which is not always the case.

5. Putting it All Together

Firstly we need the extension method that get us the dependee control:

public static AdvancedFieldTemplate GetDependeeField<T>(this Control control, MetaColumn column) where T : Control
{
 // get value of dev ddl (Community)
 var detailsView = control.GetParent<T>();
 // get parent column attribute
 var dependeeColumn = column.GetAttribute<DependeeColumnAttribute>();
 if (dependeeColumn != null)
 {
 // Get Parent FieldTemplate
 var dependeeDynamicControl = detailsView.FindDynamicControlRecursive(dependeeColumn.ColumnName) as DynamicControl;
 AdvancedFieldTemplate dependeeField = null;
 // setup the selection event
 if (dependeeDynamicControl != null)
 dependeeField = dependeeDynamicControl.Controls[0] as AdvancedFieldTemplate;
 return dependeeField;
 }
 return null;
}

Listing 7 – GetDependeeField

In Listing 7 we return the FieldTemplate extracted from the found DynamicControl  so in the Page_Load event we get the dependee control and assign it the event handler so that we can capture the SelectedIndexChanged event of the dependee DropDpownList.

protected void Page_Load(object sender, EventArgs e)
{
 if (DropDownList1.Items.Count == 0)
 {
 if (!Column.IsRequired)
 DropDownList1.Items.Add(new ListItem("[Not Set]", ""));
 PopulateListControl(DropDownList1, "");
 }
// get dependee field
 var dependeeField = this.GetDependeeField<DetailsView>(Column);
 // add event handler if dependee exists
 if (dependeeField != null)
 dependeeField.SelectionChanged += SelectedIndexChanged;
}

Listing 8 – Page_Load event

The lines in BOLD ITALIC are the added lines and demonstrates the use of GetDependeeField extension method.

protected override void OnDataBinding(EventArgs e)
{
 base.OnDataBinding(e);
 if (Mode == DataBoundControlMode.Edit)
 {
 var dependeeField = this.GetDependeeField<DetailsView>(Column);
 if (dependeeField != null)
 PopulateListControl(DropDownList1, dependeeField.SelectedValue);
 string foreignkey = ForeignKeyColumn.GetForeignKeyString(Row);
 ListItem item = DropDownList1.Items.FindByValue(foreignkey);
 if (item != null)
 {
 DropDownList1.SelectedValue = foreignkey;
 }
 }
}

Listing 9 – OnDataBinding event handler

Again in BOLD ITALIC are line are are added.

An finally some metadata

[MetadataType(typeof(Order_DetailMD))]
public partial class Order_Detail
{
 public class Order_DetailMD
 {
 public object OrderID {get;set;}
 public object ProductID {get;set;}
 public object UnitPrice {get;set;}
 public object Quantity {get;set;}
 public object Discount {get;set;}
 public object CategoryID {get;set;}
 // EntityRefs
 [DependeeColumn("Category")]
 [UIHint("Product")]
 [ColumnOrder(2)]
 public object Product {get;set;}
 [UIHint("Category")]
 [ColumnOrder(1)]
 public object Category {get;set;}
 }
}

Listing 10 – Metadata

In the attached project I've also implemented some column ordering to arrange the columns in the order that makes sense if they cascade.

There are two part to this solution which I didn’t make clear:

  1. The properties that allow a the control’s OnSelectedIndexChanges event to be captured.
  2. Finding the dependee control and capturing it’s OnSelectedIndexChanges  event and reading it’s SelectedValue.

It must be noted that if both controls implement all the features there won’t be a problem but if you do it in only the dependant control the you won’t be able to capture the OnSelectedIndexChanges event of the dependee control or read it’s SelectedValue. So you must at least implement 1. in the dependee control and 2. in the dependant control.

I hope this clarifies things.

Setting up Northwind to work with the sample

Here’s a diagram showing the changes I made to Northwind to facilitate this example (as there were no columns that fitted this scenario).

[画像:Diagram of changes to Order_Details table]

Figure 2 – Diagram of changes to Order_Details table

[画像:Added Relationship in the Model]

Figure 3 – Added Relationship in the Model

And here’s the T-SQL to update the CategoryID column in Order_Details once you've added it.

USE [Northwind]
GO
UPDATE [Order Details] 
SET [Order Details].[CategoryID] = p.[CategoryID]
FROM [Products] p 
WHERE p.[ProductID] = [Order Details].[ProductID]
SELECT * FROM [Order Details]

Listing 11 – SQL to update Oder_Details

Note: This listing is just to make changes to Northwind so it fits this scenario.

And that's it HappyWizard

Oh and the Download

[フレーム]
Subscribe to: Comments (Atom)

AltStyle によって変換されたページ (->オリジナル) /