Bound Events
Description
Event Binding (new in 7.7) provides a standard way for objects to generate and listen to events. These events are called Bound Events, or just events.
Creating an Event Provider
Events are always associated with an object. The event binding mechanism is implemented by the SimpleDataType class, so that any class in the FlexSim hierarchy can use it. Use the following instructions to add events to an object:
- Add a TreeNode* with the event name as a member of the class. The name should always begin with "on". Good event names
include "onEntry" and "onArrival". If you are using VS 2013 or later, use header initialization syntax:
class MyObject : public FlexSimObject { public: TreeNode* onDoStuff = nullptr; void bindEvents() override; void doStuff(); ...
- Override the
bindEvents()
method. For each event node you created, use the bindEvent macro. This macro takes the name of your event, without the preceding "on". If you want to inherit all the parent's events, then you sould call the parent'sbindEvents()
method.void MyObject::bindEvents() { FlexSimObject::bindEvents(); // bind the base class events bindEvent(DoStuff); // bind the onDoStuff event }
- To generate the event use the
FIRE_SDT_EVENT()
macro. This macro takes the node to fire and any data that pertains to the event as arguments. The event will only be fired if something is listening to the event. Otherwise, theFIRE_SDT_EVENT()
macro doesn't do anything.void MyObject::doStuff() { double data; TreeNode* item; // ... do logic that sets the value of data and item FIRE_SDT_EVENT(onDoStuff, item, data); }
Listening to Events
In order to listen to bound events, you must first inherit the FlexSimEvent class, and override the
execute()
method. This creates a listener class:
class OnDoStuffListener : public FlexSimEvent { public: void execute() override { CallPoint* callPoint = getListenerCallPoint(); TreeNode* item = param(1); double data = param(2); // ... define additional behavior for when onDoStuff is fired } };
Notice that by using the getListenerCallPoint()
method, you can get the parameters passed
in to the FIRE_SDT_EVENT()
macro. You must name the resulting pointer callPoint
,
or the
param()
macros will not work.
Next, you will need to assert the event on the object you wish to listen to:
MyObject* myObject = objectNode->objectAs(MyObject); TreeNode* onDoStuffNode = myObject->assertEvent("OnDoStuff");
Notice that the assertEvent()
method returns a
TreeNode*
. You need to add a subnode to this node, with your listener class on it. You
also need to make sure both nodes have switch_activelisteners turned on:
TreeNode* listenerNode = nodeinsertinto(onDoStuffNode); nodeaddcouplingdata(listenerNode, new OnDoStuffListener(), 1); switch_activelisteners(onDoStuffNode, 1); switch_activelisteners(listenerNode, 1);
When the onDoStuff
event is fired, the execute()
method
of the new OnDoStuffListener
will also be executed.
Selecting Events
When you assert an event, and pass in the name of the event, there is a search through all that object's events for the
event with the given name. If you find yourself adding many events at runtime, then consider using the
select
mechanism. Once, probably on reset, do the following:
sdt->enumerateEvents(destNode, true); // this will populate the EventBinding with all the needed data EventBinding* binding = destNode->objectAs(EventBinding); binding->select("OnDoStuff"); // this selects the given event
Then, at runtime, you can do something like the following:
treenode eventNode = sdt->assertEvent(binding);
When you select the event this way, there is no search. You can use the same binding object over and over again to get the desired event node. In fact, you can even use this same binding (with the desired event selected) with a different object, as long as those objects are of the same type:
treenode eventNode1 = sdt1->assertEvent(binding); treenode eventNode2 = sdt2->assertEvent(binding);
Each of the calls in the previous example return the onDoStuff event node for each object.
Enumerating Events
You can query an object for its events by using the enumerateEvents()
method. This method
takes a treenode and a boolean as parameters. The treenode is a destination, and it will be filled with data about all
the events that the object can generate. If the boolean is true, then the data will be stored in an EventBinding object
on the node. Otherwise, the data will be stored in a treenode table in the destination node. Once you have enumerated the
events, you can iterate through them as follows:
// as an event binding node EventBinding* binding = destNode->objectAs(EventBinding); for (EventBindingEntry& entry : binding->events) { ... // as a table (recommended) for (int i = 1; i <= content(destNode); i++) { ...
Each entry contains the name of the event, as well as the event flags. The event flags are a bitmask that include information about the event. This includes information about how many requirements the event has (if the event is relayed):
int flags = get(node("flags", rank(destNode, i))); int numReqs = sdt->getNumRequirementsFromFlags(flags); // numReqs will be 0, 1, 2, or 3
Relayed Events
Sometimes, an object will manage a dynamic number of subobjects. If these subobjects have events, it is possible to access those events through the parent object. In order to do this, you can bind relayed events as follows:
void MyObject::bindEvents() { bindRelayedClassEvents<SubObjectType>("", EVENT_1_REQUIREMENT, &MyObject::resolver); }
The bindRelayedClassEvents<>()
function is a templated function. The template parameter
should be the type of the class being managed by the parent. The sub-object should have its own
bindEvents()
method, which should not bind any additional relayed events.
The arguments to this function are as follows:
- Prefix - a char* with a prefix for the relayed events. If a sub-object has an "OnArrival" event, and the owning object adds the prefix "Sub", then the event will be enumerated by the parent as "OnSubArrival".
- Requirement Flags - a bitmask, specifying how many requirement values are needed to get this event. Usually, there will
only be one requirement value, so this parameter should be
EVENT_1_REQUIREMENT
. - Resolver - a function that uses the requirement values to return the appropriate sub object. Only three kinds of functions
are acceptable for this argument:
Notice that they all return a treenode, and that they take between 1 and 3 Variants. The treenode returned should hold the object with the desired event. The function should use the inputs to get that object.treenode resolver1(const Variant& p1); treenode resolver2(const Variant& p1, const Variant& p2); treenode resolver3(const Variant& p1, const Variant& p2, const Variant& p3);
Binding Statistics as Events
If your object is using the bound statistics mechanism, you can bind each of those statistics as an event, meaning that
you can listen to the change in any of those statistics. In order to do this, call
bindStatisticsAsEvents()
as part of your object's
bindEvents()
method. This will automatically add an On[Statistic]Change event to your object
for each bound statistic.
EventInfo Object
If you add your object to the FlexSim library, you should also add an eventinfo node to the eventfunctions node of your object. Inside this node, there should be an object for each bound event. Each of these objects should have a variables attribute, and within that attribute, it should have two subnodes: params and requirements.
params
The params node is a table that shows the name and type of each parameter passed in to the
FIRE_SDT_EVENT()
macro for that event.
For example, the eventinfo object for the List's OnPush event is shown in the following figure:
Notice that the node
- has object data
- has the same name as the event
- has a variables attribute, and
- has a params node as a variable
Since this event is not a relayed event, there are no requirements. If you right-click the params node and explore it as a table, you will see the table shown in the following figure:
From this table, we see that when the OnPush event fires, we have access to two parameters: the value that was pushed on the list (a Variant), and the partition ID (also a Variant) to which the value was pushed.
requirements
The requirements ndoe is a table that shows what requirements are needed to correctly listen to this event. Most events do not need a requirements node, since they are not relayed events. If an object has relayed events, however, those events need a requirements node.
For example, the List has an OnPartitionContentChange event. This event happens when a partition's content changes. However, to listen to this event, you must supply a partition ID, so that you can listen to the events on that partition. This is reflected in the List's OnPartitionContentChange eventinfo node:
If you explore the requirements node as a table, you will see the following table:
From this table, you can see that you need to supply a partition ID in order to listen to this event. You supply this value
as an argument to assertEvent()
, which takes up to three requirement values. This function
feeds the requirement values to the resolver specified in
bindRelayedClassEvents<>()
, which returns the object that has the named (or selected)
event.