Event Driven Programming, Notifications

From MorphOS Library

Grzegorz Kraszewski

This article in other languages: Polish

Event Driven Programming

Event driven programming is the natural consequence of the invention and development of graphical user interfaces. Most traditional, command line programs work like a pipe: data are loaded, processed and saved. There is no, or limited user interaction (like adjusting processing parameters, or choosing an alternative path). A GUI changes all that. A GUI based program initializes itself, opens a window with some icons and gadgets, then waits for user actions. Fragments of the program are executed in response to user input, after an action is finished, the program goes back to waiting. This way the program flow is determined not by the code, but rather by input events sent to the program by the user via the operating system. This is the basic paradigm of event driven programming.

Mzone eventdriven 1.png

Fig. 1. Execution flow of an event driven program.

Notifications in MUI

There are two approaches to input event decoding in an event driven program: centralized and decentralized. Centralized decoding is programmed as a big conditional statement (a switch statement usually, or a long cascade of if statements) inside the main loop of the fig. 1. flowchart. Depending on the decoded event, subroutines performing requested actions are called. Decentralized input event decoding is a more modern idea. In this case, the GUI toolkit receives and handles incoming input events internally and maps them to attribute changes of GUI objects (for example, clicking with the mouse on a button changes its attribute to "pressed"). Then an application programmer can assign actions to attribute changes of chosen objects. This is done by creating notifications on objects.

MUI uses decentralized input event decoding. All input events are mapped to attribute changes of different objects. These are visible GUI gadgets (controls) usually, but some events may be mapped to attributes of a window object, or an application object (the last one has no visible representation). After creating the complete object tree, but before entering the main loop, the program sets up notifications, assigning actions to attribute changes. Notifications can also be created and deleted dynamically at any time.

A notification connects two objects together. The source object triggers the action after one of its attributes changes. The assigned action (method) is then performed on the target object. The notification is set up by calling the MUIM_Notify() method on the source object. Arguments of the method can be divided into the source part and the target part. The general form of the MUIM_Notify() call is shown below:

DoMethod(source, MUIM_Notify, attribute, value, target, param_count, action, /* parameters */);

The first four arguments form the source part, the rest is the target part. The complete call can be "translated" to a human language in the following way:

When the source object changes its attribute to the value,
perform action method on the target object with parameters.

There is one argument not explained with the above sentence, namely param_count. This is just the number of parameters following this argument. The minimum number of parameters is 1 (the action method identifier), there is no upper limit other than using common sense.

A notification is triggered when an attribute is set to a specified value. It is often useful to have a notification on any attribute change. A special value MUIV_EveryTime should be used as a triggering value in this case.

The target action of a notification can be any method. There are a few methods designed specifically to be used in notifications:

MUIM_Set() is another method for setting an attribute. It is used when a notification has to set an attribute on the target object. OM_SET() is not suitable for using in notifications because it takes a taglist containing attributes to be set. This taglist cannot be built from arguments and must be defined separately. MUIM_Set() sets a single attribute to a specified value. They are passed to the method directly as two separate arguments. The example below opens a window when a button is pressed:

DoMethod(button, MUIM_Notify, MUIA_Pressed, FALSE, window, 3, MUIM_Set, MUIA_Window_Open, TRUE);

Those not familiar with MUI may ask why the triggering value is set as FALSE. It is related to the default behaviour of button gadgets. The gadget triggers when the left mouse button is released, so at the end of a click. The MUIA_Pressed attribute is set to TRUE when the mouse button is pushed down, and set to FALSE when the mouse button is released. Now it should be obvious why the notification is set to trigger at a MUIA_Pressed change to FALSE.

MUIM_NoNotifySet() works the same as MUIM_Set() with one important exception. It does not trigger any notifications set on the target object when the attribute has changed. It is often used to avoid notification loops, not only in notification, but also standalone in the code.

MUIM_MultiSet() allows for setting the same attribute to the same value for multiple objects. Objects are specified as this method's arguments and the final argument should be NULL. Here is an example, disabling three buttons after a checkmark is deselected:

DoMethod(checkmark, MUIM_Notify, MUIA_Selected, FALSE, application, 7, MUIM_MultiSet,
 MUIA_Disabled, TRUE, button1, button2, button3, NULL);

What is interesting is that while the target notification object is completely irrelevant here, it must still be a valid object pointer. The application object is usually used for this purpose, or the notification source object.

MUIM_CallHook() calls an external callback function called a hook. It is often abused by programmers being reluctant to perform subclassing of standard classes, instead implementing program functionality as new methods. Calling a method from a notification is usually faster and easier (however, a hook needs some additional structures to be defined).

MUIM_Application_ReturnID() returns a 32-bit integer number to the main loop of a MUI program. With this method MUI's decentralized handling of input events can be turned into a centralized one. MUI programming beginners tend to abuse this method and redirect all the event handling back to the main loop, putting a big switch statement there. While rather simple, this programming technique should be avoided in favour of implementing program functionality in methods. Adding code inside the main loop degrades the GUI responsiveness. The only legitimate use of MUIM_Application_ReturnID() is to return a special value MUIV_Application_ReturnID_Quit used for ending the program.

