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:

  1. 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(); 
    		...
  2. 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's bindEvents() method.
    void MyObject::bindEvents() 
    	{
    		FlexSimObject::bindEvents(); // bind the base class events
    		bindEvent(DoStuff); // bind the onDoStuff event
    	}
  3. 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, the FIRE_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:

  1. 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".
  2. 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.
  3. Resolver - a function that uses the requirement values to return the appropriate sub object. Only three kinds of functions are acceptable for this argument:
    treenode resolver1(const Variant& p1);
    	treenode resolver2(const Variant& p1, const Variant& p2);
    	treenode resolver3(const Variant& p1, const Variant& p2, const Variant& p3);
    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.

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.