web-dev-qa-db-fra.com

Blocage et attente d'un événement

Parfois, il veut bloquer mon thread en attendant qu'un événement se produise.

Je fais habituellement quelque chose comme ça:

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

Cependant, il semble que cela devrait être quelque chose que je pourrais extraire à une classe commune (ou peut-être même quelque chose qui existe déjà dans le cadre).

J'aimerais pouvoir faire quelque chose comme ça:

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

Mais je ne peux pas vraiment trouver un moyen de construire une telle classe (je ne peux pas trouver un bon moyen valable de passer l'événement comme argument). Quelqu'un peut-il aider?

Pour donner un exemple de pourquoi cela peut être utile, considérons quelque chose comme ceci:

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

Ceci est beaucoup plus concis et lisible que le code équivalent écrit avec une logique répartie entre plusieurs méthodes. Mais pour implémenter WaitForUsbStickOrCancel (), WaitForUsbStickToBeRemoved et WaitUntilUserPressesDone () (supposons que nous obtenons un événement lorsque des clés USB sont insérées ou supprimées), je dois réimplémenter "EventWaiter" à chaque fois. Bien sûr, vous devez faire attention à ne jamais l'exécuter sur le fil de l'interface graphique, mais parfois, cela représente un compromis intéressant pour le code plus simple.

L'alternative ressemblerait à quelque chose comme ça:

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 */

Je trouve cela beaucoup plus difficile à lire. Certes, le flux est loin d'être toujours linéaire, mais quand c'est le cas, j'aime bien le premier style.

C'est comparable à l'utilisation de ShowMessage () et de Form.ShowDialog () - ils bloquent également jusqu'à ce qu'un "événement" se produise (bien qu'ils exécuteront une boucle de message s'ils sont appelés sur le thread de gui).

35
Rasmus Faber

Ne transmettez pas l'événement, transmettez un délégué qui correspond à la signature du gestionnaire d'événements. Cela me semble vraiment dérisoire, alors soyez conscient des éventuels problèmes de blocage total.

3
Ricardo Villamil

J'ai modifié la classe EventWaiter de Dead.Rabit pour gérer EventHandler<T>. Vous pouvez donc utiliser pour attendre tous les événements de type EventHandler<T>, ce qui signifie que votre délégué ressemble à 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);
    }
}

Et par exemple, je l’utilise pour attendre d’obtenir l’URL de HttpNotificationChannel lors de mon inscription au service de notification Windows Push.

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

Vous pouvez aussi essayer ceci:

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

}

Usage:

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

J'ai rassemblé un échantillon de travail dans LinqPad en utilisant la réflexion, en obtenant une référence à l'objet EventInfo avec une chaîne (soyez prudent lorsque vous perdez la vérification de la compilation). Le problème évident est qu'il n'y a aucune garantie qu'un événement soit jamais déclenché, ou que l'événement que vous attendiez puisse être déclenché avant que la classe EventWaiter ne soit prête à commencer à bloquer. une application de production.

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

Sortie

// main thread started
// threaded work started
// threaded work complete
// work complete event fired
// main thread continues after waiting
0
Dead.Rabit