Tag Archives: Events

The DIRWATCHER control

With the release of version 10.1 a new control type called DIRWATCHER (“Directory Watcher”) has been added to OpenInsight. This is a fairly simple control which allows you to monitor one or more directories on your system and then receive notifications when the contents are changed.

Using the control is very straightforward:

  • Use the WATCHDIR method to add a directory to monitor for changes.
  • Handle the CHANGED event to receive notifications of directory changes.
  • Use the STOP method to stop monitoring directories.

We’ll take a quick look at each of these methods and events below along with a couple of important properties:

The WATCHDIR method

This method allows you to specify a directory to monitor along with some optional flags. It may be called multiple times to watch more than one directory.

bSuccess = Exec_Method( CtrlEntID, "WATCHDIR", DirName, bSubtree, Flags )
ParameterDescription
DirName(required) Specifies the name of the directory to watch. It should be a fully qualified, non-relative directory path.
bSubtree(optional) Set to TRUE$ to monitor all sub-directories beneath DirName as well. Defaults to FALSE$.
Flags(optional) A bit-mask value containing a set of flags that denote the events to monitor in the directories. It defaults to the following flags:

FILE_NOTIFY_CHANGE_FILE_NAME$
FILE_NOTIFY_CHANGE_LAST_WRITE$
FILE_NOTIFY_CHANGE_CREATION$

The flag values are specified in the MSWIN_FILENOTIFY_EQUATES insert record,

This method returns TRUE$ if successful, or FALSE$ otherwise.

The STOP method

This method stops the control monitoring its specified directories.

bSuccess = Exec_Method( CtrlEntID, "STOP" )

This method returns TRUE$ if successful, or FALSE$ otherwise.

(Note – to resume directory monitoring after the STOP method has been called the WATCHDIR method(s) must be executed again).

The CHANGED event

This event is raised when changes have been detected in the monitored directories.

bForward = CHANGED( NewData )

This event passes a single parameter called NewData which contains an @vm-delimited list of changed items (i.e. notifications). Each item in the list comprises an “action code” and the name and path of the affected file, delimited by an @svm.

Action codes are defined in the MSWIN_FILENOTIFY_EQUATES insert record like so:

   equ FILE_ACTION_ADDED$               to 0x00000001   
   equ FILE_ACTION_REMOVED$             to 0x00000002   
   equ FILE_ACTION_MODIFIED$            to 0x00000003   
   equ FILE_ACTION_RENAMED_OLD_NAME$    to 0x00000004   
   equ FILE_ACTION_RENAMED_NEW_NAME$    to 0x00000005 

Remarks

If a monitored directory experiences a high volume of changes (such as copying or removing thousands of files) it could generate a correspondingly high number of CHANGED events, which in turn could produce an adverse affect on your application and slow it down. In order to deal with this potential issue it is possible to “bundle up” multiple notifications with the NOTIFYTHRESHOLD property into a single CHANGED event so they may be processed more efficiently.

The NOTIFYTHRESHOLD property

The NOTIFYTHRESHOLD property is an integer that specifies the maximum number of notifications that should be bundled before a CHANGED event is raised.

CurrVal = Get_Property( CtrlEntID, "NOTIFYTHRESHOLD" )
PrevVal = Set_Property( CtrlEntID, "NOTIFYTHRESHOLD", NewVal )

By default it is set to 100.

The NOTIFYTIMER property

The NOTIFYTIMER property is an integer that specifies the number of milliseconds before a CHANGED event is raised if the NOTIFYTHRESHOLD property value is not met.

CurrVal = Get_Property( CtrlEntID, "NOTIFYTIMER" )
PrevVal = Set_Property( CtrlEntID, "NOTIFYTIMER", NewVal )

By default it is set to 50 (ms).

Remarks

If the NOTIFYTHRESHOLD property is set to a value greater than 1 then the control will try to bundle that number of notifications together before raising a CHANGED event. However, when this is set to a high value it is possible that the threshold may not be reached in a timely fashion and the CHANGED event not actually raised.

E.g. If the NOTIFYTHRESHOLD is set to 1000, and only 200 notifications are received then the CHANGED event would not be raised.

To prevent this problem the NOTIFYTIMER property may be used to specify the amount of time after receiving the last notification before a CHANGED event is raised even if the NOTIFYTHRESHOLD is not met.

E.g. in the example above, if the control had a NOTIFYTIMER of 50, then a CHANGED event would be raised 50ms after the last notification (200) was received, even though the NOTIFYTHRESHOLD of 1000 has not actually been met.

Developer Notes

