Keep UI thread responsive when running long task in windows forms

前端 未结 3 810

I am trying to read a large text file into a TextBox and keep the ui responsive when a file is dragged to the textbox.

Not works as expected, the windows forms is fr

相关标签:
3条回答
  • 2020-12-07 05:27

    Sometimes it is indeed required to do some asynchronous, background operation on the UI thread (e.g., syntax highlighting, spellcheck-as-you-type, etc). I am not going to question the design issues with your particular (IMO, contrived) example - most likely you should be using the MVVM pattern here - but you can certainly keep the UI thread responsive.

    You can do that by sensing for any pending user input and yielding to the main message loop, to give it the processing priority. Here's a complete, cut-paste-and-run example of how to do that in WinForms, based on the task you're trying to solve. Note await InputYield(token) which does just that:

    using System;
    using System.Runtime.InteropServices;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Windows.Forms;
    
    namespace WinFormsYield
    {
        static class Program
        {
            // a long-running operation on the UI thread
            private static async Task LongRunningTaskAsync(Action<string> deliverText, CancellationToken token)
            {
                for (int i = 0; i < 10000; i++)
                {
                    token.ThrowIfCancellationRequested();
                    await InputYield(token);
                    deliverText(await ReadLineAsync(token));
                }
            }
    
            [STAThread]
            static void Main()
            {
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
    
                // create some UI
    
                var form = new Form { Text = "Test", Width = 800, Height = 600 };
    
                var panel = new FlowLayoutPanel
                {
                    Dock = DockStyle.Fill,
                    FlowDirection = FlowDirection.TopDown,
                    WrapContents = true
                };
    
                form.Controls.Add(panel);
                var button = new Button { Text = "Start", AutoSize = true };
                panel.Controls.Add(button);
    
                var inputBox = new TextBox
                {
                    Text = "You still can type here while we're loading the file",
                    Width = 640
                };
                panel.Controls.Add(inputBox);
    
                var textBox = new TextBox
                {
                    Width = 640,
                    Height = 480,
                    Multiline = true,
                    ReadOnly = false,
                    AcceptsReturn = true,
                    ScrollBars = ScrollBars.Vertical
                };
                panel.Controls.Add(textBox);
    
                // handle Button click to "load" some text
    
                button.Click += async delegate
                {
                    button.Enabled = false;
                    textBox.Enabled = false;
                    inputBox.Focus();
                    try
                    {
                        await LongRunningTaskAsync(text =>
                            textBox.AppendText(text + Environment.NewLine),
                            CancellationToken.None);
                    }
                    catch (Exception ex)
                    {
                        MessageBox.Show(ex.Message);
                    }
                    finally
                    {
                        button.Enabled = true;
                        textBox.Enabled = true;
                    }
                };
    
                Application.Run(form);
            }
    
            // simulate TextReader.ReadLineAsync
            private static async Task<string> ReadLineAsync(CancellationToken token)
            {
                return await Task.Run(() =>
                {
                    Thread.Sleep(10); // simulate some CPU-bound work
                    return "Line " + Environment.TickCount;
                }, token);
            }
    
            //
            // helpers
            //
    
            private static async Task TimerYield(int delay, CancellationToken token)
            {
                // yield to the message loop via a low-priority WM_TIMER message (used by System.Windows.Forms.Timer)
                // https://web.archive.org/web/20130627005845/http://support.microsoft.com/kb/96006 
    
                var tcs = new TaskCompletionSource<bool>();
                using (var timer = new System.Windows.Forms.Timer())
                using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: false))
                {
                    timer.Interval = delay;
                    timer.Tick += (s, e) => tcs.TrySetResult(true);
                    timer.Enabled = true;
                    await tcs.Task;
                    timer.Enabled = false;
                }
            }
    
            private static async Task InputYield(CancellationToken token)
            {
                while (AnyInputMessage())
                {
                    await TimerYield((int)NativeMethods.USER_TIMER_MINIMUM, token);
                }
            }
    
            private static bool AnyInputMessage()
            {
                var status = NativeMethods.GetQueueStatus(NativeMethods.QS_INPUT | NativeMethods.QS_POSTMESSAGE);
                // the high-order word of the return value indicates the types of messages currently in the queue. 
                return status >> 16 != 0;
            }
    
            private static class NativeMethods
            {
                public const uint USER_TIMER_MINIMUM = 0x0000000A;
                public const uint QS_KEY = 0x0001;
                public const uint QS_MOUSEMOVE = 0x0002;
                public const uint QS_MOUSEBUTTON = 0x0004;
                public const uint QS_POSTMESSAGE = 0x0008;
                public const uint QS_TIMER = 0x0010;
                public const uint QS_PAINT = 0x0020;
                public const uint QS_SENDMESSAGE = 0x0040;
                public const uint QS_HOTKEY = 0x0080;
                public const uint QS_ALLPOSTMESSAGE = 0x0100;
                public const uint QS_RAWINPUT = 0x0400;
    
                public const uint QS_MOUSE = (QS_MOUSEMOVE | QS_MOUSEBUTTON);
                public const uint QS_INPUT = (QS_MOUSE | QS_KEY | QS_RAWINPUT);
    
                [DllImport("user32.dll")]
                public static extern uint GetQueueStatus(uint flags);
            }
        }
    }
    

    Now you should ask yourself what you're going to do if user modifies the content of the editor while it's still being populated with text on the background. Here for simplicity I just disable the button and the editor itself (the rest of the UI is accessible and responsive), but the question remains open. Also, you should look at implementing some cancellation logic, which I leave outside the scope of this sample.

    0 讨论(0)
  • 2020-12-07 05:36

    Perhaps use Microsoft's Reactive Framework for this. Here's the code you need:

    using System.Reactive.Concurrency;
    using System.Reactive.Linq;
    
    namespace YourNamespace
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
    
                IDisposable subscription =
                    Observable
                        .FromEventPattern<DragEventHandler, DragEventArgs>(h => values.DragDrop += h, h => values.DragDrop -= h)
                        .Select(ep => ((string[])ep.EventArgs.Data.GetData(DataFormats.FileDrop))[0])
                        .ObserveOn(Scheduler.Default)
                        .Where(dropped => dropped.Contains(".csv") || dropped.Contains(".txt"))
                        .SelectMany(dropped => System.IO.File.ReadLines(dropped))
                        .ObserveOn(this)
                        .Subscribe(line => values.AppendText(line + Environment.NewLine));
            }
        }
    }
    

    Should you want to clear the text box before adding values then replace the .SelectMany with this:

    .SelectMany(dropped => { values.Text = ""; return System.IO.File.ReadLines(dropped); })
    

    NuGet "System.Reactive" & "System.Reactive.Windows.Forms" to get the bits.

    When closing your form just do a subscription.Dispose() to remove the event handler.

    0 讨论(0)
  • 2020-12-07 05:43

    If you need to keep the UI responsive, just give it the time to breath.
    Reading one line of text is so fast that you are (a)waiting almost nothing, while updating the UI takes longer. Inserting even a very little delay lets the UI update.

    Using Async/Await (SynchronizationContext is captured by await)

    public Form1()
    {
       InitializeComponent();
       values.DragDrop += new DragEventHandler(this.OnDrop);
       values.DragEnter += new DragEventHandler(this.OnDragEnter);
    }
    
    public async void OnDrop(object sender, DragEventArgs e)
    {
       string dropped = ((string[])e.Data.GetData(DataFormats.FileDrop))[0];
       if (dropped.Contains(".csv") || dropped.Contains(".txt")) {
          try {
             string line = string.Empty;
             using (var reader = new StreamReader(dropped)) {
                while (reader.Peek() >= 0) {
                   line = await reader.ReadLineAsync();
                   values.AppendText(line.Replace(";", " ") + "\r\n");
                   await Task.Delay(10);
                }
             }
          }
          catch (Exception) {
             //Do something here
          }
       }
    }
    
    private void OnDragEnter(object sender, DragEventArgs e)
    {
       e.Effect = e.Data.GetDataPresent(DataFormats.FileDrop, false) 
                ? DragDropEffects.Copy 
                : DragDropEffects.None;
    }
    

    TPL using Task.Factory
    TPL executes Tasks through a TaskScheduler.
    A TaskScheduler may be used to queue tasks to a SynchronizationContext.

    TaskScheduler _Scheduler = TaskScheduler.FromCurrentSynchronizationContext();
    
    //No async here
    public void OnDrop(object sender, DragEventArgs e)
    {
       string dropped = ((string[])e.Data.GetData(DataFormats.FileDrop))[0];
       if (dropped.Contains(".csv") || dropped.Contains(".txt")) {
          Task.Factory.StartNew(() => {
             string line = string.Empty;
             int x = 0;
             try {
                using (var reader = new StreamReader(dropped)) {
                   while (reader.Peek() >= 0) {
                      line += (reader.ReadLine().Replace(";", " ")) + "\r\n";
                      ++x;
                      //Update the UI after reading 20 lines
                      if (x >= 20) {
                         //Update the UI or report progress 
                         Task UpdateUI = Task.Factory.StartNew(() => {
                            try {
                               values.AppendText(line);
                            }
                            catch (Exception) {
                               //An exception is raised if the form is closed
                            }
                         }, CancellationToken.None, TaskCreationOptions.PreferFairness, _Scheduler);
                         UpdateUI.Wait();
                         x = 0;
                      }
                   }
                }
             }
             catch (Exception) {
                //Do something here
             }
          });
       }
    }
    
    0 讨论(0)
提交回复
热议问题