Подтвердить что ты не робот

Блокировка и ожидание события

Иногда требуется блокировать мой поток, ожидая появления события.

Обычно я делаю это примерно так:

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

Однако кажется, что это должно быть то, что я мог бы извлечь для общего класса (или, возможно, даже того, что уже существует в рамках).

Я хотел бы сделать что-то вроде этого:

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

Но я не могу найти способ построить такой класс (я не могу найти хороший правильный способ передать событие в качестве аргумента). Может ли кто-нибудь помочь?

Чтобы привести пример того, почему это может быть полезно, рассмотрите что-то вроде этого:

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

Это гораздо более краткий и читаемый, чем эквивалентный код, написанный с разбросом логики между многими методами. Но для реализации WaitForUsbStickOrCancel(), WaitForUsbStickToBeRemoved и WaitUntilUserPressesDone() (предположим, что мы получаем событие, когда USB-палки вставлены или удалены) Мне нужно каждый раз переопределять "EventWaiter". Конечно, вы должны быть осторожны, чтобы никогда не запускать это в GUI-потоке, но иногда это выгодный компромисс для более простого кода.

Альтернатива будет выглядеть примерно так:

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

Мне кажется, что читать гораздо труднее. По общему признанию, далеко не всегда поток будет настолько линейным, но когда это так, мне нравится первый стиль.

Это сопоставимо с использованием ShowMessage() и Form.ShowDialog() - они также блокируются до тех пор, пока не произойдет какое-либо "событие" (хотя они будут запускать цикл сообщений, если они вызываются в gui-thread).

4b9b3361

Ответ 1

Не передавать событие, передавать делегат, который соответствует сигнатуре обработчика события. Это на самом деле звучит хитрево для меня, поэтому имейте в виду потенциальные проблемы с блокировкой.

Ответ 2

Я модифицировал класс Dead.Rabit EventWaiter для обработки EventHandler<T>. Таким образом, вы можете использовать для ожидания всех типов событий EventHandler<T>, что означает, что ваш делегат является чем-то вроде 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);
    }
}

И, например, я использую это для ожидания получения Url из HttpNotificationChannel при регистрации в службе push push-уведомлений Windows.

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

Ответ 3

Я собрал рабочий образец в LinqPad, используя отражение, получив ссылку на объект EventInfo со строкой (будьте осторожны, когда вы потеряете проверку времени компиляции). Очевидная проблема заключается в том, что нет никакого guarentee, событие будет когда-либо запущено или что событие, ожидаемое вами, может быть запущено до того, как класс EventWaiter готов начать блокировку, поэтому я не уверен, что буду спать, если я поместил это в производственное приложение.

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

Выход

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

Ответ 4

Вы также можете попробовать следующее:

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

}

Использование:

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

Ответ 5

Я думаю, что они должны работать, не пробовали просто закодированы.

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

Не думал слишком много, но не мог понять, как сделать его изолированным от EventArgs.

Взгляните на MSDN ManualResetEvent, и вы обнаружите, что можете ожидать цепочку ожиданий и, следовательно, некоторые странные вещи.