Class EventObserver<T>
- Namespace
- WebDriverBiDi
- Assembly
- WebDriverBiDi.dll
Implementation of an observer in the Observer pattern for events.
public class EventObserver<T> : IDisposable, IAsyncDisposable where T : WebDriverBiDiEventArgs
Type Parameters
TThe type of event arguments containing information about the observable event.
- Inheritance
-
EventObserver<T>
- Implements
- Inherited Members
Remarks
Capture methods (StartCapturingTasks(), StopCapturingTasks(), WaitForCapturedTasksAsync(uint, TimeSpan, CancellationToken), WaitForCapturedTasksCompleteAsync(uint, TimeSpan, CancellationToken), and GetCapturedTasks()) are thread-safe. Only one capture session may be active at a time per observer.
Disposal: Dispose() and DisposeAsync() remove the observer from its event and end any active capture session. Capture methods called afterwards throw ObjectDisposedException, and a WaitForCapturedTasksAsync(uint, TimeSpan, CancellationToken) or WaitForCapturedTasksCompleteAsync(uint, TimeSpan, CancellationToken) call that is still waiting when the observer is disposed completes with ObjectDisposedException rather than waiting for its timeout. Unobserve(), StopCapturingTasks() and repeated disposal remain safe no-ops.
Typical capture flow: Call StartCapturingTasks() before triggering the action that produces events, then use WaitForCapturedTasksAsync(uint, TimeSpan, CancellationToken) to wait for a specific number of handler tasks, or WaitForCapturedTasksCompleteAsync(uint, TimeSpan, CancellationToken) to wait for handler tasks to complete, or GetCapturedTasks() to collect whatever has arrived so far. When WaitForCapturedTasksAsync(uint, TimeSpan, CancellationToken) or WaitForCapturedTasksCompleteAsync(uint, TimeSpan, CancellationToken) collects the full requested batch, the capture session ends automatically; StopCapturingTasks() becomes a no-op and need not be called. Call StopCapturingTasks() explicitly only when ending the session early (e.g., on timeout or cancellation).
EventObserver<NavigationEventArgs> observer = driver.BrowsingContext.OnLoad.AddObserver(
e => Console.WriteLine($"Loaded: {e.Url}"));
observer.StartCapturingTasks();
await driver.BrowsingContext.NavigateAsync(navParams);
Task[] tasks = await observer.WaitForCapturedTasksAsync(1, TimeSpan.FromSeconds(30));
// When tasks.Length == 1, the capture session was automatically ended.
if (tasks.Length == 1) { /* page load event received */ }
Properties
Id
Gets the internal unique identifier of this observer.
public string Id { get; }
Property Value
IsCapturing
Gets a value indicating whether a capture session is active on this observer.
public bool IsCapturing { get; }
Property Value
Methods
Dispose()
Removes this observer from its observable event and releases all resources. Equivalent to calling Unobserve() followed by resource cleanup.
public void Dispose()
DisposeAsync()
Asynchronously removes this observer from its observable event and releases all resources.
public ValueTask DisposeAsync()
Returns
GetCapturedTasks()
Synchronously gets all tasks currently available in the capture buffer and returns them. Does not wait for additional tasks to arrive. Ownership of the returned tasks transfers to the caller.
public Task[] GetCapturedTasks()
Returns
- Task[]
All handler tasks available in the capture buffer at the time of the call, or an empty array if no capture session is active.
Examples
observer.StartCapturingTasks();
await TriggerEventsAsync();
Task[] tasks = observer.GetCapturedTasks();
await Task.WhenAll(tasks);
observer.StopCapturingTasks();
Remarks
This method acquires an internal reader lock that is shared with WaitForCapturedTasksAsync(uint, TimeSpan, CancellationToken) and WaitForCapturedTasksCompleteAsync(uint, TimeSpan, CancellationToken). If a concurrent call to either of those methods holds the lock, this method will block the calling thread until that call releases it. In async contexts — particularly those with a single-threaded SynchronizationContext, such as WPF or legacy ASP.NET — prefer WaitForCapturedTasksAsync(uint, TimeSpan, CancellationToken) to avoid blocking the calling thread.
Exceptions
- ObjectDisposedException
Thrown when the observer has been disposed.
StartCapturingTasks()
Begins an unbounded capture session on this observer. Every handler invocation that occurs while the capture is active produces a Task that can be retrieved via WaitForCapturedTasksAsync(uint, TimeSpan, CancellationToken), WaitForCapturedTasksCompleteAsync(uint, TimeSpan, CancellationToken), or GetCapturedTasks(). The session ends automatically when WaitForCapturedTasksAsync(uint, TimeSpan, CancellationToken) or WaitForCapturedTasksCompleteAsync(uint, TimeSpan, CancellationToken) collects the full requested batch; call StopCapturingTasks() to end the session early.
public void StartCapturingTasks()
Examples
observer.StartCapturingTasks();
await driver.BrowsingContext.NavigateAsync(navParams);
Task[] tasks = await observer.WaitForCapturedTasksAsync(1, TimeSpan.FromSeconds(10));
// When tasks.Length == 1, the capture session was automatically ended.
if (tasks.Length == 1) { await Task.WhenAll(tasks); }
Remarks
This method is thread-safe. Only one capture session may be active at a time.
Ownership of each captured Task transfers to the caller when it is read via WaitForCapturedTasksAsync(uint, TimeSpan, CancellationToken) or GetCapturedTasks(). The caller is responsible for observing the result of each task, including any exceptions, to avoid unobserved task exceptions.
One kind of invocation does not produce a captured task: a failing handler registered with
RunHandlerSynchronously. Notification awaits such
a handler, so its exception propagates out of the notification to the producer instead of
being captured, and a wait for a fixed number of tasks is never satisfied by it. A handler
registered with RunHandlerAsynchronously is
captured whether it later succeeds or fails, provided it returns a task (including a
task that is already faulted). A handler that instead throws synchronously, before
returning a task — for example a non-async delegate that throws while producing its
Task — propagates the exception to the producer before capture can occur, so
like the synchronous-handler case it produces no captured task and never satisfies a
fixed-count wait.
Exceptions
- WebDriverBiDiException
Thrown when a capture session is already active on this observer.
- ObjectDisposedException
Thrown when the observer has been disposed.
StopCapturingTasks()
Ends the active capture session. Any tasks still in the capture buffer can no longer be retrieved via WaitForCapturedTasksAsync(uint, TimeSpan, CancellationToken) or GetCapturedTasks() after this call.
public void StopCapturingTasks()
Remarks
This method is thread-safe and is idempotent: calling it when no capture is active does nothing.
If captured tasks that have not yet been retrieved may fault, call GetCapturedTasks() before calling this method to transfer ownership to the caller. A fault in an unretrieved, still-buffered task is always observed (no UnobservedTaskException is ever raised), but it is not reported through the observer-error pipeline either — it is silently dropped, so retrieving the tasks first is the only way to see such failures.
ToString()
Gets the string representation of this event observer.
public override string ToString()
Returns
- string
The string representation of this event observer.
Unobserve()
Stops observing the event.
public void Unobserve()
WaitForCapturedTasksAsync(uint, TimeSpan, CancellationToken)
Asynchronously waits until the specified number of handler tasks have been captured, then returns them. Ownership of the returned tasks transfers to the caller.
public Task<Task[]> WaitForCapturedTasksAsync(uint count, TimeSpan timeout, CancellationToken cancellationToken = default)
Parameters
countuintThe number of handler tasks to wait for. Must be at least 1.
timeoutTimeSpanHow long to wait before giving up. Must be non-negative and no greater than the maximum timer duration supported by the runtime, or InfiniteTimeSpan to wait indefinitely.
cancellationTokenCancellationTokenA CancellationToken that can be used to cancel the wait.
Returns
- Task<Task[]>
An array of captured handler Task objects. The length of the returned array indicates whether the wait was fulfilled:
-
If the array length equals
count, the wait was fulfilled — all expected handler invocations were captured before the timeout expired. When this is the only wait active on the observer, the capture session is automatically ended and a subsequent StopCapturingTasks() call is a no-op; when another wait is concurrently active, the session is left open for that waiter to continue receiving tasks. -
If the array length is less than
count, the wait timed out — only the tasks that arrived before the timeout are returned. The capture session remains active and subsequent calls will continue collecting tasks.
-
If the array length equals
Examples
observer.StartCapturingTasks();
await TriggerThreeEventsAsync();
Task[] tasks = await observer.WaitForCapturedTasksAsync(3, TimeSpan.FromSeconds(10));
// When tasks.Length == count, the capture session is automatically ended.
if (tasks.Length == 3) { await Task.WhenAll(tasks); }
Exceptions
- ArgumentOutOfRangeException
Thrown when
countis zero, or whentimeoutis negative (other than InfiniteTimeSpan) or exceeds the maximum supported timer duration.- InvalidOperationException
Thrown when no capture session is active.
- ObjectDisposedException
Thrown when the observer has been disposed, or is disposed while this method is waiting.
- OperationCanceledException
Thrown when
cancellationTokenis cancelled.
WaitForCapturedTasksCompleteAsync(uint, TimeSpan, CancellationToken)
Asynchronously waits until the specified number of handler tasks have been captured and all of them have completed execution. This method discards the tasks after completion. If you need to inspect the tasks, use either WaitForCapturedTasksAsync(uint, TimeSpan, CancellationToken) or GetCapturedTasks() instead. Exceptions from captured handler tasks remain owned by the caller and are propagated by this method through the returned task rather than being re-surfaced through transport-level event handler error behavior.
public Task<bool> WaitForCapturedTasksCompleteAsync(uint count, TimeSpan timeout, CancellationToken cancellationToken = default)
Parameters
countuintThe number of handler tasks to wait for. Must be at least 1.
timeoutTimeSpanHow long to wait for the handler tasks to be captured and for the handlers to complete their execution. Must be non-negative and no greater than the maximum timer duration supported by the runtime, or InfiniteTimeSpan to wait indefinitely for both phases.
cancellationTokenCancellationTokenA CancellationToken that can be used to cancel the wait.
Returns
- Task<bool>
true if the expected number of handler tasks were captured and completed before the timeout expired; otherwise, false. When true is returned and no other wait is concurrently active on the observer, the capture session is automatically ended and a subsequent StopCapturingTasks() call is a no-op.
Examples
EventObserver<BeforeRequestSentEventArgs> observer = driver.Network.OnBeforeRequestSent.AddObserver(
async (e) => await ProcessRequestAsync(e),
ObservableEventHandlerOptions.RunHandlerAsynchronously);
observer.StartCapturingTasks();
await driver.BrowsingContext.NavigateAsync(navParams);
bool occurred = await observer.WaitForCapturedTasksCompleteAsync(3, TimeSpan.FromSeconds(10));
// When occurred is true, the capture session is automatically ended.
if (occurred) { /* all 3 events received and handlers completed */ }
Remarks
A false return can arise from two distinct timeout conditions:
-
Fewer than
countevents arrived before the timeout expired (the capture phase timed out). The capture session remains active; subsequent handler invocations continue to be buffered. -
All
countevents arrived but one or more handlers had not finished executing before the remaining timeout budget was exhausted (the completion phase timed out). The capture session is automatically ended in this case, and a handler fault occurring after the timeout is observed and discarded rather than surfacing as UnobservedTaskException.
If you need to distinguish between these two conditions, use WaitForCapturedTasksAsync(uint, TimeSpan, CancellationToken) to obtain the handler tasks and then await WhenAll(params Task[]) with your own timeout handling.
Exceptions
- ArgumentOutOfRangeException
Thrown when
countis zero, or whentimeoutis negative (other than InfiniteTimeSpan) or exceeds the maximum supported timer duration.- InvalidOperationException
Thrown when no capture session is active.
- ObjectDisposedException
Thrown when the observer has been disposed, or is disposed while this method is waiting.
- OperationCanceledException
Thrown when
cancellationTokenis cancelled.