Friday, January 11, 2008

Cell blink for DataGridView

After reading many articles on Code project, I realized that it's my time to contribute. Few months ago, I came across a requirement for cells in a DataGridView control to blink when the cell value changed. The code presented here can be applied to any other grid.

The blinking of the grid cell is achieved in the following manner. When we update the value of a cell, we also change the background color of that cell to a blink color. To restore the cell background color to its original value, we run a background thread that iterates through a list of cells that are blinking and resets them to their original non blinking state.

The code
The sample project has two functions. The first function DataInputThreadFunc() is used to generate random values to be filled / updated in the grid. The second function GridBlinkThreadFunc() is used to restore the cells to the non blink state.

Let's take a look at the first function DataInputThreadFunc():

Collapseprivate void DataInputThreadFunc()
{
Random rand = new Random();
while (true)
{
if (dataGridView1.IsDisposed)
break;

CellData data = new CellData();
data.Row = rand.Next(0, 7);
data.Col = rand.Next(0, 3);
data.Time = DateTime.Now;

int value = rand.Next(0, 101);

dataGridView1.Invoke((MethodInvoker)delegate()
{
dataGridView1.Rows[data.Row].Cells[data.Col].Value = value;
dataGridView1.Rows[data.Row].Cells[data.Col].Style
.BackColor = Color.Salmon;
});

lock (_blinkData)
{
_blinkData.Add(data);
}

Thread.Sleep(1000);
}
}


The function uses a while (true) loop as it's a background thread and will be shutdown automatically when the application is closed. if (dataGridView1.IsDisposed) check is done to make sure we do not call dataGridView1.Invoke() on a disposed object. This can happen when the user closes the application.

Next, we initialize an object of the class CellData to store the blink data.

class CellData
{
public int Row;
public int Col;
public DateTime Time;
}

This class is used to store the row number, column number and the time when the value changed.

Next we use dataGridView1.Invoke() to make a call to the user interface thread and set the grid properties. We save the blink data in a generic list to be used later by the blink thread function. Since the list is altered by more than one thread, we synchronize access by locking the list on each access.

Now let's take a look at the blink thread function.

Collapseprivate void GridBlinkThreadFunc()
{
while (true)
{
// Make a copy to avoid invalid operation exception
// while iterating through the map
List tempBlinkData;
lock (_blinkData)
{
tempBlinkData = new List(_blinkData);
}

foreach (CellData data in tempBlinkData)
{
TimeSpan elapsed = DateTime.Now - data.Time;
if (elapsed.TotalMilliseconds > 500) // 500 is the Blink delay
{
if (dataGridView1.IsDisposed)
return;

dataGridView1.BeginInvoke((MethodInvoker)delegate()
{
dataGridView1.Rows[data.Row].Cells[data.Col]
.Style.BackColor = dataGridView1.Columns[data.Col]
.DefaultCellStyle.BackColor;
});

lock (_blinkData)
{
_blinkData.Remove(data);
}
}
}

Thread.Sleep(250); // Blink frequency
}
}

At the very beginning, we make a copy of the _blinkData list. This helps us to modify the list while we iterate through the contents of the temporary copy. For each cell we find in the list, we check to make sure whether the blink time has elapsed or not. In this case the blink time is 500 milliseconds. Any cell that has elapsed the blink time gets its background color reset to the default cell style background color and is removed from the list.

Again we make sure that we set the grid property only in the user interface thread. In addition, we lock the _blinkData list before altering it. Thread.Sleep(250) is the frequency with which we go through the list to turn off the cells. Ideally, it should be half the value of blink delay.

Points of Interest
You will notice this code can be applied to any grid. This code can also be hidden in a class that extends a DataGridView control.

One thing I love about .NET 2.0 is this dataGridView1.Invoke((MethodInvoker)delegate(). This statement lets you get away from writing a function and declaring a delegate. That frees up tons of browsing time!

A good point was made by "Kristof Verbiest" about the use of BeginInvoke() instead of Invoke(). The GridBlinkThreadFunc() uses BeginInvoke() to avoid unnecessary context switch.

No comments: