Maintaining position in a child table

I've recently written a document covering some changes to the CurrencyManager in .NET Framework version 2.0. I'll summarize the document here, as it will not be published for a few months, and this subtle change in behavior could be causing problems in .NET Framework 1.1 applications.

Starting with the .NET Framework version 2.0, when you display data in a parent/child view you might have to take extra steps to make sure that the currently selected row in the child table is not reset to the first row of the table. In order to do this, you will have to cache the child table position and reset it after the parent table changes. Typically the child reset occurs the first time a field in a row of the parent table changes. Following are the steps to cache and reset the child position:

First cache the child position:

1. Declare an integer variable to store the child list position and a Boolean variable to store whether to cache the child position.

private int cachedPosition = -1;

private bool cacheChildPosition = true;

2. Handle the CurrencyManager.ListChanged event for the child binding's CurrencyManager and check for a ListChangedType of ListChangedType.Reset.

3. Check the current position of the CurrencyManager. If it is greater than first entry in the list (typically 0), save it to a variable.

void relatedCM_ListChanged(object sender, ListChangedEventArgs e)

{

// Check to see if this is a caching situation.

if (cacheChildPosition)

{

// If so, check to see if it is a reset situation,

// and the current position is greater than zero.

CurrencyManager relatedCM = sender as CurrencyManager;

if (e.ListChangedType == ListChangedType.Reset &&

relatedCM.Position > 0)

// If so, cache the position of the child table.

cachedPosition = relatedCM.Position;

}

}

4. Handle the parent list's CurrentChanged event for the parent currency manager. In the handler, set the Boolean value to indicate it is not a caching scenario. If the CurrentChanged occurs, the change to the parent is a list position change and not an item value change.

void bindingSource1_CurrentChanged(object sender, EventArgs e)

{

// If the CurrentChanged event occurs, this is not a caching

// situation.

cacheChildPosition = false;

}

Then reset the child position, if necessary:

1. Handle the PositionChanged event for the child binding's CurrencyManager.

2. Reset the child table position to the cached position saved in the previous procedure.

void relatedCM_PositionChanged(object sender, EventArgs e)

{

// Check to see if this is a caching situation.

if (cacheChildPosition)

{

CurrencyManager relatedCM = sender as CurrencyManager;

// If so, check to see if the current position is

// not equal to the cached position and the cached

// position is not out of bounds.

if (relatedCM.Position != cachedPosition && cachedPosition

> 0 && cachedPosition < relatedCM.Count)

{

relatedCM.Position = cachedPosition;

cachedPosition = -1;

}

}

}

Following is the code for a sample application that demonstrates the reset behavior and the cache and reset of the child position.

To run this example in Visual Studio, you'll need to paste it into a blank project, or you can compile it on the command line.

When you run the sample, click the Clear parent field button to cause a change in a field of the parent table. Notice that the selected row in the child table does not change. Then, close and run the example again. (You need to do this because the reset behavior occurs only on the first change in a parent row.) Clear the Cache and reset position check box and click the Clear parent field button. Notice that the selected row in the child table changes to the first row.

using System;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Text;

using System.Windows.Forms;

namespace BT2

