Ответ 1
Не передавать событие, передавать делегат, который соответствует сигнатуре обработчика события. Это на самом деле звучит хитрево для меня, поэтому имейте в виду потенциальные проблемы с блокировкой.
Иногда требуется блокировать мой поток, ожидая появления события.
Обычно я делаю это примерно так:
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).
Не передавать событие, передавать делегат, который соответствует сигнатуре обработчика события. Это на самом деле звучит хитрево для меня, поэтому имейте в виду потенциальные проблемы с блокировкой.
Я модифицировал класс 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));
Я собрал рабочий образец в 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
Вы также можете попробовать следующее:
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();
Я думаю, что они должны работать, не пробовали просто закодированы.
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, и вы обнаружите, что можете ожидать цепочку ожиданий и, следовательно, некоторые странные вещи.