views:

7338

answers:

3

My app has a DataGridView object and a List of type MousePos. MousePos is a custom class that holds mouse X,Y coordinates (of type "Point") and a running count of this position. I have a thread (System.Timers.Timer) that raises an event once every second, checks the mouse position, adds and/or updates the count of the mouse position on this List.

I would like to have a similar running thread (again, I think System.Timers.Timer is a good choice) which would again raise an event once a second to automatically Refresh() the DataGridView so that the user can see the data on the screen update. (like TaskManager does.)

Unfortunately, calling the DataGridView.Refresh() method results in VS2005 stopping execution and noting that I've run into a cross-threading situation.

If I'm understanding correctly, I have 3 threads now:

  • Primary UI thread
  • MousePos List thread (Timer)
  • DataGridView Refresh thread (Timer)

To see if I could Refresh() the DataGridView on the primary thread, I added a button to the form which called DataGridView.Refresh(), but this (strangely) didn't do anything. I found a topic which seemed to indicate that if I set DataGridView.DataSource = null and back to my List, that it would refresh the datagrid. And indeed this worked, but only thru the button (which gets handled on the primary thread.)


So this question has turned into a two-parter:

  1. Is setting DataGridView.DataSource to null and back to my List an acceptable way to refresh the datagrid? (It seems inefficient to me...)
  2. How do I safely do this in a multi-threaded environment?


Here's the code I've written so far (C#/.Net 2.0)

public partial class Form1 : Form
{
    private static List<MousePos> mousePositionList = new List<MousePos>();
    private static System.Timers.Timer mouseCheck = new System.Timers.Timer(1000);
    private static System.Timers.Timer refreshWindow = new System.Timers.Timer(1000);

    public Form1()
    {
        InitializeComponent();
        mousePositionList.Add(new MousePos());  // ANSWER! Must have at least 1 entry before binding to DataSource
        dataGridView1.DataSource = mousePositionList;
        mouseCheck.Elapsed += new System.Timers.ElapsedEventHandler(mouseCheck_Elapsed);
        mouseCheck.Start();
        refreshWindow.Elapsed += new System.Timers.ElapsedEventHandler(refreshWindow_Elapsed);
        refreshWindow.Start();
    }

    public void mouseCheck_Elapsed(object source, EventArgs e)
    {
        Point mPnt = Control.MousePosition;
        MousePos mPos = mousePositionList.Find(ByPoint(mPnt));
        if (mPos == null) { mousePositionList.Add(new MousePos(mPnt)); }
        else { mPos.Count++; }
    }

    public void refreshWindow_Elapsed(object source, EventArgs e)
    {
        //dataGridView1.DataSource = null;               // Old way
        //dataGridView1.DataSource = mousePositionList;  // Old way
        dataGridView1.Invalidate();                      // <= ANSWER!!
    }

    private static Predicate<MousePos> ByPoint(Point pnt)
    {
        return delegate(MousePos mPos) { return (mPos.Pnt == pnt); };
    }
}

public class MousePos
{
    private Point position = new Point();
    private int count = 1;

    public Point Pnt { get { return position; } }
    public int X { get { return position.X; } set { position.X = value; } }
    public int Y { get { return position.Y; } set { position.Y = value; } }
    public int Count { get { return count; } set { count = value; } }

    public MousePos() { }
    public MousePos(Point mouse) { position = mouse; }
}
+3  A: 

You have to update the grid on the main UI thread, like all the other controls. See control.Invoke or Control.BeginInvoke.

Grzenio
+2  A: 

UPDATE! -- I partially figured out the answer to part #1 in the book "Pro .NET 2.0 Windows Forms and Customer Controls in C#"

I had originally thought that Refresh() wasn't doing anything and that I needed to call the Invalidate() method, to tell Windows to repaint my control at it's leisure. (which is usually right away, but if you need a guarantee to repaint it now, then follow up with an immediate call to the Update() method.)

    dataGridView1.Invalidate();

But, it turns out that the Refresh() method is merely an alias for:

    dataGridView1.Invalidate(true);
    dataGridView1.Update();             // <== forces immediate redraw

The only glitch I found with this was that if there was no data in the dataGridView, no amount of invalidating would refresh the control. I had to reassign the datasource. Then it worked fine after that. But only for the amount of rows (or items in my list) -- If new items were added, the dataGridView would be unaware that there were more rows to display.

So it seems that when binding a source of data (List or Table) to the Datasource, the dataGridView counts the items (rows) and then sets this internally and never checks to see if there are new rows/items or rows/items deleted. This is why re-binding the datasource repeatedly was working before.

Now to figure out how to update the number of rows to display in dataGridView without having to re-bind the datasource... fun, fun, fun! :-)


After doing some digging, I think I have my answer to part #2 of my question (aka. safe Multi-threading):

Rather than using System.Timers.Timer, I found that I should be using System.Windows.Forms.Timer instead.

The event occurs such that the method that is used in the Callback automatically happens on the primary thread. No cross-threading issues!

The declaration looks like this:

private static System.Windows.Forms.Timer refreshWindow2;
refreshWindow2 = new Timer();
refreshWindow2.Interval = 1000;
refreshWindow2.Tick += new EventHandler(refreshWindow2_Tick);
refreshWindow2.Start();

And the method is like this:

private void refreshWindow2_Tick(object sender, EventArgs e)
{
    dataGridView1.Invalidate();
}
Pretzel
A more elegant means of resetting the DataSource is to use a BindingContext and refresh that instead. It means you don't have to reset it to null each time. In terms of getting to auto update I think you need an dataasource that implements INotifyPropertyChanged.
Quibblesome
+1  A: 

Looks like you have your answer right there! Just in cawse you're curious about how to do cross thread calls back to ui: All controls have a Invoke() method (or BEginInvoke()- in case you want to do things asynchronously), this is used to call any method on the control within the context of the main UI thread. So, if you were going to call your datagridview from another thread you would need to do the following:

public void refreshWindow_Elapsed(object source, EventArgs e)
{

   // we use anonymous delgate here as it saves us declaring a named delegate in our class
   // however, as c# type inference sometimes need  a bit of 'help' we need to cast it 
   // to an instance of MethodInvoker
   dataGridView1.Invoke((MethodInvoker)delegate() { dataGridView1.Invalidate(); });
}
Fredrik Bonde