The DIRWATCHER control is intended as a “non-visual” control and should probably be hidden at runtime in your own applications. However, it is actually derived from a normal STATIC control so all of the properties and methods that apply to a STATIC apply to the DIRWATCHER as well, and you may use them as normal if you wish.

Yield or Die!

When executing a long running process in a desktop environment, such as selecting records from a large table, it is important for the user to be able to interact with the application even though it is busy. A common example of this is displaying a dialog box that shows the progress of an operation and allowing the user to cancel it if they wish. Failure to provide this ability generally results in user frustration, or causes the dreaded “Window Ghosting” effect where Windows changes a form’s caption to “Not Responding” (this is never a good look, and usually ends in a quick visit to the Task Manager to kill the application).

In order to avoid this problem we have to allow the application to check for user interaction, a process usually referred to as “yielding” (hence the awful title of this post), and this time we’ll take a look at the various options available to accomplish this and the differences between them. Before we go any further however, here’s a little background information on how OpenInsight runs beneath the hood so that you can appreciate how messages and events are handled.

Under the hood

OpenInsight.exe (aka. the “Presentation Server”, or “PS”) has a main thread (the “UI thread”) with a Windows message loop that manages all of the forms and controls, and it also has an internal “event queue” for storing Basic+ events that need to be executed. The PS also creates an instance of the RevEngine virtual machine (“the engine”), which has its own thread (the “engine thread”) with a Windows message loop, and is responsible for executing Basic+ code.

When the PS needs to execute an event it passes it to the engine directly if possible, otherwise it adds the details to the event queue and then posts a message to itself so the queue can be checked and processed when the engine is not busy. When the engine receives the event data it is executed on the engine thread. Stored procedures such as Get_Property, Set_Property, and Exec_Method provide a way for the Basic+ event to communicate back to the PS to interact with the user interface controls and forms during its execution.

The key point to note here is that Basic+ event code runs in a different thread to the UI, so while the engine thread is processing the event, the UI thread is basically waiting for it to finish, and this means that it may or may not get chance to process it’s own message loop. This is where the problems can begin, and why the need for a yielding ability, because:

  1. The engine thread needs to be paused or interrupted in some fashion so that the UI thread can check and process its own Windows message queue for things like mouse, keyboard and paint messages. If this queue is not checked at least every 10 seconds then Windows assumes that the PS is hung and the “Not Responding” captions are shown on the application forms.
  2. While the engine is processing an event, the PS cannot pass it a new one, so it is added to the event queue. If we are waiting to process some Basic+ event code like a button CLICK to cancel the current operation, then we need some way for this to be retrieved and executed before the current event is finished.

So, now we know why “window ghosting” happens we can take a look at the various options to deal with it.

Options for yielding

MSWin_Sleep stored procedure

This is a direct call to the Windows API Sleep function, and it puts the engine thread to sleep for at least the specified number of milliseconds. However, while calling this will allow Windows to schedule another thread to run, there’s no guarantee that this would be the UI thread, so it’s not really a good solution.

WinYield stored procedure

This is a simple wrapper around the Windows API Sleep function, with a sleep-time of 10ms. This suffers from the same disadvantages discussed for MSWin_Sleep above (This function remains for backwards compatibility with early versions of of OI and Windows).

MSWin_SwitchToThread stored procedure

This is a direct call to the Windows API SwitchToThread function which forces Windows to schedule another thread for execution. Like MSWin_Sleep and WinYield there’s no guarantee that this would cause the UI thread to run, so again it’s not a great solution.

SYSTEM PROCESSEVENTS method (a.k.a Yield stored procedure)

This is a new method in version 10.1 that performs two tasks that solve the problem:

  1. It explicitly tells the UI thread to process its message queue (which will avoid the “ghosting” issue), and
  2. It allows the UI thread to process the event queue so waiting events can be executed as well.

One possible drawback here is that waiting events are also processed, and this might not be a desirable outcome depending on what you are doing. In this case there is another method called PROCESSWINMGS that should be used instead.

(FYI – The PROCESSEVENTS method is essentially a wrapper around the venerable Yield() stored procedure, but allows the same functionality to be called via the standard object-based method API rather than as a “flat” function. Yield() itself is still and will be fully supported in the product).

SYSTEM PROCESSWINMSGS method

This is a new method in version 10.1 that tells the PS to process it’s Windows message queue but it does not process any Basic+ events, i.e. it prevents the “ghosting” effect but does not cause events to fire before your application is ready for them.

Conclusion

