Core Concepts

The Event System

Events, Not Files

GPAL's philosophy is to publish, not persist. Every significant operation, element found, not found, action taken, exception raised, publishes an event. What you do with those events is entirely up to you: write to a file, insert to a database, display in a UI, send an email, or ignore them entirely. GPAL imposes nothing. An event carries a GPALEventType, and everything that receives events subscribes to the types it wants.

Event Types and Who Raises Them

GPALEventType is a flags enum, so a subscription is a set of types rather than a single level. The individual types are INFO, WARNING, ERROR, EXCEPTION, DEBUG, DEEPDEBUG, NOTICE, CAUTION, and FAILURE.

Two named combinations save you spelling out the common cases. GPALALL is what GPAL itself raises: INFO, WARNING, ERROR and EXCEPTION. USERALL is what your workflow raises: NOTICE, CAUTION, FAILURE and EXCEPTION.

The split matters because a workflow wants to see its own story without GPAL's running commentary underneath it.

Publish your own events with GPAL.PublishSimpleEvent, which captures the calling method name for you, so your events arrive with the same context as GPAL's own.

// Handlers of your own, one for each channel

GPAL.WithInformationHandler((sender, e) => eventLog.AppendLine($"[{e.GPALEventType}] [{e.Message}]"))

.WithExceptionHandler((sender, e) => e.ScreenShot?.Save("exception-context.png"));


// Or the built-in ones, told which types they want

GPAL.WithPublishToConsole(GPALEventType.USERALL); // your workflow only

GPAL.WithPublishToDebug(GPALEventType.GPALALL); // what GPAL is doing, in the debugger

GPAL.WithPublishToConsole(GPALEventType.USERALL | GPALEventType.ERROR);


// Publish your own event into the same stream

GPAL.PublishSimpleEvent(GPALEventType.NOTICE, "Login step completed", browser, GPALObjectType.Browser);


// Dynamic values in a message go in brackets, so a message reads the same whatever is in them

GPAL.PublishSimpleEvent(GPALEventType.FAILURE, $"Could not download [{fileName}]");

NOTE

WithPublishToConsole and WithPublishToDebug both default to every type worth reading, so calling them with no argument is the quick way to see everything. Pass a type set once you know which half of the story you are watching. A logger is attached the same way, with GPAL.WithPublishToLogger.

WARNING

An event is published by the thread that raised it, which during a workflow is rarely the one your form is running on. A handler that writes into a control is therefore writing across threads. GPAL's own controls take care of that, and a text control's AppendLine is the one to reach for, because it queues the line and returns. What a handler must not do is wait for the UI thread: a workflow started from a button owns that thread until it finishes, so anything that blocks on it waits for a thread that is waiting for the workflow, and neither moves again.

What Every Event Carries

GPALEventArgs contains: GPALEventType, Message, DateTimeStamp, the current UOW and Selector, GPALObject (the Browser or Application that raised the event), the matched element list, ExceptionRaised on EXCEPTION events, Tokens and TokenList for data-bearing events, and a ScreenShot. The screenshot is the one thing that is not free and it is off until asked for: WithTakeEventScreenshot(true) turns it on, and even then it is taken only on ERROR and EXCEPTION, where seeing what was on screen when something failed is worth what it costs.

// both channels, because an ERROR with no exception is not an exception event

GPAL.WithInformationHandler(OnGPALInfo)

.WithExceptionHandler(OnGPALException)

.WithInformationHandlerEvents(GPALEventType.All & ~GPALEventType.DEBUG & ~GPALEventType.DEEPDEBUG);


// what a handler is for: read the context, decide, and let the workflow know

static bool givenUp = false;


static void OnGPALInfo(object sender, GPAL.GPALEventArgs args)

{

// the selector and the unit of work say what GPAL was doing, not just that it went wrong

if (GPALEventType.WARNING == args.GPALEventType && null != args.Selector)

GPAL.PublishSimpleEvent(GPALEventType.CAUTION,

$"[{args.Selector.Name}] warned while the unit of work held [{args.CurrentUOW?.WithSelectorList?.Count ?? 0}] selector(s)");


// a message the workflow cares about is a place to stop, and the workflow reads the flag

if (true == args.Message.Contains("unauthorized_client"))

givenUp = true;

}


static void OnGPALException(object sender, GPAL.GPALEventArgs args)

{

// the elements are the evidence: what was found, and what it looked like

foreach (GPALElement element in args.WebElements ?? new List<GPALElement>())

GPAL.PublishSimpleEvent(GPALEventType.CAUTION, $"was holding [{element.TagName}] at [{element.Xpath}]");


args.ScreenShot?.Save($"failed-{DateTime.Now:HHmmss}.png");


givenUp = true;

}

NOTE

There are two handlers and an event goes to exactly one of them. It goes to the exception handler when it carries an exception or is typed EXCEPTION, and to the information handler otherwise. The level is not what decides: an ERROR published without an exception is an ordinary message about something that went wrong, and it arrives at the information handler. A workflow that attaches only WithExceptionHandler therefore sees none of its ERRORs, which is the difference between a failure you can read and a program that appears to do nothing. Attach both. Each has its own set of types, WithInformationHandlerEvents and WithExceptionHandlerEvents, and both start as every type. Narrow them when a handler is only interested in part of the story, and remember that narrowing the information handler is what silences DEBUG rather than turning DEBUG off.

WARNING

If nothing is attached, every event is discarded. GPAL will never create log files, write to disk, or output to console on its own. Attach a handler, a console, or a logger before starting a workflow, or the run leaves no trace of itself.

💬 Ask GPAL