Table of Contents

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

T

The 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

string

IsCapturing

Gets a value indicating whether a capture session is active on this observer.

public bool IsCapturing { get; }

Property Value

bool

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

ValueTask

A ValueTask representing the asynchronous dispose operation.

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

count uint

The number of handler tasks to wait for. Must be at least 1.

timeout TimeSpan

How 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.

cancellationToken CancellationToken

A 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.

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 count is zero, or when timeout is 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 cancellationToken is 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

count uint

The number of handler tasks to wait for. Must be at least 1.

timeout TimeSpan

How 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.

cancellationToken CancellationToken

A 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 count events arrived before the timeout expired (the capture phase timed out). The capture session remains active; subsequent handler invocations continue to be buffered.
  • All count events 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 count is zero, or when timeout is 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 cancellationToken is cancelled.