Version 10.1 has added more functionality to help you avoid the dreaded “Not Responding” message via the PROCESSWINMGS and PROCESSEVENTS methods, and hopefully, armed with the information above, this will help you to write better integrated desktop-applications.

The Case of the Jumping Dialog

We recently noticed a new bug in the IDE where a dialog that hadn’t been updated for quite some time suddenly started misbehaving: it would appear at the wrong coordinates (0,0), and then jump to the proper ones.

At first glance this this looked like a classic case of creating the dialog in a visible state, and then moving it during the CREATE event, but checking the dialog properties in the Form Designer showed that the dialog’s Visible property was “Hidden”, so this wasn’t the source of the problem.

Stepping through the CREATE event in the debugger showed that the dialog was indeed created hidden, but then became visible during an RList() call that performed a SELECT statement, ergo RList() was allowing some queued event code to execute (probably from an internal call to the Yield() procedure) and that was changing the dialog’s VISIBLE property.

Checking the other events revealed that the SIZE event contained the following code:

Call Set_Property( @Window, "REDRAW", FALSE$ )

// Move some controls around

Call Set_Property( @Window, "REDRAW", TRUE$ )

The REDRAW property works by toggling an object’s WS_VISIBLE style bit – when set to FALSE$ the style bit is removed but the object is not redrawn. When set to TRUE$ the object is marked as visible and redrawn.

So, putting all this together: creating the dialog generated a queued SIZE event, which under normal circumstances would run after the CREATE event. However, the Yield() call in RList() executed the SIZE event before the dialog was ready to be shown, and the REDRAW operation made the dialog visible before the CREATE event had moved it to the correct position.

The fix for this was to ensure that the REDRAW property wasn’t set to TRUE$ if it’s original value wasn’t TRUE$, like so:

bRedraw = Set_Property( @Window, "REDRAW", FALSE$ )

// Move some controls around

If bRedraw Then
   Call Set_Property( @Window, "REDRAW", TRUE$ )
End

Conclusion

  • Always protect calls to the REDRAW property by checking its state before you set it, and then only turn it back on again if it was set to TRUE$ originally.
  • Calling Yield() can cause events to run out of the normal sequence, and you should be aware of this when writing your application.

Methods, Events, and Documentation

In a recent post we provided a preview of the OpenInsight IMAGE API documentation for the upcoming release of version 10.1. As that proved quite popular we thought we’d provide some more, this time dealing with the Common GUI API (i.e. the basic interface that virtually every GUI object supports) and the WINDOW object API – two core areas of OI GUI programming.

Methods, not Events

One thing you may notice as you look through these documents is the addition of many new methods, such as SHOWOPTIONS or QBFCLOSESESSION – this is an attempt to tidy up the API into a more logical and coherent format that is a better fit for an object-based interface.

As we went through the product in order to document it, it became very apparent that there were many instances where events were being used to mimic methods, such as sending a WRITE event to save the data in a form, or sending a CLICK event to simulate a button click and so on. In object-based terminology this sort of operation would be performed by a method, which is a directive that performs an action – the event is a notification in response to that action. So, for example, you would call a “write” method to save your data and the system would raise a “write” event so you could deal with it.

Of course, this distinction will probably not bother many developers – just API purists like myself, but this does have another advantage if you like to use Object Notation Syntax (I do) – you can now perform actions such as reading and writing form data by using the”->” notation, whereas before you would have to use the Send_Event stored procedure which essentially breaks the object-based paradigm.

So instead of:

   Call Send_Event( @Window, "WRITE" )

you would use the form’s WRITEROW method instead:

   @@Window->WriteRow( "" )

which is a more natural fit for this style of programming.

(It is also easier to explain to new OI programmers who are used to other object-based languages and environments where everything is properties, methods and events).

Methods, not Stored Procedures

This brings us finally onto the topic of Stored Procedures and the object API, where several of these also fulfill the role of methods. For example, take the venerable Msg stored procedure used to display a message box for a parent form – a different way of treating this would be to have a SHOWMESSAGE method for the parent form rather than using a “raw” Msg call. Likewise for starting a new form: instead of using the raw Start_Window procedure, the SYSTEM and WINDOW objects now support a STARTFORM method instead.

Of course, none of this changes your existing code, nor is it enforced, it’s just something you can use if and when you wish to. However, even if my API pedantry hasn’t persuaded you to change your coding style, some of the new methods are worth investigating as they provide a better opportunity for us to extend the product’s functionality further – take a look at the WINDOW READROW and WRITEROW methods for an example of this – they support new features that we couldn’t do with just sending events.

In any case, here are the links – hopefully some light reading for your weekend!

