2009-01-29 22 views
35

Czasami chcesz zablokować wątek podczas oczekiwania na zdarzenie.Blokowanie i oczekiwanie na zdarzenie

zwykle zrobić to tak:

private AutoResetEvent _autoResetEvent = new AutoResetEvent(false); 

private void OnEvent(object sender, EventArgs e){ 
    _autoResetEvent.Set(); 
} 

// ... 
button.Click += OnEvent; 
try{ 
    _autoResetEvent.WaitOne(); 
} 
finally{ 
    button.Click -= OnEvent; 
} 

Wydaje się jednak, że powinno to być coś, co mogłem wydobyć do wspólnej klasy (a może nawet coś, co już istnieje w ramach).

Chciałbym móc zrobić coś takiego:

EventWaiter ew = new EventWaiter(button.Click); 
ew.WaitOne(); 
EventWaiter ew2 = new EventWaiter(form.Closing); 
ew2.WaitOne(); 

Ale nie mogę znaleźć sposób, aby zbudować taką klasę (nie mogę znaleźć dobrą prawidłową drogę do zabicia wydarzenie jako argument). Czy ktoś może pomóc?

Aby dać przykład, dlaczego to może być przydatne, należy rozważyć coś takiego:

var status = ShowStatusForm(); 
status.ShowInsertUsbStick(); 
bool cancelled = WaitForUsbStickOrCancel(); 
if(!cancelled){ 
    status.ShowWritingOnUsbStick(); 
    WriteOnUsbStick(); 
    status.AskUserToRemoveUsbStick(); 
    WaitForUsbStickToBeRemoved(); 
    status.ShowFinished(); 
}else{ 
    status.ShowCancelled(); 
} 
status.WaitUntilUserPressesDone(); 

Jest to o wiele bardziej zwięzły i czytelny niż równoważny kod napisany z logiką rozłożone pomiędzy wieloma metodami. Ale aby zaimplementować WaitForUsbStickOrCancel(), WaitForUsbStickToBeRemoved i WaitUntilUserPressesDone() (zakładamy, że otrzymujemy zdarzenie podczas wstawiania lub usuwania pamięci USB), muszę za każdym razem ponownie zaimplementować "EventWaiter". Oczywiście musisz uważać, aby nigdy nie uruchamiać tego w wątku GUI, ale czasami jest to warte kompromisu dla prostszego kodu.

Alternatywą mogłoby wyglądać tak:

var status = ShowStatusForm(); 
status.ShowInsertUsbStick(); 
usbHandler.Inserted += OnInserted; 
status.Cancel += OnCancel; 
//... 
void OnInserted(/*..*/){ 
    usbHandler.Inserted -= OnInserted; 
    status.ShowWritingOnUsbStick(); 
    MethodInvoker mi =() => WriteOnUsbStick(); 
    mi.BeginInvoke(WritingDone, null); 
} 
void WritingDone(/*..*/){ 
    /* EndInvoke */ 
    status.AskUserToRemoveUsbStick(); 
    usbHandler.Removed += OnRemoved; 
} 
void OnRemoved(/*..*/){ 
    usbHandler.Removed -= OnRemoved; 
    status.ShowFinished(); 
    status.Done += OnDone; 
} 
/* etc */ 

uważam, że znacznie trudniej odczytać. Prawdą jest, że przepływ nie będzie zawsze taki liniowy, ale kiedy jest, podoba mi się pierwszy styl.

Jest to porównywalne do używania funkcji ShowMessage() i Form.ShowDialog() - blokują one również do momentu pojawienia się "zdarzenia" (chociaż uruchomią pętlę komunikatów, jeśli zostaną wywołane w gui-thread).

+0

Jestem ciekaw dlaczego dokładnie, czego chcesz to zrobić ... możesz rozwinąć? –

+0

Nadal nie rozumiem, dlaczego chcesz zablokować wątek, zamiast tylko czekać na wydarzenie - co to oznacza? –

+0

Czy kiedykolwiek to naprawiłeś? Mam ten sam problem. – Carlo

Odpowiedz

3

Nie należy przekazywać zdarzenia, przekazywać delegatów pasujących do sygnatury procedury obsługi zdarzeń. To naprawdę brzmi dla mnie hacky, więc bądź świadomy potencjalnych problemów z martwymi blokadami.

+0

Nie jestem pewien, czy rozumiem. Czy masz na myśli przekazywanie delegatów, którzy subskrybują i rezygnują z wydarzenia (np. nowy EventWaiter (eh => {button.Click + = eh;}, eh => {button.Click - = eh;})) To by działało, jak sądzę, ale wolałbym coś prostszego: –

-7

Myślę, że jak te powinny działać, nie próbowałem po prostu kodowane.

public class EventWaiter<T> where T : EventArgs 
{ 
    private System.Threading.ManualResetEvent manualEvent; 

    public EventWaiter(T e) 
    { 
     manualEvent = new System.Threading.ManualResetEvent(false); 
     e += this.OnEvent; 
    } 

    public void OnEvent(object sender, EventArgs e) 
    { 
     manualEvent.Set(); 
    } 

    public void WaitOne() 
    { 
     manualEvent.WaitOne(); 
    } 

    public void Reset() 
    { 
     manualEvent.Reset(); 
    } 
} 

Nie myślał o zbyt wiele, ale nie może dowiedzieć się, jak zrobić to odizolowany od EventArgs.

Spójrz na MSDN ManualResetEvent, a odkryjesz, że możesz łączyć w łańcuchy oczekiwania i tak dziwne rzeczy.

+1

To nie jest problem z EventArgs.Nie byłoby problemu z generowaniem go w typie EventArgs. nie wydaje się, aby można było przekazać zdarzenie do metody, aby można było dołączyć moduł obsługi zdarzeń (nie jest to EventArgs, do którego należy się przyłączyć, jak w przykładzie). –

