Monthly Archives: February 2020

OpenInsight 10 and QuickEvent Processing

OpenInsight 10 introduced a couple of changes to how QuickEvents are processed that you may or may not be aware of, so in this post we’ll take a look at them so you can see how they work.

1) QuickEvent arguments are now passed by reference

In previous versions of OpenInsight, arguments passed to QuickEvents were duplicated and passed as copies, which meant that if you altered them in your Commuter Module you wouldn’t see the changes in the calling routine.  As QuickEvents are normally the last item processed in the event chain this is not something you might actually notice, unless perhaps you were using the Forward_Event() procedure from an Event Script.

Event arguments in version 10 are now passed by reference, which means a calling routine will see any changes you make to them.  This is an important point to bear in mind now that it’s possible to alter the order in which QuickEvents are executed in the event chain (see the “Has Priority” flag section below), so be careful when using those arguments in your Commuter Modules.

2) The new “Has Priority” flag

Under normal circumstances QuickEvents are always the last handlers processed in the event chain which normally looks something like this:

  1. Control-specific Event Script
  2. Promoted Events
  3. “System” Promoted Events
  4. QuickEvent

For most purposes this works fine, but there are occasions when you might need to step in front of the system code to do some pre-processing on events such as READ and WRITE, and unfortunately this usually involves writing an Event Script (or a Promoted Event perhaps) to contain your pre-system code like so:

E.g.  Sample pre-WRITE Event Script

Function WRITE( CtrlEntID, CtrlClassID )

   $Insert Logical
   WriteOK = TRUE$

   // Check if we can save the record - if not then WriteOK will be 
   // Set to FALSE$ which will stop the event chain
   GoSub CheckOKToWrite
 
Return WriteOK

However, this removes one of the main advantages of using Commuter Modules in the first place: i.e. the ability to keep all of your code in one place. To alleviate this you could just place your pre-WRITE code in your Commuter Module and call it directly :

Function WRITE( CtrlEntID, CtrlClassID )

   $Insert Logical

   // (Assume Commuter Module ID is the same name as the form with an 
   // "_EVENTS" suffix)
   CommID  = @window[1,"*"] : "_EVENTS"
   WriteOK = Function( @CommID( CtrlEntID, "PREWRITE" ) )
 
Return WriteOK

But then you’re still having to create the Event Script, so you still have a fragment that you need to track.

With version 10 we added a new flag to the QuickEvent handler called “Has Priority”:

QuickEvent definition showing the "Has Priority" flag set

“Has Priority” QuickEvent

If you set this to True the event chain is changed to this instead:

  1. Control-specific Event Script
  2. QuickEvent
  3. Promoted Events
  4. “System” Promoted Events

Which means that you can place something like a WRITE event handler in the Commuter Module, do your pre-processing, and return TRUE$ to allow the system to continue the chain, or use Forward_Event() if you need to do some post processing as well.

Points to note:

  • If you call Set_EventStatus( TRUE$ ) in your Commuter Module QuickEvent handler the event chain is stopped.
  • If you return FALSE$ (“0”) from your Commuter Module QuickEvent handler (and it has to be “0”, not null) the event chain is stopped.

Hopefully this removes the need for Event Scripts for writing pre-system events and should help to keep your applications a little more streamlined and organized, and preserve the benefits of using Commuter Modules.

What is the ENDDIALOG event?

If you’ve been using OpenInsight 10 one of the new features you may have noticed is that forms now have a new event called ENDDIALOG, and in this post we’ll take a brief look at how to use it.

The downsides of modal Dialog_Box programming

There are two primary issues with the classic Dialog_Box/End_Dialog style of programming:

  1. The system has to wait in a loop until the dialog is dismissed, and
  2. This forms a “stack” where the dialogs can only be closed in the order in which they were opened.

Historically the first issue was a bigger problem due to the fact that it tied up the engine thread and maxed out the CPU core it was running on.  This was resolved by moving the dialog “wait loop” into the Presentation Server in version 8, where it could be managed better and the engine thread would effectively “sleep” without consuming resources (prior to this it was a simple Basic+ For/Next loop – very processor intensive).

The second issue still remained however. While it may initially seem logical that modal dialogs should form a “stack” this actually breaks down when they are used from different top-level windows.

For example, suppose I have two instances of the IDE, and I load a modal dialog from the first instance and then another from the second instance.  I can’t close the first before I close the second as the Dialog_Box calls are stacked “inside” the engine.  This is potentially confusing behavior and likely to become a bigger issue if your development moves away from single MDI frames and multiple-monitor setups become more prevalent.

Dialog_Box programming without stacking

The only real way to solve this stacking problem is to break away from the current model of “synchronous” Dialog_Box programming and adopt an “asynchronous” callback model instead.  With this methodology there is no looping at all so the CPU usage stays low and code is only executed as needed, and because the loop is removed there is no stacking either.

The drawback of this approach, however, is that it complicates programming because your code has to be split up into two different sections; one to execute the dialog, and another to respond to the return value.

