C# 如何在多线程应用程序中安全地填充数据和 Refresh() DataGridView?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/259123/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-03 20:17:19  来源:igfitidea点击:

How do I safely populate with data and Refresh() a DataGridView in a multi-threaded application?

c#.netmultithreadingdatagridviewdelegates

提问by Pretzel

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.

我的应用程序有一个 DataGridView 对象和一个 MousePos 类型的列表。MousePos 是一个自定义类,用于保存鼠标 X、Y 坐标(“点”类型)和该位置的运行计数。我有一个线程 (System.Timers.Timer) 每秒引发一个事件,检查鼠标位置,添加和/或更新此列表上鼠标位置的计数。

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.)

我想要一个类似的运行线程(再次,我认为 System.Timers.Timer 是一个不错的选择)它会再次每秒引发一个事件以自动刷新()DataGridView,以便用户可以看到数据画面更新。(就像 TaskManager 那样。)

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

不幸的是,调用 DataGridView.Refresh() 方法会导致 VS2005 停止执行并注意到我遇到了跨线程情况。

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

如果我理解正确,我现在有 3 个线程:

  • Primary UI thread
  • MousePos List thread (Timer)
  • DataGridView Refresh thread (Timer)
  • 主界面线程
  • MousePos 列表线程(定时器)
  • DataGridView 刷新线程(定时器)

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.)

为了查看我是否可以在主线程上 Refresh() DataGridView,我向名为 DataGridView.Refresh() 的表单添加了一个按钮,但这(奇怪)没有做任何事情。我发现一个主题似乎表明如果我设置 DataGridView.DataSource = null 并返回到我的列表,它将刷新数据网格。这确实有效,但只能通过按钮(在主线程上处理。)



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?
  1. 是否将 DataGridView.DataSource 设置为 null 并返回到我的 List 刷新数据网格的可接受方式?(对我来说似乎效率低下......)
  2. 如何在多线程环境中安全地执行此操作?


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

这是我到目前为止编写的代码(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; }
}

采纳答案by Pretzel

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

更新!-我部分想出答案部分#1的书“临.NET 2.0的Windows窗体和控件的客户在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.)

我最初认为Refresh()没有做任何事情,我需要调用Invalidate()方法,告诉 Windows 在空闲时重新绘制我的控件。(这通常是马上,但如果您需要保证现在重新绘制它,然后立即调用 Update() 方法进行跟进。)

    dataGridView1.Invalidate();

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

但是,事实证明Refresh()方法只是以下各项的别名:

    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.

我发现的唯一小故障是,如果 dataGridView 中没有数据,则任何无效操作都不会刷新控件。我不得不重新分配数据源。然后它在那之后工作得很好。但仅限于行(或我列表中的项目)的数量——如果添加了新项目,dataGridView 将不知道还有更多行要显示。

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.

因此,似乎在将数据源(列表或表)绑定到数据源时,dataGridView 会计算项目(行)的数量,然后在内部进行设置,并且从不检查是否有新的行/项目或行/项目被删除。这就是为什么之前反复重新绑定数据源是有效的。

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! :-)

现在要弄清楚如何更新要在 dataGridView 中显示的行数,而无需重新绑定数据源……有趣,有趣,有趣!:-)



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

在做了一些挖掘之后,我想我对问题的第 2 部分有了答案(又名安全多线程):

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

我发现我应该使用System.Windows.Forms.Timer而不是使用System.Timers.Timer

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();
}

回答by Grzenio

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

您必须像所有其他控件一样更新主 UI 线程上的网格。请参见 control.Invoke 或 Control.BeginInvoke。

回答by Fredrik Bonde

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:

看起来你的答案就在那里!只是在 cawse 你很好奇如何跨线程调用回 ui:所有控件都有一个 Invoke() 方法(或 BEginInvoke() - 如果你想异步做事),这用于调用任何方法主 UI 线程上下文中的控件。因此,如果您要从另一个线程调用 datagridview,则需要执行以下操作:

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(); });
}