在 windows 表单中 运行 长任务时保持 UI 线程响应

Keep UI thread responsive when running long task in windows forms

我正在尝试将一个大文本文件读入文本框,并在将文件拖到文本框时保持 ui 响应。

未按预期工作,windows 表单被冻结,似乎只能执行读取文件并将内容附加到文本框的任务。

IDE 引发了 ContextSwitchDeadLock,但这并不是真正的错误。 这是一项很长的 运行 任务。我已修复它更改异常菜单下的行为。

感谢 JSteward,Peter 将代码更改为这个。

如何在 运行 此任务时保持 ui(主线程)响应? 谢谢

private SynchronizationContext fcontext;

public Form1()
{      
    InitializeComponent();            
    values.DragDrop += values_DragDrop; //<----------- This is a textbox
    fcontext = WindowsFormsSynchronizationContext.Current;
}

// The async callback 
async void values_DragDrop(object sender, DragEventArgs e)
{
    try
    {
        string dropped = ( (string[]) e.Data.GetData(DataFormats.FileDrop))[0];
        if ( dropped.Contains(".csv") || dropped.Contains(".txt"))
        {
                using ( StreamReader sr = File.OpenText(dropped) )
                {
                    string s = String.Empty;
                    while ( ( s = await sr.ReadLineAsync() ) != null )
                    {                                                                
                       values.AppendText(s.Replace(";",""));
                    }
                }                 
         }
     }
  catch (Exception ex) { }
}

也许为此使用 Microsoft 的 Reactive Framework。这是您需要的代码:

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

如果您想在添加值之前清除文本框,则将 .SelectMany 替换为:

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

NuGet "System.Reactive" & "System.Reactive.Windows.Forms" 获取位。

关闭表单时只需执行 subscription.Dispose() 即可删除事件处理程序。

如果您需要保持 UI 响应,只需给它喘息的时间。
阅读一行文本是如此之快,以至于您 (a) 几乎什么都不等待,而更新 UI 则需要更长的时间。即使插入非常小的延迟也会让 UI 更新。

使用Async/Await(SynchronizationContext被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 使用 Task.Factory
TPL 通过 TaskScheduler 执行任务。
TaskScheduler 可用于将任务排队到 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
         }
      });
   }
}

有时确实需要在 UI 线程上执行一些异步的后台操作(例如,语法高亮显示、拼写检查等)。我不会质疑您的特定(IMO,人为的)示例的设计问题 - 很可能您应该在这里使用 MVVM 模式 - 但您当然可以保持 UI 线程响应。

您可以通过检测任何待处理的用户输入并让位于主消息循环来为其提供处理优先级来做到这一点。这是一个完整的剪切粘贴 运行 示例,说明如何根据您要解决的任务在 WinForms 中执行此操作。注意 await InputYield(token) 就是这样做的:

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

现在您应该问问自己,如果用户修改了编辑器的内容,而编辑器的内容仍在背景中填充文本,您将如何处理。在这里为了简单起见,我只是禁用按钮和编辑器本身(UI 的其余部分是可访问和响应的),但问题仍然悬而未决。此外,您应该考虑实现一些取消逻辑,我将其排除在本示例的范围之外。