Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you update a datagrid (or any other UI control) continuously without freezing the UI?

In a WinForms application, I have a datagrid that is associated to a datasource. As and when data comes in through a background thread the dataset needs to be updated which in turn automatically updates the datagrid. Now, the updates can be in the order of say, 7000 updates per 20 seconds. The probelm is that the UI hangs when such an update happens because it has to happen on the main thread. Is there a know solution for this problem?

Generally, how can you design highly performant enterprise applications in WinForms where the UI is being updated continuously without the application freezing?


Adding a scenario to explain this:

Consider this scenario. You have a tree view which you are using to represent some hierarchical data. Now the data update on the tree is async. The server can publish one or 1000 updates at the same same time. The update can be a modification of an existing item or addition of new nodes. The point to be noted is that the update cannot be delayed. The nodes represent a real time entity somewhere. Delaying the update will give the user the perception that the event itself was delayed. So this cant be done. If it was possible (from the business logic point of view) I would have done it a long time back.

There is a key point here: All the data need not be visible at the same time.

So that people dont suggest this anymore:

Adding a background worker thread WILL NOT HELP because the thread has to switch to the main thread to perform the update. The worker thread will make no difference.

like image 922
bobbyalex Avatar asked Nov 24 '11 11:11

bobbyalex


5 Answers

You cannot, unless you want to use DirectX.

Windows Forms is not designed for real-time information display. As many others have pointed out, you can get very close, but due to the way the Windows message loop works, you absolutely cannot guarantee that what is on the screen will be "real-time", even if you create a Timer that ticks at 60hz. Even if you do this in an event-driven manner, Windows will still queue up a WM_PAINT message that will inevitably be late if you are looking for real-time display.

If you truly want a extremely-close-to-realtime display, you will need to implement something similar to a Game Loop.

For an explanation of why the Windows Message loop won't work for real-time display, and what a game loop is, see: http://www.mvps.org/directx/articles/writing_the_game_loop.htm

A computer game cannot have any perceivable delay, so most computer games try to optimize performance so that they approach a framerate at or above the holy grail of 60hz. (Movies are only projected at 24hz, do you perceive them to be "delayed"?)

Writing an application with a real-time display is non-trivial and I would highly suggest considering compromising with what Windows provides in any of the following ways:

  • Create a timer that queues up screen updates at an acceptable rate (10 or more times per second). The user will not perceive the event as delayed, because a user cannot perceive delays that occur at a small fraction of a second.
  • Raise an event when the underlying data changes and let Windows to decide when to update the display (this will virtually always be acceptable).
  • If possible, come up with an alternate display that is not grid-based. Perhaps a scrolling console, or some other interface that displays the relevant information without overwriting old information. This may not be applicable, but coming up with another interface idea is often a good approach when the interface you want won't work.

If you really, really still want a very high-performance user interface, and write a game loop, you can do so in C# and paint a grid on your own to a DirectX surface. Once you get the hang of DirectX, drawing a grid is fairly easy, it's just a bunch of lines. With this approach you will avoid dealing with the Windows message loop, and potentially approach real-time performance.

Here's a great tutorial on how to use DirectX and how to render on a Windows form:

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

like image 126
Kevin McCormick Avatar answered Nov 20 '22 19:11

Kevin McCormick


In your comment you say that your heavy processing is reporting progress very often and you can't drop any report (because the report is real data that needs showing).

What you should do is implement (double) buffering, report progress to a buffer and only synchronize the buffer with GUI every once in a while.

Pseudocode follows:

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

If you give more precise info, I might be able to help further.

UPDATE

With the new details, I'd suggest buffering individual changes made to objects. The most general way of representing a change to some objects structure would be a function (perhaps parameterless Action). While receiving changes, you construct the update-functions modifying directly the view-bound data and store them in the buffer:

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
}

Now the DrawBuffer (name no longer fully adequate, but no matter) method would be easy:

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

Obviously I have not tried this exact solution, but it gives you the ability to control redraw frequency and redraw whole batches instead of single updates.

like image 29
Jacek Gorgoń Avatar answered Nov 20 '22 18:11

Jacek Gorgoń


I just made an sample application, that will fill its internal list through a BackgroundWorker and the data will be displayed within a DataGridView. You can change the speed of the inserts to find out if it meets your requirements:

The most interesting part should be the code within the form itself:

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

To let you access all these fields within the form, here comes the 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;
}

Last but not least my little person class that is used for the payload:

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; }
}
like image 26
Oliver Avatar answered Nov 20 '22 19:11

Oliver


Is the bottleneck in processing data from server or in actually putting it to DataGridView? If latter, VirtualMode can help you: http://msdn.microsoft.com/en-us/library/2b177d6d.aspx.

like image 1
Arthur Stankevich Avatar answered Nov 20 '22 19:11

Arthur Stankevich


Are you using a BackgroundWorker? Put the code that makes you application to freeze in DoWork event:

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

And start backgroundWorker like

backgroundWorker1.RunWorkerAsync();
like image 1
Deitools Avatar answered Nov 20 '22 20:11

Deitools