2008-08-08 30 views
32

Po zasubskrybowaniu zdarzenia na obiekcie z poziomu formularza zasadniczo przekazujesz kontrolę nad metodą wywołania zwrotnego do źródła zdarzenia. Nie masz pojęcia, czy to źródło zdarzenia wybierze wyzwolenie zdarzenia w innym wątku.Jak sprawić, aby wywołania zwrotne zdarzeń w moich formularzach wygranych były bezpieczne?

Problem polega na tym, że kiedy wywoływany jest wywołanie zwrotne, nie można założyć, że można wprowadzać formanty aktualizacji w formularzu, ponieważ czasami te formanty wyrzucą oczekiwanie, jeśli wywołanie zwrotne zdarzenia zostało wywołane w wątku innym niż wątek, który formularz był biegnij dalej.

Odpowiedz

31

Aby nieco uprościć kod Simona, można użyć wbudowanego ogólnego gestu akcji. Oszczędza to pieprzenie kodu przy użyciu kilku typów delegatów, których naprawdę nie potrzebujesz. Ponadto w .NET 3.5 dodali parametr params do metody Invoke, więc nie musisz definiować tymczasowej tablicy.

void SomethingHappened(object sender, EventArgs ea) 
{ 
    if (InvokeRequired) 
    { 
     Invoke(new Action<object, EventArgs>(SomethingHappened), sender, ea); 
     return; 
    } 

    textBox1.Text = "Something happened"; 
} 
15

Oto najistotniejsze punkty:

  1. Nie można wykonywać połączenia kontrolne UI z innego wątku niż ten którym zostały utworzone (przewlec formularza).
  2. Delegowane wywołania (tj. Haki zdarzeń) są wyzwalane w tym samym wątku, co obiekt, który uruchamia zdarzenie.

Tak więc, jeśli masz oddzielną „silnik” wątek jakiejś pracy i mają niektóre UI oglądania dla zmian stanu, które mogą być odzwierciedlone w interfejsie użytkownika (takie jak pasek postępu lub cokolwiek), masz problem. Pożar silnika to obiekt, który zmienił wydarzenie, które zostało zahaczone przez formę. Ale delegat wywołania zwrotnego, że formularz zarejestrowany w silniku zostanie wywołany w wątku silnika ... nie w wątku formularza. Dlatego nie można aktualizować żadnych formantów z tego wywołania zwrotnego. Doh!

BeginInvoke przychodzi na ratunek. Wystarczy użyć tego prostego modelu kodowania wszystkich metod zwrotnych i można mieć pewność, że wszystko będzie w porządku:

private delegate void EventArgsDelegate(object sender, EventArgs ea); 

void SomethingHappened(object sender, EventArgs ea) 
{ 
    // 
    // Make sure this callback is on the correct thread 
    // 
    if (this.InvokeRequired) 
    { 
     this.Invoke(new EventArgsDelegate(SomethingHappened), new object[] { sender, ea }); 
     return; 
    } 

    // 
    // Do something with the event such as update a control 
    // 
    textBox1.Text = "Something happened"; 
} 

To naprawdę bardzo proste.

  1. Zastosowanie InvokeRequired, aby dowiedzieć się, czy to zwrotna się na odpowiednim wątku.
  2. Jeśli nie, ponownie wywołaj wywołanie zwrotne dla prawidłowego wątku o tych samych parametrach. Metodę można przywrócić, stosując metody Inhibition (blocking) lub BeginInvoke (bez blokowania).
  3. Następnym razem, gdy funkcja zostanie wywołana, InvokeRequired zwraca wartość false, ponieważ jesteśmy teraz we właściwym wątku i wszyscy są szczęśliwi.

Jest to bardzo kompaktowy sposób rozwiązania tego problemu i zabezpieczenia formularzy przed wielowątkowymi wywołaniami zdarzeń.

+1

Ogólnie preferuję BeginInvoke do Invoke, ale jest pewne zastrzeżenie: należy unikać stania w kolejce zbyt wielu zdarzeń. Używam zmiennej updateRequired, która jest ustawiona na 1, gdy nastąpi BeginInvoke, i wykonuje tylko BeginInvoke, jeśli było zero (za pomocą Interlocked.Exchange). Obsługa wyświetlania ma pętlę while, która usuwa updateRequired, a jeśli nie było zera, wykonuje aktualizację i wykonuje pętle. W niektórych przypadkach dodawany jest zegar, aby dodatkowo ograniczyć częstotliwość aktualizacji (aby uniknąć sytuacji, w której kod spędzał cały czas aktualizując odczyt postępu zamiast wykonywać prawdziwą pracę), ale to jest bardziej skomplikowane. – supercat

+0

@Supercat ... zdarzenie dławienie jest ważnym tematem dla wielu aplikacji, ale nie jest to coś, co powinno być częścią warstwy interfejsu użytkownika. Oddzielna magistrala proxy zdarzeń powinna zostać utworzona w celu odbierania, kolejkowania, łączenia i ponownego wysyłania zdarzeń w odpowiednich odstępach czasu. Dowolny subskrybent magistrali zdarzeń nie powinien wiedzieć, że występuje dławienie zdarzenia. –

+0

Widzę miejsca, w których może być przydatna osobna "magistrala zdarzeń" do obsługi synchronizacji, ale w wielu przypadkach wydaje się najłatwiejszemu użytkownikowi końcowemu coś takiego, jak klasa wskaźnika postępu, jeśli klasa po prostu ujawniła właściwość MinimumUpdateInterval. – supercat

0

