Events
In SiteFusion, events are the driving force. Events signify anything that happens and might require a reaction. There are two kinds of events:
application events and
global events. An application event is fired by a node, and exists only within the application containing the node. A global event however is fired by an application or service and distributed by the SiteFusion daemon to every running application and service process within the same application group. Before getting into global events, we'll first describe the function and use of application events.
Application events
Recall that a node consists of a client part and a server part. Both parts can fire events. When the client part fires an event it is usually the result of a user action, like clicking a button. Events are distinguished by their name. Most events triggered on the client side are
standard XUL events. For instance, many XUL controls fire the 'command' event when they are activated by the user. When such an event is triggered, an event message can be generated and sent to the server part. Notifying the server part of an event fired on the client part is not always necessary or desirable, because it requires network communication. We don't want the client to tell the server about the 'mouseover' event indicating that the mouse is moving over some node in the application, or about keys being pressed in a textbox, unless we intend to do something with that information right away. We could also want to know about something happening, but only as soon as something else happens. When for instance the user has to fill out a dialog with ten textboxes, we could have the client send every keystroke in every textbox, have it send the contents of every textbox as soon as the user moves to the next, or have it send the contents of all ten at once when the user clicks the OK button in the dialog. All of these options can be achieved in SiteFusion, but which one is most desirable depends on the type of functionality we want to provide to the user. In most cases however, sending everything at once when the dialog is accepted is the most efficient and practical option.
The transfer of the contents or state of a control to its server part is done through the 'yield' event. Every node that is able to contain data (like a textbox) or have a state (like a checkbox) has a client-side yield() function. When called, it triggers a yield event carrying the data to be transferred. The resulting event message can then be sent to the server and picked up by the server part of the node, making the data available to the application.
We usually don't want the client to send the yield event of every data-containing node until we need it, but we do want it to send the command event of a button immediately. In order to facilitate these requirements, events always have a message type associated with them. There are three message types, represented by these constants:
- MSG_NONE: the event does not generate any message and thus the server is not notified. This is the default for most events.
- MSG_QUEUE: the event message is appended to the client-side queue waiting to be sent. This is the default for yield events.
- MSG_SEND: the event message is also appended to the queue, but immediately triggers a queue flush sending everything in the queue to the server. This is the default for some events that will almost always require a server reaction, like the 'command' event of a button or the 'initialized' and 'close' events of a window.
When event messages are received by the server, they are processed in the order they were received in, by a first-in first-out principle. That means that messages of type MSG_QUEUE (i.e. the yield event) are always processed before the message of type MSG_SEND that triggered the sending. This has the practical implication that the server part of the nodes receiving the yield events will have processed the accompanying data and made it available to the application before any handlers for the MSG_SEND event are executed.
The following example application class implements the option where the textboxes get yielded when the button is pressed:
<?php
class SFEventExample extends Application
{
public function init( $args ) {
$this->window->addChild(
$this->textboxes = array(
new XULTextBox(),
new XULTextBox()
),
$button = new XULButton( 'Do something' )
);
// Attach the handler to the event, in
// this case $this->doSomething()
$button->setEventHandler( 'command', $this, 'doSomething' );
// Tell the button to call the yield
// function on the textboxes when the
// command event fires:
$button->setEventYielder( 'command', $this->textboxes );
// Alternatively, you could use the
// shorthand method setEvent() that also
// includes the event message type:
// $button->setEvent( 'command', MSG_SEND, $this,
// 'doSomething', $this->textboxes );
}
public function doSomething( $event ) {
$this->window->alert(
"Event fired: " . $event->name .
"\nTextbox 1 contains: " .
$this->textboxes[0]->value() .
"\nTextbox 2 contains: " .
$this->textboxes[1]->value()
);
}
}
This variation implements the option where the textboxes are yielded when the user jumps to the next. (Note that the alert() call has been commented out, because you will end up in an endless loop because the alert prompt will continue to have the textboxes loose focus. Replace the alert for something more useful in a realistic use situation):
<?php
class SFEventExample extends Application
{
public function init( $args ) {
$this->window->addChild(
$this->textboxes = array(
new XULTextBox(),
new XULTextBox()
)
);
// Tell the textboxes to call the yield
// function on themselves when they lose
// focus, let this event flush the queue
// by setting it to MSG_SEND and have
// $this->doSomething() handle it:
foreach ( $this->textboxes as $textbox ) {
$textbox->setEventType( 'blur', MSG_SEND )
->setEventYielder( 'blur', $textbox )
->setEventHandler( 'blur', $this, 'doSomething' );
}
// The shorthand method setEvent() would be
// in this case:
// $textbox->setEvent( 'blur', MSG_SEND, $this,
// 'doSomething', $textbox );
}
public function doSomething( $event ) {
/*
$this->window->alert(
"Event fired: " . $event->name .
"\nTextbox 1 contains: " .
$this->textboxes[0]->value() .
"\nTextbox 2 contains: " .
$this->textboxes[1]->value()
);
*/
}
}
And finally the the option where every keystroke is synchronized with the server:
<?php
class SFEventExample extends Application
{
public function init( $args ) {
$this->window->addChild(
$this->textboxes = array(
new XULTextBox(),
new XULTextBox()
)
);
// Tell the textboxes to call the yield
// function on themselves when their input
// event fires, set it to MSG_SEND to flush
// the queue and have $this->doSomething()
// handle it:
foreach ( $this->textboxes as $textbox ) {
$textbox->setEventType( 'input', MSG_SEND )
->setEventYielder( 'input', $textbox )
->setEventHandler( 'input', $this, 'doSomething' );
}
// The shorthand method setEvent() would be
// in this case:
// $textbox->setEvent( 'input', MSG_SEND, $this,
// 'doSomething', $textbox );
}
public function doSomething( $event ) {
$this->window->alert(
"Event fired: " . $event->name .
"\nTextbox 1 contains: " .
$this->textboxes[0]->value() .
"\nTextbox 2 contains: " .
$this->textboxes[1]->value()
);
}
}
Multiple handlers can be attached to one event. They will be executed in
the order in which they were set with
Node::setEventHandler()
or
Node::setEvent(),
and can be removed with
Node::removeEventHandler(). Event handlers always receive the
Event object as the first parameter. If the event was fired with arguments that carry its data (like the yield event), these arguments are supplied to handlers as additional parameters. An event can be cancelled by a handler by setting the
Event::$cancel property on the event object to TRUE. The event object always contains a reference to the node it originates from in the
Event::$sourceObject property, and the name of the event in the
Event::$name property.
Events can also be triggered without input from the user. There are two
methods for this.
Node::fireClientEvent()
fires an event on the client side which, depending on the associated
message type, may arrive at the server again. An actual user-generated
client event and an event fired with
Node::fireClientEvent()
are in fact indistinguishable to the application. Also, only valid
client events (that is, all
XUL events and
some SiteFusion specific additions) and custom client events created with
Node::createClientEvent() method can be fired.
Node::fireLocalEvent()
fires an event on the server side, which will never reach the client
side. These local events are not bound to the predefined set of event names, and don't have to be created before they can be fired. Also, local events can be fired on unregistered nodes, allowing the use of simple Node objects as triggers for state synchonisation across an application. There are a few local events that nodes fires at times you might want to start some action.
- onBeforeAttach: fires right before the node becomes registered and 'attached' to the node tree.
- onAfterAttach: fires right after the nodes becomes attached.
- onBeforeDetach: fires right before the node is detached from the node tree with the extractChild or removeChild functions.
- onAfterDetach: fires right after the node becomes detached.
These are useful events when writing a class extending the Node class. Certain node methods require that the node is registered. For example the
Node::callMethod() method, which calls a JavaScript method on the client part of the node, can only work if the node already has a client part. In an extending class you may want to automatically do things right after the node becomes registered. The onAfterAttach event is useful for that:
<?php
class SFLocalEventExample extends Application
{
public function init( $args ) {
$this->window->addChild(
$button = new XULButton( 'Get me a textbox' )
);
$button->setEventHandler( 'command', $this, 'newTextbox' );
}
public function newTextbox( $event ) {
$talkingTextbox = new TalkingTextbox( "I'm a textbox" );
$this->window->alert( 'This is before attaching' );
$this->window->addChild( $talkingTextbox );
$this->window->sizeToContent();
}
}
class TalkingTextbox extends XULTextBox
{
public function __construct( $text ) {
$this->sayWhat = $text;
$this->setEventHandler( 'onAfterAttach', $this, 'talk' );
}
public function talk( $event ) {
$this->hostWindow->alert( $this->sayWhat );
}
}
Running this example application you will see the alerts 'This is before attaching' followed by 'I'm a textbox' after pressing the button.
Reflexes
In some cases the required response to a client event is so simple that asking the server about it is really not necessary or even desirable. When for example hovering a box should color it red, it would be pointless to contact the server each time the mouse hovers the box. This is where reflexes come in.
<?php
$box = new XULBox();
$box->width(100)->height(100);
$this->window->addChild( $box );
$box->setEventReflex( 'mouseover',
'this.element.style.backgroundColor = "red";' );
$box->setEventReflex( 'mouseout',
'this.element.style.backgroundColor = "";' );
Reflexes can be set with
Node::setEventReflex() and removed with
Node::removeEventReflex(), and are basically pieces of JavaScript that are sent to the client
beforehand and stored there. When the event fires, the reflex code is
executed in method of the client-side SiteFusion node JavaScript object (which can be referred to as
this). This object contains a property
element which is a reference to the XUL element associated with the node. The executing context offers several local variables:
- eventObject: the JavaScript event object, or undefined if not a XUL event
- eventArguments: array of argument data, can be modified before sending it to the server
- eventName: the name of the event
for example by adding event arguments, you can supply custom clientside data to the event arguments received by the event handlers on the server side. This custom textbox sends its vertical scroll position along with the yield event and restores it when it is detached and reattached:
<?php
class MyTextBox extends XULTextBox
{
public function __construct( $text ) {
$this->value( $text );
$this->multiline( TRUE );
$this->scrollPos = 0;
$this->setEventHandler( 'onAfterAttach', $this, 'scrollPos' );
}
public function scrollPos( $event, $value = NULL,
$scrollPos = NULL ) {
if( $event->name == 'onAfterAttach' ) {
$this->setEventReflex( 'yield',
"eventArguments.push( this.element.scrollTop );"
);
$this->setEventHandler( 'yield', $this, 'scrollPos' );
$this->setProperty( 'element.scrollTop',
$this->scrollPos );
}
else
$this->scrollPos = $scrollPos;
}
}
In this example, the method scrollPos is a handler for both the 'onAfterAttach' and the 'yield' events.
Global events
Apart from in-application events, applications themselves can trigger and react to global events. This kind of event is fired by an application or service, and then distributed by the daemon to all running application and service processes within the same application group. These events can be used to synchonize changes among interrelated processes, for instance to propagate changes in a datastructure so that all processes operating on it are notified of the changes others make.
<?php
class SFGlobalEventExample extends Application
{
public function init( $args ) {
$this->window->addChild(
$this->textbox = new XULTextBox,
$button = new XULButton( 'Send text' )
);
$button->setEvent( 'command', MSG_SEND, $this,
'sendText', $this->textbox );
$this->setGlobalEventHandler( 'text', $this, 'receiveText' );
}
public function sendText( $event ) {
$this->fireGlobalEvent( 'text', $this->textbox->value() );
}
public function receiveText( $event, $text ) {
$this->window->alert( $text );
}
}
Since global events fired by one application are are only sent to the other applications or services, this example application only does something when you have at least two instances started. Applications never receive their own global events back. Global events are fired with the
Application::fireGlobalEvent() method. A handler can be attached to a global event with the
Application::setGlobalEventHandler() method, and removed with the
Application::removeGlobalEventHandler() method.
Global events behave the same as application events. Their name identifies them and they carry an arbitrary amount of arguments containing any kind of data (except resources). Be careful with passing nodes through global event arguments though, as these arguments are serialized before transfer, registered nodes or any object containing references to registered nodes will cause the entire application to be serialized and sent with the event to the other processes. With application events passing registered nodes in event arguments is not a problem because the registry takes care of the translation.
The following example is MiniChat, a tiny chat application that allows anyone starting it to send messages to each other through global events.
<?php
function authorizeLogin( $args, $user, $pass ) {
return TRUE;
}
function getApplication( $args ) {
return 'SFMiniChat';
}
class SFMiniChat extends Application
{
public function init( $args ) {
$this->window->size( 400, 400 );
$this->window->title( 'MiniChat' );
// Basic interface construction
$this->window->addChild(
$this->messagesBox = new XULScrollBox,
new XULHBox( 'center',
$this->textBox = new XULTextBox,
$button = new XULButton( 'Send message' )
)
);
// Make the scrollbox stretch, show a scrollbar and
// distribute its children vertically
$this->messagesBox
->flex(1)
->setStyle( 'overflow', 'auto' )
->orient( 'vertical' );
// Textbox stretches in the width
$this->textBox->flex(1);
// Connect the button to the sendText() handler and
// yield the textbox
$button->setEvent( 'command', MSG_SEND, $this,
'sendText', $this->textBox );
// Add the promptservice and the return key handler
$this->window->addChild(
$this->ps = new PromptService,
new XULKeySet(
new XULKey( 'VK_RETURN', '', $this,
'sendText', $this->textBox )
)
);
// Attach the global event 'message' to the handler
// $this->receiveText()
$this->setGlobalEventHandler( 'message',
$this, 'receiveText' );
// Let the promptservice open a prompt for the user's
// name and have it's result handled by the
// $this->login() method
$this->ps->prompt( 'Your name', "What's your name?",
'', $this, 'login' );
}
// Promptservice handlers always receive the promptservice object
public function login( $ps ) {
if( ! $ps->result ) { // User cancelled
$this->window->close();
return;
}
// Save the name
$this->myName = $ps->value;
// Fire a 'message' global event with the
// text as first argument
$this->fireGlobalEvent( 'message',
$this->myName . ' joined the chat'
);
}
public function sendText( $event ) {
$text = $this->myName . " says: " . $this->textBox->value();
// Clear the textbox
$this->textBox->value('');
// Fire the message global event
$this->fireGlobalEvent( 'message', $text );
// We don't receive our own messages back so add
// them separately:
$this->messagesBox->addChild( new XULDescription($text) );
}
public function receiveText( $event, $text ) {
$this->messagesBox->addChild( new XULDescription($text) );
}
}