Winforms updates with high performance

谁都会走 提交于 2019-11-28 14:34:03

There is little point in trying to report progress any faster than the user can keep track of it.

If your background thread is posting messages faster than the GUI can process them, (and you have all the symtoms of this - poor GUI resonse to user input, DoEvents runaway recursion), you have to throttle the progress updates somehow.

A common approach is to update the GUI using a main-thread form timer at a rate sufficiently small that the user sees an acceptable progress readout. You may need a mutex or critical section to protect shared data, though that amy not be necessary if the progress value to be monitored is an int/uint.

An alternative is to strangle the thread by forcing it to block on an event or semaphore until the GUI is idle.

noseratio

The UI thread should not be held for more than 50ms by a CPU-bound operation taking place on it ("The 50ms Rule"). Usually, the UI work items are executed upon events, triggered by user input, completion of an IO-bound operation or a CPU-bound operation offloaded to a background thread.

However, there are some rare cases when the work needs to be done on the UI thread. For example, you may need to poll a UI control for changes, because the control doesn't expose proper onchange-style event. Particularly, this applies to WebBrowser control (DOM Mutation Observers are only being introduced, and IHTMLChangeSink doesn't always work reliably, in my experience).

Here is how it can be done efficiently, without blocking the UI thread message queue. A few key things was used here to make this happen:

  • The UI work tasks yields (via Application.Idle) to process any pending messages
  • GetQueueStatus is used to decide on whether to yield or not
  • Task.Delay is used to throttle the loop, similar to a timer event. This step is optional, if the polling needs to be as precise as possible.
  • async/await provide pseudo-synchronous linear code flow.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WinForms_21643584
{
    public partial class MainForm : Form
    {
        EventHandler ContentChanged = delegate { };

        public MainForm()
        {
            InitializeComponent();
            this.Load += MainForm_Load;
        }

        // Update UI Task
        async Task DoUiWorkAsync(CancellationToken token)
        {
            try
            {
                var startTick = Environment.TickCount;
                var editorText = this.webBrowser.Document.Body.InnerText;
                while (true)
                {
                    // observe cancellation
                    token.ThrowIfCancellationRequested();

                    // throttle (optional)
                    await Task.Delay(50);

                    // yield to keep the UI responsive
                    await ApplicationExt.IdleYield();

                    // poll the content for changes
                    var newEditorText = this.webBrowser.Document.Body.InnerText;
                    if (newEditorText != editorText)
                    {
                        editorText = newEditorText;
                        this.status.Text = "Changed on " + (Environment.TickCount - startTick) + "ms";
                        this.ContentChanged(this, EventArgs.Empty);
                    }
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

        async void MainForm_Load(object sender, EventArgs e)
        {
            // navigate the WebBrowser
            var documentTcs = new TaskCompletionSource<bool>();
            this.webBrowser.DocumentCompleted += (sIgnore, eIgnore) => documentTcs.TrySetResult(true);
            this.webBrowser.DocumentText = "<div style='width: 100%; height: 100%' contentEditable='true'></div>";
            await documentTcs.Task;

            // cancel updates in 10 s
            var cts = new CancellationTokenSource(20000);

            // start the UI update 
            var task = DoUiWorkAsync(cts.Token);
        }
    }

    // Yield via Application.Idle
    public static class ApplicationExt
    {
        public static Task<bool> IdleYield()
        {
            var idleTcs = new TaskCompletionSource<bool>();
            if (IsMessagePending())
            {
                // register for Application.Idle
                EventHandler handler = null;
                handler = (s, e) =>
                {
                    Application.Idle -= handler;
                    idleTcs.SetResult(true);
                };
                Application.Idle += handler;
            }
            else
                idleTcs.SetResult(false);
            return idleTcs.Task;
        }

        public static bool IsMessagePending()
        {
            // The high-order word of the return value indicates the types of messages currently in the queue. 
            return 0 != (GetQueueStatus(QS_MASK) >> 16 & QS_MASK);
        }

        const uint QS_MASK = 0x1FF;

        [System.Runtime.InteropServices.DllImport("user32.dll")]
        static extern uint GetQueueStatus(uint flags);
    }
}

This code is specific to WinForms. Here is a similar approach for WPF.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!