What’s “this”?

As I’m sure many of you will know, when you’re working with object oriented languages like C++, JavaScript and VB , the compiler provides you with a keyword (e.g. ‘this’ or ‘Me’) that you can use as a reference to the specific instance of an object under which the code is currently executing.  This provides a neat and easy way to access details about the current context when responding to events and methods, and generally improves the clarity of the code.

However, a ‘this/Me’ construct is not something we’ve really had in OpenInsight, because when we write our event handling code the system explicitly passes the name of the current object as the first argument called “CtrlEntID”, so in our event scripts we can use that instead.  Obviously this works well enough, but over the years I’ve found some situations where it would be nice to go a little further:

  • In many of the OpenInsight training courses I’ve run, one of the most common questions I get asked from new students is: “what is the equivalent of ‘this’, or “where is the ‘Me’ keyword”?   Having some sort of “this/me” construct in Basic+ would make learning the system much easier for them, and the name “CtrlEntID” hardly seems slick!
  • With the trend away from using event scripts and moving code into commuter modules the name “CtrlEntID” is no longer enforced – it can be named anything the author of the commuter module wishes, leading to a possible loss of clarity (for example in my own commuter modules I always use the variable name “Object” in place of “CtrlEntID”, but that’s just my convention, and is something subsequent code maintainers must adopt).
  • As code becomes deeper and more nested passing the “CtrlEntID” variable to each subsequent procedure as an argument becomes more of a chore, and I’ve seen global variables used in place of this which can lead to code that is difficult to maintain.

Of course, we do have the “@Window” system variable, which contains the name of the parent WINDOW instance for the currently executing context, so we’re nearly there, but unfortunately that’s not the same as ‘this/Me’ unless you’re responding to a WINDOW event.

So, with the release of version 10.0.8 we’ve now gone the whole way and added a new system variable called:

@Self

When an event is triggered this variable contains the full name of the control instance under which the event code is executing, just like @Window contains the name of the parent WINDOW instance.

E.g:

// Using CtrlEntID
Name = Get_Property( CtrlEntID, "TEXT" )

// Using @Self
Name = Get_Property( @Self, "TEXT" )

We’ve also included two more system variable names as synonyms for @Self as well:

@This
@Me

You may use these in place of @Self if you’re more inclined to use names that are familiar from another language (@Self was chosen because it is already referenced in OpenInsight as a pseudo-variable name when defining QuickEvents).

Hopefully, moving forward, this small addition may help to maintain a cleaner code-base and make teaching new students a little easier.

 

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.

A focus on the FOCUS property

There are two methods for setting the input focus in OpenInsight, and there is a subtle implementation difference that can impact your applications if you’re not careful how you apply them.

The first method is to use an object’s own FOCUS property and set it to TRUE$ like so:

Call Set_Property_Only( @Window : ".EDL_SURNAME", "FOCUS", TRUE$ )

This moves the focus to the specified object, but the event queue will be flushed both before and after the focus has been set, thereby preventing any events raised as a result of the focus being moved from being processed.  This method was originally designed for use with validation routines so the focus could be reset to an invalid control “safely”.

The second method is to use the SYSTEM object’s FOCUS property:

Call Set_Property_Only( "SYSTEM", "FOCUS", @Window : ".EDL_SURNAME" )

This moves the focus, but any events triggered as a result of moving the focus (like LOSTFOCUS and GOTFOCUS events) will be processed.

That all sounds straightforward enough, but using the first method can lead to unexpected results if you are relying on an event already in the queue that you subsequently need – not a common situation but one we encountered recently while converting an old form to v10.  In our case we had a menu failing to show when the focus was on a specific control, and it turned out that the LOSTFOCUS event for the control was setting it’s FOCUS property to TRUE$.  This had the effect of killing a pending MENUDROPDOWN event (new in v10) that created the menu to display, hence no menu.

In this case the solution is to use the SYSTEM BLOCKEVENTS property to turn off events being triggered while the focus is moved and then restore event processing afterwards:

Call Set_Property_Only( "SYSTEM", "BLOCKEVENTS", TRUE$ )
Call Set_Property_Only( "SYSTEM", "FOCUS", @Window : ".EDL_SURNAME" )
Call Set_Property_Only( "SYSTEM", "BLOCKEVENTS", FALSE$ )

This prevented any LOSTFOCUS and GOTFOCUS events from being raised, but the MENUDROPDOWN event was still in the queue.

Using the SYSTEM FOCUS and BLOCKEVENTS properties in this way is a far “safer” alternative when moving the focus because you have full control over how events are handled, and I would always advocate using this method over using a control’s own FOCUS property.

 