+0

OMG !! Tak, masz rację, ja nie nie wiem o czym myślałem! – brutuscat

0

Połączyłem działającą próbkę w LinqPadzie za pomocą odbicia, uzyskując odniesienie do obiektu EventInfo za pomocą ciągu (uważaj, ponieważ tracisz kontrolę czasu kompilacji). Oczywistym problemem jest to, że nie ma gwarancji, że zdarzenie kiedykolwiek zostanie wyrzucone, lub że zdarzenie, o które spodziewamy się może zostać wywołane, zanim klasa EventWaiter będzie gotowa do rozpoczęcia blokowania, więc nie jestem pewien, czy spałbym wygodnie, gdybym to umieścił aplikacja do produkcji.

void Main() 
{ 
    Console.WriteLine("main thread started"); 

    var workerClass = new WorkerClassWithEvent(); 
    workerClass.PerformWork(); 

    var waiter = new EventWaiter(workerClass, "WorkCompletedEvent"); 
    waiter.WaitForEvent(TimeSpan.FromSeconds(10)); 

    Console.WriteLine("main thread continues after waiting"); 
} 

public class WorkerClassWithEvent 
{ 
    public void PerformWork() 
    { 
     var worker = new BackgroundWorker(); 
     worker.DoWork += (s, e) => 
     { 
      Console.WriteLine("threaded work started"); 
      Thread.Sleep(1000); // <= the work 
      Console.WriteLine("threaded work complete"); 
     }; 
     worker.RunWorkerCompleted += (s, e) => 
     { 
      FireWorkCompletedEvent(); 
      Console.WriteLine("work complete event fired"); 
     }; 

     worker.RunWorkerAsync(); 
    } 

    public event Action WorkCompletedEvent; 
    private void FireWorkCompletedEvent() 
    { 
     if (WorkCompletedEvent != null) WorkCompletedEvent(); 
    } 
} 

public class EventWaiter 
{ 
    private AutoResetEvent _autoResetEvent = new AutoResetEvent(false); 
    private EventInfo _event    = null; 
    private object _eventContainer   = null; 

    public EventWaiter(object eventContainer, string eventName) 
    { 
     _eventContainer = eventContainer; 
     _event = eventContainer.GetType().GetEvent(eventName); 
    } 

    public void WaitForEvent(TimeSpan timeout) 
    { 
     _event.AddEventHandler(_eventContainer, (Action)delegate { _autoResetEvent.Set(); }); 
     _autoResetEvent.WaitOne(timeout); 
    } 
} 

Wyjście

// main thread started 
// threaded work started 
// threaded work complete 
// work complete event fired 
// main thread continues after waiting 
3

I zmodyfikowane martwy.Klasa Rabit EventWaiter do obsługi EventHandler<T>. Możesz więc użyć do oczekiwania na wszystkie zdarzenia typu EventHandler<T>, co oznacza, że ​​Twój delegat jest podobny do delegate void SomeDelegate(object sender, T EventsArgs).

public class EventWaiter<T> 
{ 

    private AutoResetEvent _autoResetEvent = new AutoResetEvent(false); 
    private EventInfo _event = null; 
    private object _eventContainer = null; 

    public EventWaiter(object eventContainer, string eventName) 
    { 
     _eventContainer = eventContainer; 
     _event = eventContainer.GetType().GetEvent(eventName); 
    } 

    public void WaitForEvent(TimeSpan timeout) 
    { 
     EventHandler<T> eventHandler = new EventHandler<T>((sender, args) => { _autoResetEvent.Set(); }); 
     _event.AddEventHandler(_eventContainer, eventHandler); 
     _autoResetEvent.WaitOne(timeout); 
     _event.RemoveEventHandler(_eventContainer, eventHandler); 
    } 
} 

I na przykład używam tego oczekiwania, aby uzyskać URL z HttpNotificationChannel kiedy rejestracji do okien wcisnąć usługę powiadamiania.

  HttpNotificationChannel pushChannel = new HttpNotificationChannel(channelName); 
      //ChannelUriUpdated is event 
      EventWaiter<NotificationChannelUriEventArgs> ew = new EventWaiter<NotificationChannelUriEventArgs>(pushChannel, "ChannelUriUpdated"); 
      pushChannel.Open(); 
      ew.WaitForEvent(TimeSpan.FromSeconds(30)); 
0

Można też spróbować:

class EventWaiter<TEventArgs> where TEventArgs : EventArgs 
{ 
    private readonly Action<EventHandler<TEventArgs>> _unsubHandler; 
    private readonly Action<EventHandler<TEventArgs>> _subHandler; 

    public EventWaiter(Action<EventHandler<TEventArgs>> subHandler, Action<EventHandler<TEventArgs>> unsubHandler) 
    { 
     _unsubHandler = unsubHandler; 
     _subHandler = subHandler; 
    } 

    protected void Handler(object sender, TEventArgs args) 
    { 
     _unsubHandler.Invoke(Handler); 
     TaskCompletionSource.SetResult(args); 
    } 

    public TEventArgs WaitOnce() 
    { 
     TaskCompletionSource = new TaskCompletionSource<TEventArgs>(); 
     _subHandler.Invoke(Handler); 
     return TaskCompletionSource.Task.Result; 
    } 

    protected TaskCompletionSource<TEventArgs> TaskCompletionSource { get; set; } 

} 

Zastosowanie:

EventArgs eventArgs = new EventWaiter<EventArgs>((h) => { button.Click += new EventHandler(h); }, (h) => { button.Click -= new EventHandler(h); }).WaitOnce();