Reusing Triggering Value

When the action of a notification is to set an attribute in the target object, it is often desired to forward the triggering value to the target object. It is very easy, when the notification is set to occur on a particular value. Things change however, when the notification is set to occur on any value with MUIV_EveryTime. A special value MUIV_TriggerValue may be used for this. It is replaced with the actual value of the triggering attribute at every trigger. Another special value, MUIV_NotTriggerValue is used for boolean attributes and is replaced by a logical negation of the current value of the triggering attribute.

The first example uses MUIV_Trigger_Value, to display the value of a slider in a string gadget:

DoMethod(slider, MUIM_Notify, MUIA_Numeric_Value, MUIV_EveryTime, string, 3,
 MUIM_Set, MUIA_String_Integer, MUIV_TriggerValue);

The second example connects a checkmark with a button. When the checkmark is selected, the button is enabled. Deselecting the checkmark disables the button:

DoMethod(checkmark, MUIM_Notify, MUIA_Selected, MUIV_EveryTime, button, 3,
 MUIM_Set, MUIA_Disabled, MUIV_NotTriggerValue);

MUIV_EveryTime, MUIV_TriggerValue and MUIV_NotTriggerValue are defined as particular values in the 32-bit range. Because of this, it is impossible to set a notification on the value 1 233 727 793 (which is MUIV_EveryTime). It is also impossible to set the value to a fixed number 1 233 727 793 (MUIV_TriggerValue) or 1 233 727 795 (MUIV_NotTriggerValue) using MUIM_Set() in a notification.

Notification Loops

There may be hundreds of notifications defined in a complex program. Changing an attribute may trigger a notification cascade. It is possible that the cascade contains loops. The simplest example of a notification loop is a pair of objects having notifications on each other. Let's assume there are two sliders which should be coupled together. It means moving one slider should move the other one as well. A set of two notifications can ensure this behaviour.

DoMethod(slider1, MUIM_Notify, MUIA_Numeric_Value, MUIV_EveryTime, slider2, 3,
 MUIM_Set, MUIA_Numeric_Value, MUIV_Trigger_Value); 
DoMethod(slider2, MUIM_Notify, MUIA_Numeric_Value, MUIV_EveryTime, slider1, 3,
 MUIM_Set, MUIA_Numeric_Value, MUIV_Trigger_Value); 

When the user moves slider1 to value 13, the first notification triggers and sets slider2 value to 13. This triggers the second notification. Its action is to set slider1 value to 13, which in turn triggers the first notification again. Then the loop triggers itself endlessly... Or rather it would, if MUI had no anti-looping measures. The solution is very simple: if an attribute is set for an object to the same value as the current one, any notifications on this attribute in this object are ignored. In our example the loop will be broken after the second notification sets the value of slider1.

The Ideal MUI Main Loop

The ideal main loop of a MUI program should contain almost no code inside. All actions should be handled with notifications. Here is the code of the loop:

ULONG signals = 0;

while (DoMethod(application, MUIM_Application_NewInput, (ULONG)&signals)
 != (ULONG)MUIV_Application_ReturnID_Quit)
  signals = Wait(signals | SIGBREAKF_CTRL_C);
  if (signals & SIGBREAKF_CTRL_C) break;

The variable signals contains a bit-mask of input event signals sent to the process by the operating system. Its initial value 0 just means "no signals". When the MUIM_Application_NewInput() method is performed on the application object, MUI sets signal bits for input events it expects in the signals variable. These signals are usually signals of application windows' message ports, where Intuition sends input events. Then the application calls the exec.library function Wait(). Inside this function, the execution of the process is stopped. The process scheduler will not give any processor time to the process until one of the signals in the mask arrives. Other than input event signals set by MUI, only the system CTRL-C signal is added to the mask. Every well written MorphOS application should be breakable by sending this signal. It can be sent via the console by pressing the CTRL + C keys, or from tools like the TaskManager. When one of the signals in the mask arrives at the process, the program returns from Wait(). If the CTRL-C signal is detected, the main loop ends. If not, MUI decodes the received input events based on its received signal mask, translates events to changes of attributes of relevant objects and performs the triggered notifications. All this happens inside the MUIM_Application_NewInput() method. Finally the signal mask is updated. If any notification calls the MUIM_Application_ReturnID() method, the identifier passed is returned as the result of MUIM_Application_NewInput(). In the event of receiving the special value MUIV_Application_ReturnID_Quit the loop ends.

Any additional code inserted into the loop will introduce delay in GUI handling and redrawing. If a program does some processor intensive calculations, the best way to deal with them is to delegate them into a subprocess. Loading the main process with computational tasks may result in the program being perceived as slow and unresponsive to user actions.