W wielu prostych przypadkach można użyć delegata MethodInvoker i uniknąć konieczności tworzenia własnego typu delegata.

9

używam anonimowe metody dużo w tym scenariuszu:

void SomethingHappened(object sender, EventArgs ea) 
{ 
    MethodInvoker del = delegate{ textBox1.Text = "Something happened"; }; 
    InvokeRequired ? Invoke(del) : del(); 
} 
2

Jestem trochę późno na ten temat, ale warto spojrzeć na Event-Based Asynchronous Pattern. Po prawidłowym wdrożeniu gwarantuje, że zdarzenia są zawsze wywoływane z wątku interfejsu użytkownika.

Oto krótki przykład, który zezwala tylko na jedno równoczesne wywoływanie; obsługa wielu inwokacji/zdarzeń wymaga trochę więcej hydrauliki.

using System; 
using System.ComponentModel; 
using System.Threading; 
using System.Windows.Forms; 

namespace WindowsFormsApplication1 
{ 
    public class MainForm : Form 
    { 
     private TypeWithAsync _type; 

     [STAThread()] 
     public static void Main() 
     { 
      Application.EnableVisualStyles(); 
      Application.Run(new MainForm()); 
     } 

     public MainForm() 
     { 
      _type = new TypeWithAsync(); 
      _type.DoSomethingCompleted += DoSomethingCompleted; 

      var panel = new FlowLayoutPanel() { Dock = DockStyle.Fill }; 

      var btn = new Button() { Text = "Synchronous" }; 
      btn.Click += SyncClick; 
      panel.Controls.Add(btn); 

      btn = new Button { Text = "Asynchronous" }; 
      btn.Click += AsyncClick; 
      panel.Controls.Add(btn); 

      Controls.Add(panel); 
     } 

     private void SyncClick(object sender, EventArgs e) 
     { 
      int value = _type.DoSomething(); 
      MessageBox.Show(string.Format("DoSomething() returned {0}.", value)); 
     } 

     private void AsyncClick(object sender, EventArgs e) 
     { 
      _type.DoSomethingAsync(); 
     } 

     private void DoSomethingCompleted(object sender, DoSomethingCompletedEventArgs e) 
     { 
      MessageBox.Show(string.Format("DoSomethingAsync() returned {0}.", e.Value)); 
     } 
    } 

    class TypeWithAsync 
    { 
     private AsyncOperation _operation; 

     // synchronous version of method 
     public int DoSomething() 
     { 
      Thread.Sleep(5000); 
      return 27; 
     } 

     // async version of method 
     public void DoSomethingAsync() 
     { 
      if (_operation != null) 
      { 
       throw new InvalidOperationException("An async operation is already running."); 
      } 

      _operation = AsyncOperationManager.CreateOperation(null); 
      ThreadPool.QueueUserWorkItem(DoSomethingAsyncCore); 
     } 

     // wrapper used by async method to call sync version of method, matches WaitCallback so it 
     // can be queued by the thread pool 
     private void DoSomethingAsyncCore(object state) 
     { 
      int returnValue = DoSomething(); 
      var e = new DoSomethingCompletedEventArgs(returnValue); 
      _operation.PostOperationCompleted(RaiseDoSomethingCompleted, e); 
     } 

     // wrapper used so async method can raise the event; matches SendOrPostCallback 
     private void RaiseDoSomethingCompleted(object args) 
     { 
      OnDoSomethingCompleted((DoSomethingCompletedEventArgs)args); 
     } 

     private void OnDoSomethingCompleted(DoSomethingCompletedEventArgs e) 
     { 
      var handler = DoSomethingCompleted; 

      if (handler != null) { handler(this, e); } 
     } 

     public EventHandler<DoSomethingCompletedEventArgs> DoSomethingCompleted; 
    } 

    public class DoSomethingCompletedEventArgs : EventArgs 
    { 
     private int _value; 

     public DoSomethingCompletedEventArgs(int value) 
      : base() 
     { 
      _value = value; 
     } 

     public int Value 
     { 
      get { return _value; } 
     } 
    } 
} 
+1

Myślę, że to trochę mylące stwierdzenie: "gwarantuje, że zdarzenia są zawsze wywoływane z wątku UI". Czy nie byłoby dokładniej powiedzieć, że zapewnia to, że procedura obsługi zdarzeń jest wykonywana w tym samym katalogu SynchronizationContext/wątku, w którym zostało utworzone zadanie? (Które mogą nie być wątki UI/Kontekst synchronizacji) – jspaey

1

Jako lazy programmer, mam bardzo leniwy sposób to robi.

To, co robię, to po prostu to.

private void DoInvoke(MethodInvoker del) { 
    if (InvokeRequired) { 
     Invoke(del); 
    } else { 
     del(); 
    } 
} 
//example of how to call it 
private void tUpdateLabel(ToolStripStatusLabel lbl, String val) { 
    DoInvoke(delegate { lbl.Text = val; }); 
} 

Możesz wbudować funkcję DoInvoke w swojej funkcji lub ukryć ją w oddzielnej funkcji, aby wykonać za ciebie brudną robotę.

Pamiętaj, że możesz przekazywać funkcje bezpośrednio do metody DoInvoke.

private void directPass() { 
    DoInvoke(this.directInvoke); 
} 
private void directInvoke() { 
    textLabel.Text = "Directly passed."; 
} 
+0

Jestem za leniwym programowaniem :) Jeśli używasz .NET 3.5 lub nowszego, możesz użyć 'Action' lub' Action 'razem z lambda wyrażenia: 'Doinvoke (() => textLabel.Text =" Something ")' –