E.g. Consider a CLICK event handler using the normal approach:

OnClick:
   ... <prepare dialog args> ...
   RetVal = Dialog_Box( "MYDIALOG", DlgParent, DlgParam )
   ... <process RetVal> ...    
Return

… all nice and simple. To do this in an asynchronous fashion would take something like this:

   OnClick:
      ... <prepare dialog args> ...
      Call Dialog_Box( "MYDIALOG", DlgParent, DlgParam  )
   return
   

   OnSomeCallbackEvent:
      // param1 -> DialogID
      // param2 -> RetVal
      Begin Case
         Case ( Param1 == "MYDIALOG" )
            ... <process param2 (RetVal) > ...  
      End Case
   Return

… which is a little more complex.

This approach can actually be taken in any version of OpenInsight, but there’s no framework in place to enforce it and so it ends up needing even more work as the developer needs to find a way to define and target the callback process (E.g. implementing a custom event, using OMNIEVENT, calling a commuter module directly, and so on).

For version 10 we decided to provide that framework so that your code could be structured in a standardized fashion, resulting in some updates to the Dialog_Box and End_Dialog stored procedures and the addition of a new WINDOW event called ENDDIALOG.

Changes to Dialog_Box

Dialog_Box now accepts a new dynamic array argument called “AsyncParams”:

RetVal = Dialog_Box( DialogID, DlgParent, CreateParam, DlgOptions, AsyncParams )

Where AsyncParams is:

<1> An Async flag, to denote it needs to send it's return value to 
    the parent window's ENDDIALOG event

<2> A "cookie" value, which is a simple string passed to the ENDDIALOG 
    event that can be used to identify the returning dialog

e.g.

   AsyncParams    = TRUE$
   AsyncParams<2> = "wibble"

   RetVal = Dialog_Box( "MYDIALOG", DlgParent, DlgParam, "", AsyncParams )

Changes to End_Dialog

This procedure now checks to see if the dialog is in asynchronous mode, and if so it takes the dialog ID, the return value, and the cookie, and sends them to the parent’s ENDDIALOG event, thereby giving us a properly defined framework to implement the callback process.

(Note that the “parent” is considered to be the parent as specified in the originating Dialog_Box call, not the actual runtime PARENT, as these may not be the same thing!).

The new ENDDIALOG event

This event is called from the End_Dialog procedure and accepts three arguments:

DialogID  - name of the dialog that triggered the callback

ReturnVal - the dialog return value passed to End_Dialog

AsyncID   - the cookie value that was passed to Dialog_Box

Example

For the purposes of this example we assume that we are going to launch a simple form called “MY_DIALOG_BOX” using the Dialog_Box function.  The form contains a single EDITLINE control called “EDL_NAME”, and a button called “BTN_OK”.  When BTN_OK is clicked it will get the text from EDL_NAME and return it to the owner with an End_Dialog call like so:

   // CLICK event script for MY_DIALOG_BOX.BTN_OK

   // Get the data the user entered
   Name = Get_Property( @Window : ".EDL_NAME", "TEXT" )
   
   // Return it to the owner window
   Call End_Dialog( @Window, Name )

To launch MY_DIALOG_BOX in asynchronous fashion, do the following from an event on the owner window:

// Launches MY_DIALOG_BOX as a modal Dialog_Box in asynchronous fashion 
// passing it the contents of a variable called CurrName as the CreateParam.
   
AsyncParams = ""
AsyncParams<1> = TRUE$      ; // Async mode
AsyncParams<2> = "GetName"  ; // Optional “AsyncID” param for the 
                            ; // ENDDIALOG event
   
// This code does not halt here - anything the user selects in the dialog
// will be passed back in the ENDDIALOG event 
DlgID = Dialog_Box( "MY_DIALOG_BOX", @Window, CurrName, "", AsyncParams )

In this mode the calling program will NOT halt at the Dialog_Box call and wait for the user to close it. Instead any data returned from the End_Dialog call on MY_DIALOG_BOX.BTN_OK will be passed as an argument to the calling window’s ENDDIALOG event.

The ENDDIALOG event on the parent form would look something like this:

Function ENDDIALOG( CtrlEntID, CtrlClassID, DialogID, DialogValue, AsyncID )
 
// This is an ENDDIALOG event that will be triggered by the End_Dialog 
// call on MY_DIALOG_BOX.BTN_OK when launched in asynchronous mode,
//
// ENDDIALOG is passed three parameters:
//
//   DialogID 
//   DialogValue
//   AsyncID
   
Begin Case
   Case ( DialogID = "MY_DIALOG_BOX" )
      // This is optional but we can check AsyncID if we wanted to have
      //  more fine grain control over how this event is processed.
      If ( AsyncID == "GetName" ) Then
         Call Do_Something_With_This_Name( NewName )
      End
End Case 

Return 0

More details on using the ENDDIALOG event can be found in the online help for the Dialog_Box and End_Dialog procedures in version 10.