如何在不冻结UI的情况下连续更新数据网格(或任何其他UI控件)

本文关键字:UI 网格 数据网 任何 控件 其他 数据 更新 冻结 连续 情况下 | 更新日期: 2023-09-27 18:15:41

在WinForms应用程序中,我有一个与数据源关联的数据网格。当数据通过后台线程进入时,需要更新数据集,从而自动更新数据网格。现在,更新可以是大约每20秒7000次更新。问题是,当这样的更新发生时,UI会挂起,因为它必须发生在主线程上。这个问题有已知的解决方案吗?

一般来说,如何在WinForms中设计高性能的企业应用程序,在不冻结应用程序的情况下不断更新UI?


添加一个场景来解释这一点:

考虑一下这种情况。您有一个树视图,用于表示一些层次数据。现在,树上的数据更新是异步的。服务器可以同时发布一个或1000个更新。更新可以是对现有项目的修改或添加新节点。需要注意的是,更新不能延迟。节点表示某个地方的实时实体延迟更新会让用户感觉到事件本身被延迟了所以这是不可能的。如果可能的话(从商业逻辑的角度来看(,我早就这么做了。

这里有一个关键点:所有数据不必同时可见

所以人们不再建议这样做:

添加后台工作线程将没有帮助,因为线程必须切换到主线程才能执行更新。工作线程不会有任何区别

如何在不冻结UI的情况下连续更新数据网格(或任何其他UI控件)

除非您想使用DirectX,否则您不能

Windows窗体不是为实时信息显示而设计的。正如许多其他人所指出的,你可以非常接近,但由于Windows消息循环的工作方式,你绝对不能保证屏幕上的内容是"实时的",即使你创建了一个以60hz计时的定时器。即使您以事件驱动的方式执行此操作,Windows仍然会对WM_PAINT消息进行排队,如果您正在寻找实时显示,该消息将不可避免地延迟。

如果你真的想要一个非常接近实时的显示,你需要实现类似于游戏循环的东西。

有关Windows消息循环不适用于实时显示的原因以及什么是游戏循环的解释,请参阅:http://www.mvps.org/directx/articles/writing_the_game_loop.htm

电脑游戏不可能有任何可感知的延迟,因此大多数电脑游戏都试图优化性能,使其帧速率接近或高于60hz的圣杯。(电影只以24hz放映,你觉得它们"延迟"了吗?(

编写一个具有实时显示的应用程序并非易事,我强烈建议考虑以以下任何一种方式与Windows提供的内容妥协:

  • 创建一个计时器,以可接受的速率(每秒10次或更多次(对屏幕更新进行排队。用户不会将事件感知为延迟,因为用户无法感知在很小的一秒钟内发生的延迟
  • 当基础数据更改时引发事件,并让Windows决定何时更新显示(这几乎总是可以接受的(
  • 如果可能的话,提出一个不基于网格的替代显示。也许是一个滚动控制台,或者其他一些显示相关信息而不覆盖旧信息的界面。这可能不适用,但当你想要的接口不起作用时,想出另一个接口想法通常是一个很好的方法

如果你真的,真的仍然想要一个非常高性能的用户界面,并编写一个游戏循环,你可以在C#中这样做,并在DirectX表面上绘制自己的网格。一旦你掌握了DirectX的窍门,绘制网格就相当容易了,它只是一堆线。使用这种方法,您将避免处理Windows消息循环,并可能接近实时性能。

以下是关于如何使用DirectX以及如何在Windows窗体上渲染的精彩教程:

http://www.godpatterns.com/2005/02/using-directx-and-c-sharp-to-create.html

在您的评论中,您说您的繁重处理经常报告进度,并且您不能丢弃任何报告(因为报告是需要显示的真实数据(。

您应该做的是实现(双重(缓冲,向缓冲区报告进度,并且每隔一段时间只将缓冲区与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.
}

如果你提供更准确的信息,我可能会提供进一步的帮助。

更新

对于新的细节,我建议缓冲对对象所做的个别更改。表示对某些对象结构的更改的最通用方法是函数(可能是无参数的Action(。在接收更改时,您可以构造更新函数,直接修改视图绑定的数据,并将其存储在缓冲区中:

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
}

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

显然,我还没有尝试过这种确切的解决方案,但它使您能够控制重新绘制频率,并重新绘制整个批次,而不是单个更新。

我刚刚制作了一个示例应用程序,它将通过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);
    }
}

要允许您访问表单中的所有这些字段,下面是Designer.cs:

partial class FormMain
{
    /// <summary>
    /// Required designer variable.
    /// </summary>
    private System.ComponentModel.IContainer components = null;
    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
        {
            components.Dispose();
        }
        base.Dispose(disposing);
    }
    #region Windows Form Designer generated code
    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
        this.components = new System.ComponentModel.Container();
        this.dataGridView = new System.Windows.Forms.DataGridView();
        this.Index = new System.Windows.Forms.DataGridViewTextBoxColumn();
        this.lastNameDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
        this.firstNameDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
        this.bornDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
        this.ageDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
        this.personBindingSource = new System.Windows.Forms.BindingSource(this.components);
        this.backgroundWorker = new System.ComponentModel.BackgroundWorker();
        this.labelDataGridViewCountText = new System.Windows.Forms.Label();
        this.labelDataGridViewCount = new System.Windows.Forms.Label();
        this.labelSpinsBetweenInsertsText = new System.Windows.Forms.Label();
        this.numericUpDownTimeoutBetweenInserts = new System.Windows.Forms.NumericUpDown();
        this.checkBoxToggleWorker = new System.Windows.Forms.CheckBox();
        this.timerGuiUpdater = new System.Windows.Forms.Timer(this.components);
        ((System.ComponentModel.ISupportInitialize)(this.dataGridView)).BeginInit();
        ((System.ComponentModel.ISupportInitialize)(this.personBindingSource)).BeginInit();
        ((System.ComponentModel.ISupportInitialize)(this.numericUpDownTimeoutBetweenInserts)).BeginInit();
        this.SuspendLayout();
        // 
        // dataGridView
        // 
        this.dataGridView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 
        | System.Windows.Forms.AnchorStyles.Left) 
        | System.Windows.Forms.AnchorStyles.Right)));
        this.dataGridView.AutoGenerateColumns = false;
        this.dataGridView.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill;
        this.dataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
        this.dataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
        this.Index,
        this.lastNameDataGridViewTextBoxColumn,
        this.firstNameDataGridViewTextBoxColumn,
        this.bornDataGridViewTextBoxColumn,
        this.ageDataGridViewTextBoxColumn});
        this.dataGridView.DataSource = this.personBindingSource;
        this.dataGridView.Location = new System.Drawing.Point(12, 12);
        this.dataGridView.Name = "dataGridView";
        this.dataGridView.Size = new System.Drawing.Size(560, 212);
        this.dataGridView.TabIndex = 0;
        // 
        // Index
        // 
        this.Index.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.DisplayedCells;
        this.Index.DataPropertyName = "Index";
        this.Index.HeaderText = "Index";
        this.Index.Name = "Index";
        this.Index.Width = 58;
        // 
        // lastNameDataGridViewTextBoxColumn
        // 
        this.lastNameDataGridViewTextBoxColumn.DataPropertyName = "LastName";
        this.lastNameDataGridViewTextBoxColumn.HeaderText = "LastName";
        this.lastNameDataGridViewTextBoxColumn.Name = "lastNameDataGridViewTextBoxColumn";
        // 
        // firstNameDataGridViewTextBoxColumn
        // 
        this.firstNameDataGridViewTextBoxColumn.DataPropertyName = "FirstName";
        this.firstNameDataGridViewTextBoxColumn.HeaderText = "FirstName";
        this.firstNameDataGridViewTextBoxColumn.Name = "firstNameDataGridViewTextBoxColumn";
        // 
        // bornDataGridViewTextBoxColumn
        // 
        this.bornDataGridViewTextBoxColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.DisplayedCells;
        this.bornDataGridViewTextBoxColumn.DataPropertyName = "Born";
        this.bornDataGridViewTextBoxColumn.HeaderText = "Born";
        this.bornDataGridViewTextBoxColumn.Name = "bornDataGridViewTextBoxColumn";
        this.bornDataGridViewTextBoxColumn.Width = 54;
        // 
        // ageDataGridViewTextBoxColumn
        // 
        this.ageDataGridViewTextBoxColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.DisplayedCells;
        this.ageDataGridViewTextBoxColumn.DataPropertyName = "Age";
        this.ageDataGridViewTextBoxColumn.HeaderText = "Age";
        this.ageDataGridViewTextBoxColumn.Name = "ageDataGridViewTextBoxColumn";
        this.ageDataGridViewTextBoxColumn.ReadOnly = true;
        this.ageDataGridViewTextBoxColumn.Width = 51;
        // 
        // personBindingSource
        // 
        this.personBindingSource.DataSource = typeof(WindowsFormsApplication.Person);
        // 
        // backgroundWorker
        // 
        this.backgroundWorker.WorkerSupportsCancellation = true;
        this.backgroundWorker.DoWork += new System.ComponentModel.DoWorkEventHandler(this.OnBackgroundWorkerDoWork);
        this.backgroundWorker.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(this.OnBackgroundWorkerRunWorkerCompleted);
        // 
        // labelDataGridViewCountText
        // 
        this.labelDataGridViewCountText.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
        this.labelDataGridViewCountText.Location = new System.Drawing.Point(12, 230);
        this.labelDataGridViewCountText.Name = "labelDataGridViewCountText";
        this.labelDataGridViewCountText.Size = new System.Drawing.Size(50, 23);
        this.labelDataGridViewCountText.TabIndex = 1;
        this.labelDataGridViewCountText.Text = "Count:";
        this.labelDataGridViewCountText.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
        // 
        // labelDataGridViewCount
        // 
        this.labelDataGridViewCount.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
        this.labelDataGridViewCount.Location = new System.Drawing.Point(68, 230);
        this.labelDataGridViewCount.Name = "labelDataGridViewCount";
        this.labelDataGridViewCount.Size = new System.Drawing.Size(82, 23);
        this.labelDataGridViewCount.TabIndex = 2;
        this.labelDataGridViewCount.Text = "0";
        this.labelDataGridViewCount.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
        // 
        // labelSpinsBetweenInsertsText
        // 
        this.labelSpinsBetweenInsertsText.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
        this.labelSpinsBetweenInsertsText.Location = new System.Drawing.Point(265, 230);
        this.labelSpinsBetweenInsertsText.Name = "labelSpinsBetweenInsertsText";
        this.labelSpinsBetweenInsertsText.Size = new System.Drawing.Size(155, 23);
        this.labelSpinsBetweenInsertsText.TabIndex = 3;
        this.labelSpinsBetweenInsertsText.Text = "Spins between inserts:";
        this.labelSpinsBetweenInsertsText.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
        // 
        // numericUpDownTimeoutBetweenInserts
        // 
        this.numericUpDownTimeoutBetweenInserts.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
        this.numericUpDownTimeoutBetweenInserts.Increment = new decimal(new int[] {
        10,
        0,
        0,
        0});
        this.numericUpDownTimeoutBetweenInserts.Location = new System.Drawing.Point(426, 233);
        this.numericUpDownTimeoutBetweenInserts.Maximum = new decimal(new int[] {
        500,
        0,
        0,
        0});
        this.numericUpDownTimeoutBetweenInserts.Minimum = new decimal(new int[] {
        10,
        0,
        0,
        0});
        this.numericUpDownTimeoutBetweenInserts.Name = "numericUpDownTimeoutBetweenInserts";
        this.numericUpDownTimeoutBetweenInserts.Size = new System.Drawing.Size(65, 20);
        this.numericUpDownTimeoutBetweenInserts.TabIndex = 4;
        this.numericUpDownTimeoutBetweenInserts.ThousandsSeparator = true;
        this.numericUpDownTimeoutBetweenInserts.Value = new decimal(new int[] {
        500,
        0,
        0,
        0});
        this.numericUpDownTimeoutBetweenInserts.ValueChanged += new System.EventHandler(this.OnNumericUpDownTimeoutBetweenInsertsValueChanged);
        // 
        // checkBoxToggleWorker
        // 
        this.checkBoxToggleWorker.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
        this.checkBoxToggleWorker.Appearance = System.Windows.Forms.Appearance.Button;
        this.checkBoxToggleWorker.Location = new System.Drawing.Point(497, 230);
        this.checkBoxToggleWorker.Name = "checkBoxToggleWorker";
        this.checkBoxToggleWorker.Size = new System.Drawing.Size(75, 23);
        this.checkBoxToggleWorker.TabIndex = 6;
        this.checkBoxToggleWorker.Text = "&Start";
        this.checkBoxToggleWorker.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
        this.checkBoxToggleWorker.UseVisualStyleBackColor = true;
        this.checkBoxToggleWorker.CheckedChanged += new System.EventHandler(this.OnCheckBoxToggleWorkerCheckedChanged);
        // 
        // timerGuiUpdater
        // 
        this.timerGuiUpdater.Tick += new System.EventHandler(this.OnTimerGuiUpdaterTick);
        // 
        // FormMain
        // 
        this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.ClientSize = new System.Drawing.Size(584, 262);
        this.Controls.Add(this.checkBoxToggleWorker);
        this.Controls.Add(this.numericUpDownTimeoutBetweenInserts);
        this.Controls.Add(this.labelSpinsBetweenInsertsText);
        this.Controls.Add(this.labelDataGridViewCount);
        this.Controls.Add(this.labelDataGridViewCountText);
        this.Controls.Add(this.dataGridView);
        this.MinimumSize = new System.Drawing.Size(600, 300);
        this.Name = "FormMain";
        this.Text = "DataGridView Performance Tester";
        ((System.ComponentModel.ISupportInitialize)(this.dataGridView)).EndInit();
        ((System.ComponentModel.ISupportInitialize)(this.personBindingSource)).EndInit();
        ((System.ComponentModel.ISupportInitialize)(this.numericUpDownTimeoutBetweenInserts)).EndInit();
        this.ResumeLayout(false);
    }
    #endregion
    private System.Windows.Forms.DataGridView dataGridView;
    private System.ComponentModel.BackgroundWorker backgroundWorker;
    private System.Windows.Forms.BindingSource personBindingSource;
    private System.Windows.Forms.Label labelDataGridViewCountText;
    private System.Windows.Forms.Label labelDataGridViewCount;
    private System.Windows.Forms.Label labelSpinsBetweenInsertsText;
    private System.Windows.Forms.NumericUpDown numericUpDownTimeoutBetweenInserts;
    private System.Windows.Forms.CheckBox checkBoxToggleWorker;
    private System.Windows.Forms.Timer timerGuiUpdater;
    private System.Windows.Forms.DataGridViewTextBoxColumn Index;
    private System.Windows.Forms.DataGridViewTextBoxColumn lastNameDataGridViewTextBoxColumn;
    private System.Windows.Forms.DataGridViewTextBoxColumn firstNameDataGridViewTextBoxColumn;
    private System.Windows.Forms.DataGridViewTextBoxColumn bornDataGridViewTextBoxColumn;
    private System.Windows.Forms.DataGridViewTextBoxColumn ageDataGridViewTextBoxColumn;
}

最后但同样重要的是,我的小人物类用于有效载荷:

public class Person
{
    public int Age
    {
        get
        {
            // ToDo: better algorithm to determine real age is left as an exercise to the reader. ;-)
            var age = (int)((DateTime.Now - Born).TotalDays / 365);
            return Math.Max(0, age);
        }
    }
    public DateTime Born { get; set; }
    public string FirstName { get; set; }
    public int Index { get; set; }
    public string LastName { get; set; }
}

是处理来自服务器的数据还是实际将数据放入DataGridView的瓶颈?如果是后者,VirtualMode可以帮助您:http://msdn.microsoft.com/en-us/library/2b177d6d.aspx.

您是否使用BackgroundWorker?将使您的应用程序冻结在DoWork事件中的代码:

 private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
 {
     YourFreezingCodeHere
 }

并启动类似的后台工作人员

backgroundWorker1.RunWorkerAsync();

您可以使用BackgroundWorker来完成此操作。在DoWork方法中,您可以迭代更新数据网格。

要从非UI线程更新数据网格,您需要如下

  1. 创建一个类似的扩展方法

    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();
        }
    }
    
  2. 将数据网格更新为(假设dataGrid是您的控制id,dataSource为您的数据源(

    dataGrid.Invoke(() => { dataGrid.DataSource = dataSource; };
    

希望这对你有用。

UI将始终由主/UI线程更新。这就是WinForms的工作方式。

你能做的就是防止UI线程做太多的事情。要做到这一点:

  1. 确保在一个或多个不同的线程上执行所有其他处理
  2. 仅在对用户有用时更新UI。我看不到/读不到每3毫秒变化一次的数字,所以跳过显示更新

请注意,我使用的术语是ViewModel,View和Model是这个答案的剩余部分。我不是强迫你使用MVVM,但它使解释事情变得更容易。你可以用同样的方式使用MVP或MVC。

您可以创建一种特殊类型的ViewModel,它在x毫秒后引发一个事件,以检查"脏位"并引发适当的PropertyChanged事件。这将要求您在属性setter中设置脏位,而不是在setter中引发PropertyChanged事件。

也许更好的做法是跟踪ViewModel上次更新的时间;如果时间长于x毫秒前,请从模型中更新ViewModel,否则不要更新。这保证了UI与ViewModel同步。但是您必须意识到ViewModel与Model不同步。当然,可以创建显式同步模型的方法。

这两者之间的选择可能取决于您如何看待ViewViewModel关系,以及这一切需要花费多少时间。

Application.DoEvents();

在计时器内使用此方法。

private void timer1_Tick(object sender, EventArgs e)
{
     Application.DoEvents();
}

您应该在UI冻结的地方启动计时器,或者您可以在form_Load中启动计时器,并将计时器的间隔设置为较小的数字,使其频繁滴答作响。例如,将其设置为10。

timer1.Start();
timer1.Interval = 10;

我已经做了很多像这样的大容量数据传输(每秒数百次(,我认为DataGrid不是你想要的控件。它的设计目的是呈现数据并让用户编辑,它并没有真正优化为信息流。在这种规模下,实时查看数据对用户没有多大好处,只是数据流太大太快,无法理解。

我建议你继续使用后台工作人员来完成工作(就像你说的那样(,并使用ReportProgress方法将完成的百分比发送回进度条。您还可以使用正在处理的文件更新页面上的标签。标签将自动更新,而不会冻结您的UI。为此,请在后台工作人员调用的类中创建一个实例变量。在UI上,创建该类的实例,并在后台工作程序的ProgressChanged方法中将UI标签设置为类实例变量。每次你打电话给后台工作人员时,它都会更新。ReportProgress((

然后把所有的信息放在日志中,这样以后有人可以查看它。尝试以每秒350次的视觉变化并没有那么有益。

这个问题的一个解决方案是定期更新数据模型,即每x毫秒从通信线程批量更新一次。关于如何访问服务器数据的更多信息将有助于为您提供更明确的建议。

同时,您应该使用虚拟化控件(尤其是数据网格(。使用虚拟网格基本上意味着可以动态渲染可见细胞。因此,您只需要更新当前显示的数据。当每个单元格变得可见时,它将访问数据模型以获得当时的相关值。将此链接视为实现虚拟网格的起点。

通过结合这两种方法,您应该能够最大限度地减少网格的更新量。

re:问题是,当发生这样的更新时,UI会挂起,因为它必须在主线程上发生。这个问题有已知的解决方案吗

不,正如你看到的

一般来说,如何在WinForms中设计高性能的企业应用程序,在不冻结应用程序的情况下不断更新UI

按照你所描述的规模,你做不到。尝试限制UI控件的范围,这样它就不会试图同时显示所有地方发生的事情,而是迫使用户选择一个可以以可接受的速度更新的区域。

我建议使用两层来处理这种情况。

非UI数据层:

这个层可以从后台线程获取所有更新,并生成最终的Data对象(我们称之为ObjectX(,它是最新和最新的数据状态。这个层应该在它自己的线程上运行,这根本不会影响UI。此外,在接收到任何更新后,您可以有一个布尔变量(让我们称之为NewUpdateExist(,并将其设置为true,这表示已接收到新的更改。不要忘记使用线程安全锁定机制将此变量设置为True,以避免并发问题。

UI同步层:

这个层也可以在分离的线程上运行。您可以有一个计时器,它将在特定的时间间隔(*(触发,通过检查NewUpdateExist变量来检查自上次UI更新以来是否有任何新数据,如果有,则将NewUpdateExst设置为false,并生成只需要在屏幕上显示的新数据子集(***(。在生成数据子集和更新NewUpdateExist变量时,不要忘记使用线程安全锁定。

生成数据子集后,您需要通过控件调用另一个方法(在UI线程中(将此数据子集应用于控件。这是UI线程将阻塞的地方,直到进程完成,所以您需要使此方法尽可能轻!所有繁重的工作都需要在Invoke之外完成,并且只有与UI控件相关的部分需要在该方法中。

(*(正如"Hans Passant"在评论中提到的,人眼只能处理50毫秒的刷新,但我甚至建议将其增加到100毫秒。你可以从这个帖子中得到一些想法:可感知的最短应用程序响应延迟是多少?

(**(在这种情况下,棘手的部分是如何只使用所需的数据更新控件,而不是一次将所有数据推送到UI。我真的建议实现自定义控件来处理这一部分,而不是使用标准控件;因为您可以完全访问如何以及何时更新UI,并且可以获得最佳性能。例如,在网格上,您可以找到第一个可见的项目和可以在UI上显示的项目数,然后只更新该部分,而不是尝试用所有数据更新控件。

对不起,我知道我想在短信中解释解决方案,但这是我能遇到的最短版本。我希望这能有所帮助:-(

MSDN上有一篇关于Windows窗体异步调用模式的文章。我希望这能有所帮助。

使用backgroundWorker,它将在分离的线程中运行插入的代码,因此应用程序不会冻结。

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