2017-12-20 4 views
1

Ich versuche, eine große Textdatei in eine TextBox zu lesen und halten Sie die UI reagieren, wenn eine Datei in das Textfeld gezogen wird.Behalten Sie UI-Thread reagieren, wenn lange Task in Windows-Formularen ausgeführt wird

Nicht funktioniert wie erwartet, die Windows-Formulare ist eingefroren und scheint nur die Aufgabe zu arbeiten, die Datei zu lesen und den Inhalt an die Textbox anzufügen.

Ein ContextSwitchDeadLock wurde von der IDE geworfen, aber nicht wirklich ein Fehler. Dies ist eine lange laufende Aufgabe. Ich habe es behoben, das Verhalten im Ausnahmemenü zu ändern.

Dank Jsteward hat Peter den Code geändert.

Wie kann ich die UI (Haupt-Thread) reagieren, wenn diese Aufgabe ausgeführt wird? Danke.

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) { } 
} 
+2

Sie könnten einfach '... AppendText (erwarten ... ReadLineAsync)' anstelle der blockierenden Variante der 'File' API. Auf diese Weise müssen Sie nicht manuell in einem Kontext speichern oder posten. – JSteward

+0

@JSteward Tried values.AppendText (erwartet sr .ReadLineAsync()) aber nur warten kann auf einen Lamba-Ausdruck angewendet werden. – ppk

+2

Ich denke, du meinst, es kann nur auf einen ** asynchronen ** Lamda-Ausdruck angewendet werden. Können Sie den Code in der Frage aktualisieren? – JSteward

Antwort

2

Manchmal ist es in der Tat erforderlich, um einigen asynchronen Hintergrundvorgang auf dem UI-Thread zu tun (z.B. Syntaxhervorhebung, Rechtschreibprüfung-as-you-Typ usw.). Ich werde die Design-Probleme nicht mit Ihrem bestimmten (IMO, erfundenen) Beispiel in Frage stellen - höchstwahrscheinlich sollten Sie das MVVM-Muster hier verwenden - aber Sie können den UI-Thread sicher reaktionsfähig halten.

Sie können dies tun, indem Sie nach einer ausstehenden Benutzereingabe suchen und der Hauptnachrichtenschleife nachgeben, um ihr die Verarbeitungspriorität zu geben. Im Folgenden finden Sie ein vollständiges Beispiel für das Ausschneiden, Einfügen und Ausführen von WinForms anhand der Aufgabe, die Sie lösen möchten.Hinweis await InputYield(token), die genau das tut:

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

Jetzt sollten Sie sich fragen, was Sie tun werden, wenn der Benutzer den Inhalt des Editors ändert, während er noch mit dem Text auf dem Hintergrund bevölkert werden wird. Aus Gründen der Einfachheit deaktiviere ich einfach die Schaltfläche und den Editor selbst (der Rest der Benutzeroberfläche ist zugänglich und reaktionsfähig), aber die Frage bleibt offen. Außerdem sollten Sie eine Löschlogik implementieren, die ich außerhalb des Anwendungsbereichs dieses Beispiels belasse.

+0

Schön! Aber ich habe eine Frage. Warum verwenden Sie Thread.Sleep stattdessen Task.Delay? – ppk

+0

@ppk, die ReadLineAsync-Implementierung ist hier nur ein Testbeispiel. Ich benutze 'Thread.Sleep' in einem' Task.Run'-Lambda, um hier eine CPU-gebundene Arbeit zum Erzeugen einer Textzeile zu bezeichnen, die in einen Pool-Thread ausgelagert wird. Anders als beim Testen benötigen Sie 'Thread.Sleep' fast nie in einem Produktionscode. – Noseratio

+2

Danke für die zusätzliche Erklärung. – ppk

1

Vielleicht verwenden Sie Microsoft Reactive Framework dafür. Hier ist der Code, den Sie benötigen:

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

Sollten Sie das Textfeld zu löschen, bevor das Hinzufügen Werte dann die .SelectMany mit diesem ersetzen:

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

NuGet "System.Reactive" & „System.Reactive. Windows.Forms "um die Bits zu bekommen.

Wenn Sie Ihr Formular schließen Sie einfach eine subscription.Dispose(), um den Event-Handler zu entfernen.

2

Wenn Sie die Benutzeroberfläche ansprechbar halten müssen, geben Sie ihr einfach die Zeit zum Atmen.
Das Lesen einer Textzeile ist so schnell, dass Sie (a) fast nichts erwarten, während das Aktualisieren der Benutzeroberfläche länger dauert. Wenn Sie nur eine sehr kleine Verzögerung einfügen, wird die Benutzeroberfläche aktualisiert.

Mit Async/Await (SynchronizationContext durch Await erfasst wird)

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 Verwendung führt Aufgaben durch ein Taskscheduler.
Ein TaskScheduler kann zum Einreihen von Aufgaben in einen SynchronizationContext verwendet werden.

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