C# 如何在不冻结UI的情况下连续更新datagrid(或任何其他UI控件)?
在WinForms应用程序中,我有一个与数据源关联的datagrid。当数据通过后台线程传入时,需要更新数据集,而数据集又会自动更新datagrid。现在,更新的顺序可以是每20秒7000次更新。 问题是当这样的更新发生时UI挂起,因为它必须发生在主线程上。这个问题有解决办法吗 通常,如何在WinForms中设计高性能的企业应用程序,在WinForms中不断更新UI而不冻结应用程序C# 如何在不冻结UI的情况下连续更新datagrid(或任何其他UI控件)?,c#,winforms,user-interface,C#,Winforms,User Interface,在WinForms应用程序中,我有一个与数据源关联的datagrid。当数据通过后台线程传入时,需要更新数据集,而数据集又会自动更新datagrid。现在,更新的顺序可以是每20秒7000次更新。 问题是当这样的更新发生时UI挂起,因为它必须发生在主线程上。这个问题有解决办法吗 通常,如何在WinForms中设计高性能的企业应用程序,在WinForms中不断更新UI而不冻结应用程序 添加一个场景来解释这一点: 考虑一下这个场景。您有一个树视图,用于表示一些层次结构数据。现在树上的数据更新是异步
添加一个场景来解释这一点: 考虑一下这个场景。您有一个树视图,用于表示一些层次结构数据。现在树上的数据更新是异步的。服务器可以同时发布一个或1000个更新。更新可以是修改现有项或添加新节点。需要注意的是,更新不能延迟。这些节点表示某个地方的实时实体延迟更新会让用户感觉事件本身被延迟。所以这是不可能的。如果可能的话(从业务逻辑的角度来看),我早就这么做了 这里有一个关键点:所有数据不必同时可见。 这样人们就不会再提这个了:
添加后台工作线程没有帮助,因为该线程必须切换到主线程才能执行更新。工作线程不会有任何区别。您是否使用BackgroundWorker?在DoWork事件中放入使应用程序冻结的代码:
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
YourFreezingCodeHere
}
然后像这样开始后台工作
backgroundWorker1.RunWorkerAsync();
您可以使用。在DoWork
方法中,可以迭代更新数据网格
要从非UI线程更新datagrid,您需要如下所示
public static class ControlExtensions
{
public static void Invoke(this Control control, Action action)
{
if (control.InvokeRequired) control.Invoke(new MethodInvoker(action), null);
else action.Invoke();
}
}
希望这对您有用。UI将始终由主/UI线程更新。这就是WinForms的工作方式 你能做的就是防止UI线程做太多的事情。为此:
这两者之间的选择可能取决于您对视图-视图-模型关系的看法以及这一切所花费的时间。我刚刚制作了一个示例应用程序,它将通过BackgroundWorker填充其内部列表,并且数据将显示在DataGridView中。您可以更改插入件的速度,以确定其是否满足您的要求: 最有趣的部分应该是表单本身中的代码:
public partial class FormMain : Form
{
private List<Person> _Persons;
private Random _Random;
private int _TimeoutBetweenInserts;
public FormMain()
{
InitializeComponent();
// Initialize our private fields
_Random = new Random();
_Persons = new List<Person>();
_TimeoutBetweenInserts = (int)numericUpDownTimeoutBetweenInserts.Value;
// Attach the list to the binding source and get informed on list changes.
personBindingSource.DataSource = _Persons;
personBindingSource.ListChanged += (sender, e) => labelDataGridViewCount.Text = _Persons.Count.ToString();
}
private void OnBackgroundWorkerDoWork(object sender, DoWorkEventArgs e)
{
var spinner = new SpinWait();
var worker = (BackgroundWorker)sender;
// Should we abort our adding?
while (!worker.CancellationPending)
{
// Create a new entry ...
var person = new Person();
person.Index = _Persons.Count;
person.Born = new DateTime(_Random.Next(1950, 2012), _Random.Next(1, 13), _Random.Next(1, 28));
person.FirstName = "Hello";
person.LastName = "World";
// ... and add it to the list
_Persons.Add(person);
// Do a little waiting ... (to avoid blowing out the list)
for (int i = 0; i < _TimeoutBetweenInserts; i++)
{
spinner.SpinOnce();
}
spinner.Reset();
}
}
private void OnBackgroundWorkerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
// Stop the gui updater, cause the background worker also stopped.
timerGuiUpdater.Stop();
}
private void OnCheckBoxToggleWorkerCheckedChanged(object sender, EventArgs e)
{
// Update the "button" according to the state
checkBoxToggleWorker.Text = checkBoxToggleWorker.Checked ? "&Pause" : "&Start";
if (checkBoxToggleWorker.Checked)
{
if (!backgroundWorker.IsBusy)
{
// Start the gui updater and the background worker
timerGuiUpdater.Start();
backgroundWorker.RunWorkerAsync();
}
}
else
{
// Stop the background worker
backgroundWorker.CancelAsync();
}
}
private void OnNumericUpDownTimeoutBetweenInsertsValueChanged(object sender, EventArgs e)
{
// Update the internal value, to let it propagate into the background worker
_TimeoutBetweenInserts = (int)numericUpDownTimeoutBetweenInserts.Value;
}
private void OnTimerGuiUpdaterTick(object sender, EventArgs e)
{
// Tell the BindingSource it should inform its clients (the DataGridView)
// to update itself
personBindingSource.ResetBindings(false);
}
}
在计时器内使用此方法
private void timer1_Tick(object sender, EventArgs e)
{
Application.DoEvents();
}
您应该在UI冻结的地方启动计时器,或者您可以在form_Load中启动计时器,并将计时器的间隔设置为较小的数字,以使其频繁地滴答作响。例如,将其设置为10
timer1.Start();
timer1.Interval = 10;
在您的评论中,您说您的繁重处理经常报告进度,您不能删除任何报告(因为报告是需要显示的真实数据) 您应该做的是实现(双重)缓冲,向缓冲区报告进度,并且每隔一段时间仅将缓冲区与GUI同步一次 伪代码如下:
DataGrid Grid; // This displays the data
List<object> DataBuffer; // Frequent updates are performed on this list
void BackgroundThreadLoop()
{
while(true) // This loop iterates 7000 times in 20 seconds
{
var result = DoSomeHeavyCalculations();
// Depending on the nature of the result, you can either just add it to list
// or perhaps modify existing entries in the list in some way.
DataBuffer.Add(result); // The simple case
PerformSomeUpdating(DataBuffer, result); // The complicated case
}
}
Timer RefreshTimer;
override void OnLoad()
{
RefreshTimer = new Timer();
RefreshTimer.Interval = 500; // easy to experiment with this
RefreshTimer.Tick += (s, ea) => DrawBuffer(DataBuffer);
}
void DrawBuffer(List<object> DataBuffer)
{
// This should copy DataBuffer and put it in the grid as fast as possible.
// How to do this really depends on how the list changes and what it contains.
// If it's just a list of strings:
Grid.DataSource = DataBuffer.ToList(); // Shallow copy is OK with strings
// If it's a list of some objects that have meaningful Clone method:
Grid.DataSource = DataBuffer.Select(o => o.Clone).ToList();
// If the number of elements is like constant and only some values change,
// you could use some Dictionary instead of List and just copy values.
}
现在DrawBuffer
(名称不再足够,但无所谓)方法将变得简单:
void DrawBuffer()
{
List<Action> bufferCopy;
lock(UpdateBuffer) // the other thread should also lock the buffer for adding
{
bufferCopy = UpdateBuffer.ToList();
UpdateBuffer.Clear();
}
view.SuspendLayout();
foreach(Action a in bufferCopy)
a();
view.ResumeLayout();
}
void DrawBuffer()
{
清单副本;
lock(UpdateBuffer)//另一个线程也应该锁定缓冲区以添加
{
bufferCopy=UpdateBuffer.ToList();
UpdateBuffer.Clear();
}
view.SuspendLayout();
foreach(bufferCopy中的操作a)
a();
view.ResumeLayout();
}
显然,我没有尝试过这个精确的解决方案,但它让您能够控制重画频率,并重画整个批次,而不是单个更新 我做过很多这样的大容量数据传输(每秒数百次),我认为DataGrid并不是您想要的控件。它的设计目的是显示数据并让用户编辑,但它并没有真正优化为信息流。在这种情况下,实时查看数据对用户没有多大好处,它只是一个太大、太快而无法理解的数据流 我建议您继续使用后台工作人员来完成工作(就像您说的那样),并使用ReportProgress方法将%done发送回进度条。您还可以使用正在处理的文件更新页面上的标签。标签将自动更新,不会冻结用户界面。为此,请在后台工作程序调用的类中创建一个实例变量。在UI上,创建该类的实例
private void timer1_Tick(object sender, EventArgs e)
{
Application.DoEvents();
}
timer1.Start();
timer1.Interval = 10;
DataGrid Grid; // This displays the data
List<object> DataBuffer; // Frequent updates are performed on this list
void BackgroundThreadLoop()
{
while(true) // This loop iterates 7000 times in 20 seconds
{
var result = DoSomeHeavyCalculations();
// Depending on the nature of the result, you can either just add it to list
// or perhaps modify existing entries in the list in some way.
DataBuffer.Add(result); // The simple case
PerformSomeUpdating(DataBuffer, result); // The complicated case
}
}
Timer RefreshTimer;
override void OnLoad()
{
RefreshTimer = new Timer();
RefreshTimer.Interval = 500; // easy to experiment with this
RefreshTimer.Tick += (s, ea) => DrawBuffer(DataBuffer);
}
void DrawBuffer(List<object> DataBuffer)
{
// This should copy DataBuffer and put it in the grid as fast as possible.
// How to do this really depends on how the list changes and what it contains.
// If it's just a list of strings:
Grid.DataSource = DataBuffer.ToList(); // Shallow copy is OK with strings
// If it's a list of some objects that have meaningful Clone method:
Grid.DataSource = DataBuffer.Select(o => o.Clone).ToList();
// If the number of elements is like constant and only some values change,
// you could use some Dictionary instead of List and just copy values.
}
List<Action> UpdateBuffer;
void OnUpdateReceived(MyType objToModify, object newValue)
{
// The point is to make the lambda (below) as efficient as you can;
// finding the object and preparing the update should be done here, so that
// no time is wasted during redraw in the main thread.
UpdateBuffer.Add(() => objToModify.ApplyNewValueInSomeWay(newValue));
// some other method should be constructed to add data to the view, but you get the point
}
void DrawBuffer()
{
List<Action> bufferCopy;
lock(UpdateBuffer) // the other thread should also lock the buffer for adding
{
bufferCopy = UpdateBuffer.ToList();
UpdateBuffer.Clear();
}
view.SuspendLayout();
foreach(Action a in bufferCopy)
a();
view.ResumeLayout();
}
public void backgroundWorkerPinger_DoWork(object sender, DoWorkEventArgs e)
{
Ping ping = new Ping();
try
{
PingReply pingreply = ping.Send("46.4.106.10", 500);
string active = pingreply.Status.ToString();
if (active == "Success")
{
//Pokud je spojení aktivni pak se nastavi barva labelu na zelenou a vypise se aktivni
ActiveOrNotLabel.ForeColor = Color.Green;
ActiveOrNotLabel.Text = "Aktivní";
// MessageBox.Show("vyjimka2");
if (connection_enabled == false)
{
admini.Enabled = true;
connection_enabled = true;
}
}
if (active != "Success") {
ActiveOrNotLabel.ForeColor = Color.Red;
ActiveOrNotLabel.Text = "Neaktivní";
admini.Enabled = false;
connection_enabled = false;
}
}
catch
{
//Jinak na cervenou a neaktivni
//MessageBox.Show("vyjimka");
ActiveOrNotLabel.ForeColor = Color.Red;
ActiveOrNotLabel.Text = "Neaktivní";
admini.Enabled = false;
connection_enabled = false;
}
}