Tracking the SAVEWARN property

As veteran OpenInsight programmers know, the system uses a simple boolean flag (exposed as the SAVEWARN property) to determine if the contents of a data-bound form have changed.  This flag can be updated in several ways, the most common being:

  • From the LOSTFOCUS event of a control.
  • From the POSCHANGED, INSERTROW and DELETEROW events of an EDITTABLE control.
  • From setting a control’s DEFPROP property.
  • From the CLOSE event of a form when the control with focus is inspected to see if it has changed.

It is checked during the CLEAR and CLOSE events to see if it has been set and an “Unsaved Changes” warning issued to the user if so.  Most of the time this system works quite well, but (as anyone who has spent several years working with OI systems knows) sometimes it gets triggered when you least expect it, and you’re left with no clue as to why.

To help with this situation the next version of OpenInsight introduces SAVEWARN tracking, so you can see which parts of the system update the SAVEWARN property and when they actually do it. In previous versions the system updated the SAVEWARN flag directly (it’s a simple variable in the “window common area”) but this has been changed to use the Set_Property function so it can be monitored effectively from a single point.

To track SAVEWARN you have two choices:

  • Use the SYSMSG event
  • Use the System Monitor

 

Using the SYSMSG event to track SAVEWARN

Every time SAVEWARN is set a standard SYSMSG event is raised with a SAVEWARNINFO code; the system itself does nothing with this message, but it’s there for you to use if you wish.  This option is probably more suited for run-time tracing as it’s something you could add to your applications easily if you needed to.

The PS_EQUATES insert record defines the SAVEWARNINFO message number that you can intercept:

equ SYSMSG_SAVEWARNINFO$ to 21  ; // Save warn has been changed - null msg

The Auxiliary parameter passed to the  SYSMSG event contains information that describes why the SAVEWARN property was changed.

 

Using the System Monitor to track SAVEWARN

The SetDebugger() function has been updated to support a new method called “SAVEWARN” that enables SAVEWARN tracking so that changes are displayed in the System Monitor.  This option is probably more suited to development use rather than run-time.

From the System Monitor execute:

setdebugger savewarn 1

to turn on tracing, and:

setdebugger savewarn 0

to turn it off.

E.g:

SAVEWARN tracing in the System Monitor

SAVEWARN tracing in the System Monitor

Setting the SAVEWARN property

If you wish to set SAVEWARN yourself you may use the “index” parameter to pass a description for the change, so this can be picked up in any tracing scenario like so:

Call Set_Property_Only( @Window, "SAVEWARN", TRUE$, "From My Stuff" )

This description is then passed in the Auxiliary parameter of the  SYSMSG event as noted above.

 

Hopefully you will find this facility useful if you ever suffer from problems with SAVEWARN in the future.

EditTables – The new “CELL” events

The OpenInsight EditTable has always supported a set of cell-related common events that are fired when a user interacts with the control:

  • CHAR
  • CHANGED
  • CLICK
  • DBLCLK
  • OPTIONS

In order to process these events properly however, it is necessary to know which cell they relate to, and this can ostensibly be found by using the NOTIFYPOS property, which is set to the position of the cell that raised the event.

In theory this approach works well, but in practice it can exhibit problems:  Events in OpenInsight are nearly always raised in an asynchronous fashion, which means that if two of those events where executed in quick succession for different cells, then NOTIFYPOS could be set to the position of the second cell, before the Basic+ event handler could process the event for the first cell, thereby leading to incorrect results.

In order to handle this better the EditTable now supports a series of corresponding “CELL” events:

  • CELLCHAR
  • CELLCHANGED
  • CELLCLICK
  • CELLDBLCLK
  • CELLOPTIONS

The only difference here is that these events pass the indexes of the cell that raised them as arguments to the event handler, thereby preserving their origin accurately.

E.g. The signature for the old CHANGED event looks like this:

Function Changed( CtrlEntID, CtrlClassID, NewData )

Whilst the signature for the CELLCHANGED event looks like this:

Function CellChanged( CtrlEntID, CtrlClassID, ColNum, RowNum, NewData )

Note that when a “CELL” event is defined the EditTable will non longer raise the ordinary event to prevent the notification from being processed twice.

(One useful example of the benefits of having the “CELL” events is that you can now use the new CELLCHANGED event for cell validation, rather than the usual POSCHANGED event, due to the fact that you know precisely where the change originated from. You also know that there actually was a change, rather than having to compare the cell’s current contents to it’s GOTFOCUSVALUE to discover this).