簡體   English   中英

在 Windows 窗體中運行長任務時保持 UI 線程響應

[英]Keep UI thread responsive when running long task in windows forms

我試圖將一個大文本文件讀入一個文本框,並在將文件拖到文本框時保持 ui 響應。

無法按預期工作,windows 窗體被凍結,似乎只能執行讀取文件並將內容附加到文本框的任務。

IDE 拋出了 ContextSwitchDeadLock,但實際上並不是錯誤。 這是一項長期運行的任務。 我已修復它更改異常菜單下的行為。

感謝JSteward,彼得將代碼改成了這樣。

運行此任務時如何保持 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) { }
}

如果您需要保持 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;
}

使用 Task.Factory 的 TPL
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 的其余部分是可訪問和響應的),但問題仍然存在。 此外,您應該考慮實現一些取消邏輯,我不在本示例的范圍內。

也許為此使用 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()即可刪除事件處理程序。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM