Last updated 28/09/2021
This blog describes an event framework that implements domain events.
The example code shows the creation of events TEvent_OnEStopTripped and TEvent_OnEStopReset, handled by class implementing the interfaces IEventHandler_OnEStopTripped and IEventHandler_OnEStopReset. The base classes and interfaces are defined in the library tcl_BaseClasses, and the open-source project for this library can be found at https://github.com/RedRockControls/tcl_BaseClasses
Domain Events
The event mechanism we are discussing is a simple implementation of Domain Events. It allows the consistent synchronous update of the state of a program based on things that have happened, and is based on the publisher/subscriber model.
Events and Handlers
Each event class defines an event of a particular type, and has an associated event handler interface definition. This interface defines the name of the method to be executed when the event is raised.
If a class is to handle a specific event, it must implement the event’s associated event handler interface (i.e it must implement a method with the correct name and signiture).
During an initialisation phase, any objects which must receive a notification of an event are added to the event’s list of subscribers.
When the event is raised in the course of the program’s execution, the event object iterates through its list of subscribers and calls the event handler method of each in turn. This means that the event handling code in the subscribing objects is executed there and then (Note1)
The publishing class knows nothing of the subscribing class – the only thing that links them is the event handler interface. They could each be defined in separate projects, with the event and event handler defined in a third project..
Defining an Event handler
An event handler interface is defined by creating an interface that extends a base interface IEventHandlerBase, and adding a method with the required name and signature. For example, to implement an EStopTripped and an EStopReset event we need to define two event handler interfaces that extend IEventHandlerBase, each with one method to be executed when the event is raised:
In the example project, the events are to be handled here by a class TMachine, so this class must implement the event handler interfaces:
In the example project, the actual event handler methods implemented by TMachine just set or reset a boolean variable:
Defining an Event class
Event classes are defined by creating classes that extend a base class, with an instance variable that can hold a reference to the appropriate event handler. For example:
Each event class needs to override two abstract base class methods.
The IsHanderSupported method is used by the base class’s AddHandler method to check that the object passed to the method implements the required event handler interface for the event. This method is always as shown below – there is no variation between event classes:
The CallHandler method is used by the base class’s Raise method to call the event handler method implemented by the event handler. It casts the interface pointer passed in to the actual event handler interface and calls the event handler method. The EventArgs parameter is used if event data is to be passed to the handler method – this is explained in a later section. This method will be slightly different for each event class – the event handler method name will be that defined in the event handler interface.
Linking Event Handlers to Events
All that remains is to link the objects that must handle the events with the event instances. The simplest arrangement is to have a global list of events and a global list of objects:
Each event holds a list of registered event handlers that are called when the event is raised. The event base class has a method call ClearEventHandlers() that clears the list and a method call AddEventHandler() that adds an event handler to the list.
Note: These methods have a return type IEvent (which defines the ClearEventHandlers and AddEventHandler methods) so that we can chain method calls to improve the readability of the application code. Here adding event handlers to the defined events looks like this:
Raising Events
If the event instances are global, the events can be raised from any module:
The above code will result in the local EnablePower variable in Objects.Machine being set and reset as the state of the EStopHealthy input changes.
The events could also be declared as instance variables in the TSafety module:
The code that links the event handlers can reference the local instances directly (Note2):
Event Data
The event base class has an EventArgs object which can be used to pass event data from the object that is raising the event to the objects that are handling the event. Internally, the EventArgs is implemented as a resizable memory stream.
Event data is written to the EventArgs object using the following methods of the IStreamWriter interface implemented by the EventArg object:
- Add_BOOL()
- Add_BYTE()
- Add_INT()
- Add_UINT()
- Add_DINT()
- Add_UDINT()
- Add_LINT()
- Add_ULINT()
- Add_REAL()
- Add_LREAL()
- Add_STRING()
- Add_PVOID()
Each of these methods returns an IStreamWriter interface pointer, so if multiple arguments are to be added, the method calls can be chained. Arguments can be added in any order provided they are retrieved in the same order in the event handler.
So raising an event with event data might look like this:
The CallHandler method of the event passes an interface pointer that points to the EventArgs object to the event handler method:
The event handler method uses this interface pointer to retreive the event data. Note that if multiple data items are passed in the EventArgs object, they must be retreived in the same order as they were added when the event was raised:
Summary
Events and handlers are easy to create, and the benefits of decoupling events from their effects can be enormous for large projects. Additionally, it opens the possibility of writing library classes that raise custom events that can be handled in client code.
Notes- There is no synchronisation between tasks here. If an object running in Task 1 raises an event on an object running in Task 2, there are no built in checks to prevent inconsistent data resulting from concurrent access. This must be done in the event handler using FB_IecCriticalSection or similar.[↩]
- The local instances of events should not be accessible to the code that adds the event handlers (as they should be private). However, TwinCAT does allow this and it works (but the events do not appear in the intellisense menu unless it has been configured in TwinCAT Options to show all variables). If this offends you, you can add a public property of type IEvent that points to the local event instance, and use this in the code that adds the handlers. This is shown in the example project[↩]