{

public class Form1 : Form

{

public Form1()

{

InitializeControlsAndDataSource();

}

// Declare the controls to be used.

private BindingSource bindingSource1;

private DataGridView dataGridView1;

private Button button1;

private DataGridView dataGridView2;

private CheckBox cachePositionCheckBox;

public DataSet set1;

private void InitializeControlsAndDataSource()

{

// Initialize the controls and set location, size and

// other basic properties.

this.dataGridView1 = new DataGridView();

this.bindingSource1 = new BindingSource();

this.button1 = new Button();

this.dataGridView2 = new DataGridView();

this.cachePositionCheckBox =

new System.Windows.Forms.CheckBox();

this.dataGridView1.ColumnHeadersHeightSizeMode =

DataGridViewColumnHeadersHeightSizeMode.AutoSize;

this.dataGridView1.Dock = DockStyle.Top;

this.dataGridView1.Location = new Point(0, 20);

this.dataGridView1.Size = new Size(292, 170);

this.button1.Location =

new System.Drawing.Point(18, 175);

this.button1.Size = new System.Drawing.Size(125, 23);

button1.Text = "Clear Parent Field";

this.button1.Click +=

new System.EventHandler(this.button1_Click);

this.dataGridView2.ColumnHeadersHeightSizeMode =

DataGridViewColumnHeadersHeightSizeMode.AutoSize;

this.dataGridView2.Location =

new System.Drawing.Point(0, 225);

this.dataGridView2.Size =

new System.Drawing.Size(309, 130);

this.cachePositionCheckBox.AutoSize = true;

this.cachePositionCheckBox.Checked = true;

this.cachePositionCheckBox.Location =

new System.Drawing.Point(150, 175);

this.cachePositionCheckBox.Name = "radioButton1";

this.cachePositionCheckBox.Size =

new System.Drawing.Size(151, 17);

this.cachePositionCheckBox.Text =

"Cache and restore position";

this.ClientSize = new System.Drawing.Size(325, 420);

this.Controls.Add(this.dataGridView1);

this.Controls.Add(this.cachePositionCheckBox);

this.Controls.Add(this.dataGridView2);

this.Controls.Add(this.button1);

// Initialize the data.

set1 = InitializeDataSet();

// Set the data source to the DataSet.

bindingSource1.DataSource = set1;

//Set the DataMember to the Menu table.

bindingSource1.DataMember = "Customers";

// Add the control data bindings.

dataGridView1.DataSource = bindingSource1;

// Set the data source and member for the second

// DataGridView.

dataGridView2.DataSource = bindingSource1;

dataGridView2.DataMember = "custOrders";

// Get the currency manager for the customer orders

// binding.

CurrencyManager relatedCM =

bindingSource1.GetRelatedCurrencyManager(

"custOrders");

// Set the position in the child table for demonstration

// purposes.

relatedCM.Position = 3;

// Handle the current changed event. This event occurs

// when the current item is changed, but not when a

// field of the current item is changed.

bindingSource1.CurrentChanged +=

new EventHandler(bindingSource1_CurrentChanged);

// Handle the two events for caching and resetting the

// position.

relatedCM.ListChanged += new

ListChangedEventHandler(relatedCM_ListChanged);

relatedCM.PositionChanged

+= new EventHandler(relatedCM_PositionChanged);

}

// Establish the data set with two tables and a relationship

// between them.

private DataSet InitializeDataSet()

{

set1 = new DataSet();

// Declare the DataSet and add a table and column.

set1.Tables.Add("Customers");

set1.Tables[0].Columns.Add("CustomerID");

set1.Tables[0].Columns.Add("Customer Name");

set1.Tables[0].Columns.Add("Contact Name");

// Add some rows to the table.

set1.Tables["Customers"].Rows.Add("c1",

"Fabrikam, Inc.", "Ellen Adams");

set1.Tables[0].Rows.Add("c2", "Lucerne Publishing",

"Don Hall");

set1.Tables[0].Rows.Add("c3", "Northwind Traders",

"Lori Penor");

set1.Tables[0].Rows.Add("c4", "Tailspin Toys",

"Michael Patten");

set1.Tables[0].Rows.Add("c5", "Woodgrove Bank",

"Jyothi Pai");

// Declare the DataSet and add a table and column.

set1.Tables.Add("Orders");

set1.Tables[1].Columns.Add("CustomerID");

set1.Tables[1].Columns.Add("OrderNo");

set1.Tables[1].Columns.Add("OrderDate");

// Add some rows to the table.

set1.Tables[1].Rows.Add("c1", "119", "10/04/2006");

set1.Tables[1].Rows.Add("c1", "149", "10/10/2006");

set1.Tables[1].Rows.Add("c1", "159", "10/12/2006");

set1.Tables[1].Rows.Add("c2", "169", "10/10/2006");

set1.Tables[1].Rows.Add("c2", "179", "10/10/2006");

set1.Tables[1].Rows.Add("c2", "189", "10/12/2006");

set1.Tables[1].Rows.Add("c3", "122", "10/04/2006");

set1.Tables[1].Rows.Add("c4", "130", "10/10/2006");

set1.Tables[1].Rows.Add("c5", "1.29", "10/14/2006");

DataRelation dr = new DataRelation("custOrders",

set1.Tables["Customers"].Columns["CustomerID"],

set1.Tables["Orders"].Columns["CustomerID"]);

set1.Relations.Add(dr);

return set1;

}

private int cachedPosition = -1;

private bool cacheChildPosition = true;

void relatedCM_ListChanged(object sender,

ListChangedEventArgs e)

{

// Check to see if this is a caching situation.

if (cacheChildPosition && cachePositionCheckBox.Checked)

{

// If so, check to see if it is a reset situation,

//and the current position is greater than zero.

CurrencyManager relatedCM = sender as

CurrencyManager;

if (e.ListChangedType == ListChangedType.Reset &&

relatedCM.Position > 0)

// If so, cache the position of the child table.

cachedPosition = relatedCM.Position;

}

}

void bindingSource1_CurrentChanged(object sender,

EventArgs e)

{

// If the CurrentChanged event occurs, this is not a

// caching situation.

cacheChildPosition = false;

}

void relatedCM_PositionChanged(object sender, EventArgs e)

{

// Check to see if this is a caching situation.

if (cacheChildPosition && cachePositionCheckBox.Checked)

{

CurrencyManager relatedCM =

sender as CurrencyManager;

// If so, check to see if the current position is

// not equal to the cached position and the cached

// position is not out of bounds.

if (relatedCM.Position != cachedPosition &&

cachedPosition > 0 && cachedPosition

< relatedCM.Count)

{

relatedCM.Position = cachedPosition;

cachedPosition = -1;

}

}

}

private void button1_Click(object sender, EventArgs e)

{

// For demo purposes--modifies a value in the first row

// of the parent table.

DataRow row1 = set1.Tables[0].Rows[0];

row1[1] = DBNull.Value;

}

[STAThread]

static void Main()

{

Application.EnableVisualStyles();

Application.SetCompatibleTextRenderingDefault(false);

Application.Run(new Form1());

}

}

}