在Windows窗体中运行长任务时,保持UI线程响应

问题描述 投票:1回答:3

我试图将一个大文本文件读入一个TextBox,并在文件被拖动到文本框时保持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) { }
}
c# winforms async-await task-parallel-library
3个回答
2
投票

有时确实需要在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的其余部分是可访问的和响应的),但问题仍然存在。此外,您应该考虑实现一些取消逻辑,我将其留在本示例的范围之外。


2
投票

如果您需要保持UI响应,请给它时间呼吸。 读取一行文本是如此之快,以至于您(a)几乎什么都不等,而更新UI需要更长时间。插入甚至很短的延迟都可以让UI更新。

使用Async / Await(等待捕获SynchronizationContext)

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 _s = string.Empty;
         using (TextReader tr = new StreamReader(_dropped))
         {
            while (tr.Peek() >= 0)
            {
               _s = await tr.ReadLineAsync();
               values.AppendText(_s.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 _s = string.Empty;
         int x = 0;
         try
         {
            using (TextReader tr = new StreamReader(_dropped))
            {
               while (tr.Peek() >= 0)
               {
                  _s += (tr.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(_s);
                        }
                        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
         }
      });
   }
}

1
投票

或许可以使用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()即可删除事件处理程序。

© www.soinside.com 2019 - 2024. All rights reserved.