Home page Home page Home page Home page
Pixel Header R1 C1 Pixel
Pixel Header R2 C1 Pixel
Pixel Header R3 C1 Pixel
By Sprezz | Saturday, 1 October 2022 21:02 | 0 Comments

One of the intermittent problems we face when developing with OI 10 is knowing about the new properties and features that we now have access to. The pace of development is so rapid that the documentation just doesn't keep up. There's a fine source of documentation at revDevX.com along with multiple blog posts at the same site but sometimes the Lord helps those who help themselves.

With this in mind, we'll use a real world example to illustrate how much easier life can be when we have access to such new features.

We recently wanted to have a radio button contract or expand based upon a control record. We knew how we would have accomplished this in the past - we'd grab the button control object, destroy it, modify it then recreate it. That's all well and good, it works and the control stays at the right place in the tab order - but it can change the Z-Order of the control which isn't always desirable.

Naturally we wondered if there was a better way in 10.

Of course there is, or we wouldn't be making this blog post.

Taking a simple example where we want to alter the available buttons based on the membership type, we'd have one set of radio buttons for the member type and another for the letter type.

An existing member wouldn't need a Welcome Letter so clicking on "Existing Member" has to do this

i.e. the "Welcome Letter" option is removed.

We all like to boast that OpenInsight is written in OpenInsight but the implications of this can be missed in the excitement of the new environment. We're so used to OI not exposing all of a control's properties that we lose track of the possibility that what we need may be staring us right in the face.

The simple fact is, that if something exists in the property panel in the form designer for a control, then it is an exposed property. So let's look at the property panel for a radio button.

and what do we see as the fourth property? Buttons. 

Clicking on the options button displays a dialog containing the text and values for the radio button :-

As we've telegraphed this far in advance you can probably guess what this means - radio buttons expose a BUTTONS property. It consists of two columns, both multivalued. The first is the label description and the second is the associated code.

So now, to accomplish the above goal all we have to do is manipulate this property. Here's a simple code snippet to do this :-

If you're not familiar with the syntax being used for getting and setting properties check out the blog post here.

We'd encourage you to take a look around the property panels to see what other goodies you can find!

By Sprezz | Tuesday, 7 June 2022 14:25 | 0 Comments

 We've been doing a lot of stress testing of LH at Sprezz Towers to help a client with speed issues on large tables. As part of that we've created some 60 million row tables to play with combinations and permutations of differing indexing setups.

In so doing, we came across a situation where the index sort would get to 80% and then just exit with an FS220 error. (FS_REL_NEED_REBUILD_ERR$). Now to be honest, this was confusing to us as there wasn't a relational index on the table. So speaking to the good folks at Rev, it was clarified that this was the wrong error but for fairly obvious reasons we were the first to notice. After discussing further with them we came to the conclusion that it must be related to disk space.

So began an increasingly frustrating attempt to free up disk space (why oh why did I format my USB sticks with FAT32 and its 4GB file size limit?). Subdirectories were zipped, old files deleted until - finally we had the 40GB or so free that we needed. 

We tried again - and again around the 80% mark the index rebuild just stopped. It didn't crash, it just stopped - having cleared out the %ALL.IDS% token in the ! file.

As V119 was suspected to be the culprit, we redirected the temporary sort file in the environment to the same disk as the data tables so that we could keep an eye on what happened. 

At 77% and the sortfile is growing

78% and still going

and then - boom

It seems that the largest sort file that can be created is 2GB and that attempts to exceed this will fail. Note that this applies to both IDX_SETS1 and 2. This is a limitation of a 32 bit OS rather than OpenInsight, so can not easily be addressed in version 9.x.

Now it should be pointed out that this is a particularly extreme situation, as the row ids in this large table are 5 part and quite long. With a more normal key structure than the long complex one we were using, it is unlikely that this limit would be breached without having hundreds of millions of rows in the table... but it does mean that if you need to rebuild such large indexes you need to undertake the task in OI 10.1 where the issue has been resolved. Note also that this doesn't affect day to day use of the indexed table, normal additions and deletions will still update the index - this issue only affects a rebuild.

As a side note - if you DO want to do this in 10.x there is a major caveat. Indexing has been rewritten for 10 and works in a different way than 9. This means that out of the box, OI 9 and OI 10 have different indexing routines and are not compatible. Fortunately Rev have provided a way to deal with this.

All that we need to do is edit or create a record in SYSENV called CFG_RTI_UPDATE_INDEX. Set line 1 to RTI_UPDATE_INDEX_90 then save it, exit OI and restart. This will force 10 to use 9 indexing logic. We've been working a lot with indexes on large tables in 10 and we'd recommend using this setting in any case if you're working with large data volumes. By large tables, we are talking tens of millions of rows.

For the avoidance of doubt - if you are sharing data between OI 9 and OI 10 you MUST do this or you will experience issues.
By apk | Wednesday, 4 August 2021 17:48 | 0 Comments

This will probably be obvious to most, but it's been a real thorn in my side since day one of OpenInsight, and by day one I mean alpha testing and writing bits of OI 2.0 back in my apk@revelation.com days.

I've always had trouble remembering which property is LIST and which property is ARRAY.  Every time I use these, I either get it wrong or just load HELP to check which one is column by row and which one is row by column.  Finally, with a little help from Captain C, I have a working mnemonic.

ARRAY == Across, because the data moves across the table.
LIST == Lines, because the data comes back line by line.

Finally, almost 28 years later, I can stop searching HELP daily.

By Sprezz | 10:14 | 0 Comments

 Over the years, we've had numerous queries as to what exactly clientsetup.exe does and why it has to be run on each and every machine that will be running OpenInsight. With the help of RTI stalwart Bryan Shumsky, we've put together this post to tell you all you need to know about client install.

Over the years, OpenInsight has evolved from a pretty much self contained environment, where you could just pick up an application folder, move it elsewhere and expect it to work, to an environment which requires increasing levels of workstation adaptation. Firstly the OIPI print engine required that an OCX be registered at the workstation before it could be used. Of course, the OCX could still reside on the server, so all that was required for the client was an OCX registration via REGSVR32.EXE. The same went for the AREV32/64 command window. Then the new scintilla editor.

But as more of OI began to be developed in external languages, (specifically .NET) the requirements became more onerous. Windows doesn't take too kindly to being asked to run .NET components over a network and to make it happen, various security overrides need to be put in place. These are beyond the capabilities of a run of the mill installation script, so the decision was taken to install the .NET components locally on the workstation and to register them there. At this point it's important to add that you MUST install these assemblies on the local machine not on a network drive,

For reference, it is worthwhile pointing out that whilst historically the client setup is run from the OINSIGHT subdirectory, it can actually be run from any location as long as the "Assemblies" and "Client Files" subdirectories exist (with contents) relative to the clientsetup.exe.

With Bryan's help we were able to put together this list of what actually happens during a client install. So with no more ado, here's what the program actually gets up to...

First things first. Check if the client setup has already been run at some stage on this workstation - so it's off to the local registry the program goes. This makes it easier for the program to suggest defaults for the new installation.

Next, just to be nice, the program sets up shortcuts to the documentation, website etc etc.

Then, checks if there are already existing .NET assemblies at the requested installation location. If there are, they are removed. This is to stop the installation process interjecting with annoying messages about pre-existence etc.

Penultimately it then copies the files from the aforementioned subdirectories (Assemblies, Client Files) to the specified location on the local machine.

It then finally registers and installs the OCXs and .NET assemblies at their new local location.

Of course, there are circumstances where the client install might not be needed - for example if your installation doesn't need to edit, print, or sort information. Paradoxically this does mean that if you're running the engine server on the network server, you must ensure you do the client install on that machine too!

Hopefully this information will help you in understanding your deployment issues, and a big shout out to Bryan for helping with this.

By Sprezz | Wednesday, 14 April 2021 17:52 | 4 Comments

Like many Revelation consultants, we have a number of clients who continue to use decades old software under new, more modern technologies - specifically AREV32/64. A technology that was meant to provide a transition path to a more modern UI is frequently just used as a way to continue taking advantage of the massive ROI provided by Revelation Software - in this case AREV.

But sometimes, things go wrong. And this week we were bitten by a particularly pernicious bug that we hadn't seen before.

The client reported that suddenly, blank documents were being emailed to their customers rather than the order confirmations that they were expecting. Investigation was relatively easy - isolate the section of the code that creates the files and test this. The section of code in question simply executed a PDISK to redirect printer output to a file, produced the report, then PDISK PRNed to cancel the redirection.

Upon testing, it failed to produce any output, despite reporting that the redirection was successful. Following Occam's Razor we tried to reproduce the issue by issuing a PDISK at TCL, performing a simple LIST report to disk, then doing a PDISK PRN. Surprisingly, no disk file was produced. 

But looking at the subdirectory no file was created.

So we reverted to an older version of the software and tried again - regretfully with the same result. It didn't matter how far back we went, the error persisted. At this point it became obvious that this was not the issue that the client was facing - all had been well until the last upgrade - but it WAS an issue that we were facing in our testing. But our software was a direct copy of their system, so what could it possibly be?

We tried calling SETPTR directly, we tried different filenames and different extensions but to no avail. So it was time to roll out the big guns and deploy PROCMON, the stunningly useful utility from SysInternals available from Process Monitor - Windows Sysinternals | Microsoft Docs. We reproduced the lack of output with PROCMON running and examined the captured output. We filtered the output on process OENGINE.EXE and path containing ZZZZ.txt and saw 6 entries as below :-

The first 4 were as expected, but the last two were strange to say the least. The spaces in our directory path had been replaced with underscores - and strangely, Windows could not find a path with that name.

This provided us with a working theory - so we copied the system to a subdirectory with no spaces in the name and repeated the experiment. This time we saw the results we expected - a file containing output :-

and the PROCMON trace showed no errors

So it would seem that PDISK has been engineered not to work with subdirectories containing spaces in the path. 

By Sprezz | Tuesday, 2 March 2021 12:42 | 0 Comments

 With more and more applications (in Europe at least) moving to UTF8 we got to thinking about the inherent limitations in case conversion for developers in OpenInsight. Historically developers have changed from lower to upper case by simply converting @lower.case to @upper.case in the string to be converted. This is all well and good in an ANSI system but could create issues in a UTF8 system.

The strings @lower.case and @upper.case aren't magic - they are just text strings loaded by the system at startup, and there is no way that they can easily be made to contain all of the characters necessary in a UTF8 system. Naturally we could manually load them with all of the characters that we think might be required but the strong chances are we'd miss something, and in any case it's 2021 - there has to be a better way right?


Of course there is - it's an issue all Windows developers face - not just Rev developers. So naturally Microsoft have made it easy by providing two exported DLL functions for developers to use, namely CharUpperW and CharLowerW. (For an explanation of why we are using the W version rather than the A see this blog entry). These functions do the conversion of the string "in place" - so we simply have to call them passing in the string to be converted and it will be changed in the string (rather than returning a changed value).

The only drawback is that to be able to use it, we have to prototype it first in a DLL definition. Historically this was a messy process, involving creating a DLL Prototype for the functions, saving it and then running a system routine called DECLARE_FCNS against this definition. If you're not familiar with this, where have you been for the last decade or so? ;). Then if we wanted to track the definition for deployment we'd have to create an APPROW definition because the OI Repository didn't know about the existence of DLL prototypes.

Fortunately in OI 10, DLL Prototypes are now part of the OI entity family and as such there are UI tools to accomplish all of the above. So let's see how we'd go about defining the above two functions.

Let's go to File/Entity/New

and select DLL prototype record. The DLL Prototype Designer launches and we can define our functions. Let's start by telling it that the functions we need are in USER32 and define a namespace (a unique string to prepend to the name of the function so that somebody else's (potentially incorrect prototype) definition doesn't overwrite ours). This particular function doesn't return a value  so we're going to use a return type of VOID :-

It's a STDCALL type (like most definitions we make)

and it takes an LPWSTR type

Then define the arguments for both functions

and apply the namespace (to which I've remembered to add an underscore :))

Then save it using Ctrl-S

The system obligingly tells us that is has saved the prototype

and finally we can compile the prototype (running DECLARE_FCNS effectively) by pressing F9

We can check that the system functions as in version 9 by checking for the DLL_ row in SYSPROCS

and sure enough - the only difference in 10 from 9 is that there are now comment lines for the namespace and comments :-

The routines are now available to us to perform UTF8 compliant lower and uppercase conversions. So "convert @lower.case to @upper.case in string" becomes "call zz_CharUpperW( string )" and "convert @upper.case to @lower.case in string" becomes "call zz_CharLowerW (string )". Of course there's nothing to stop you aliasing the functions to more friendly names like "toUpper" or "toLower" but there's always the chance of a clash with existing names. 

By Sprezz | Wednesday, 23 September 2020 17:35 | 2 Comments
Are you sitting comfortably readers? Then buckle yourself in because this is going to be a long ride! 

The Challenge

It all started when Martyn Phillips of Revelation UK approached us for some assistance with toggling button states in his new sample app. Originally he was interested in capturing the various actions that could be used to update the cut/copy/paste buttons in his app and we were able to point him to Carl’s EDITSTATECHANGED blog post, which along with some hints from our own blog on promoting HELP events got him to where he wanted to be.  

Then Martyn asked if we could do something similar for his Save/Clear/Delete buttons. This was a tougher challenge as what we really need is a FORMSTATECHANGED event, not dissimilar to the above, but triggered when the form changes in such a way that the save/clear/delete buttons should be updated. (SPOILER ALERT – it is coming, so treat this blog post as educational not proscriptive!). The problem is, that so many things can change the state of the form. LOSTFOCUS, CLICK, CHANGED, whether it’s a new row, whether it’s an existing row etc, etc. 

So, we concluded that the most viable way would be to promote an event to the level of “all events for all controls” and perform our magic from there.

The Chosen Solution

But this is a sample app, and we’ve already got one promoted event, so we wanted another technique. Musing about how we could possibly know that something had changed the save status of the form, we recalled that the SAVEWARN property is set by the system when the current record is “dirty” and needs to be saved. 

So, we decided to try and intercept the SET_PROPERTY call that updates SAVEWARN. 

Historically we’ve done this using the time-honoured technique of copying $SET_PROPERTY to $RTI_SET_PROPERTY and providing our own shell $SET_PROPERTY. The issue with this, is that updates can overwrite OUR version of $SET_PROPERTY so it’s a maintenance nightmare.

Checking LH logs gave us an idea. The program loader application, RTP27 always seems, when asked to load a program, to look first in the MD file, and if it doesn’t find what it’s looking for, then looks in SYSOBJ. (The reality of the situation is more complex as Revelation’s Bryan Shumsky explained to us when we asked. See below.)

Those of you who are coming to OpenInsight from an AREV or Pick background will know that the MD file (“Master Dictionary”) is simply a collection of pointers telling the system (amongst other things) where to find the program that has just been requested. The traditional format would be as follows:


Type of Pointer
File to find Object in
Name of Object

So, for example the row for SET_PROPERTY would be:


OpenInsight did away with these pointers and just assumed that executable code would be found in SYSOBJ. But the introduction of the CTO made the MD required again.

Given this information, and the fact that we can modify MD records as we wish, it becomes obvious that we can change the SET_PROPERTY entry to point to OUR replacement for SET_PROPERTY, add a new entry called say, RTI_SET_PROPERTY, and point this at the original SET_PROPERTY. When RTP27 comes to load SET_PROPERTY, it will find SET_PROPERTY (which redirects to ZZ_SET_PROPERTY) which will in turn find RTI_SET_PROPERTY (Which redirects to the original SET_PROPERTY).

So, we would have an MD entry called SET_PROPERTY:



And an MD entry called RTI_SET_PROPERTY:



So, we can do whatever processing we want (in this case checking amongst other things the SAVEWARN property) and then call RTI_SET_PROPERTY to actually do the real work. (Well in our case, the other way round – let RTI_SET_PROPERTY do its work, then do the checking.)

For an in-depth discussion of this technique, we recommend checking out SRP’s excellent (and amusingly titled) blog post on this subject at https://blog.srpcs.com/hooking-in-basic-using-different-bait/.

We created our MD entries and tested. Logging out and back in to ensure that our new shelled routines were in memory, we were delighted to see that the concept was working. Our routine was being called! 

The First Hurdle

Regretfully, we fell at the first hurdle. Whenever we tried to call our replacement SET_PROPERTY, the system would crash, complaining that we were being passed too many parameters. This seemed ludicrous, I mean, everyone knows that SET_PROPERTY takes 4 parameters, right?

OR so we think – until we take advantage of the wonderful new System Editor’s ability to open Entity OBJECT rows without blowing chunks. ( and dictionary items - thanks SRP!) - let’s give that a try…

Checking the property panel and the function takes FIVE parameters, not FOUR. As you can see, there’s an undocumented “bSetOnly” parameter. Presumably so that we can call SET_PROPERTY in place of SET_PROPERTY_ONLY and have it behave the same, although he who lives by the undocumented feature also dies by it, so make of that what you will. 

We modified our SET_PROPERTY to take five parameters and it was good.

Except it wasn’t. It worked beautifully on single windows, but the moment that you swapped between MDI children or other dialogs, the buttons failed to reflect the current window until data changed. With hindsight this was a rookie mistake. Switching between windows doesn’t generate a SET_PROPERTY, it generates an ACTIVATED event.

So back to square one. 

The NEXT Chosen Solution

In a Windows based environment, all attempts to set a property will come from an event script context, but events also trigger when a window is activated. So, if we could intercept the running of an event, we could not only do our SAVEWARN property tests but we could react to a change of focus between windows, and remove the issue where doing so failed to update the buttons. The routine responsible for this is RUN_EVENT. 

We did some testing and established that RUN_EVENT is called about twice as often as SET_PROPERTY so as there wasn’t an order of magnitude difference we settled upon performing the same shelling trick with as before but with RUN_EVENT. And thereby we entered a world of pain.

A World of Pain, BFSs and Virtual MD Files

The problem with the MD solution described above, is that, unlike in AREV, in OpenInsight the MD file has been made GLOBAL so any changes made in there will affect ALL users of ALL applications. 

We’re great believers in damage limitation, so we thought it’d be much nicer if this redirection only applied when we wanted it to. 
So, we had a choice – we could create an application specific MD and add our pointer rows to that OR we could take advantage of another little tweak highlighted in SRP’s blog post. You see, they documented an area of labelled common utilised by RTP27 to store the file variable of the MD file. It essentially contains three variables, which for the sake of our program we referenced as follows:

Common /RTP27COMMON/ mdFileHandle@, lastFileHandle@, mdHasBeenOpenedAlready@

We knew that we weren’t actually using the MD file anywhere else in our application other than for this very specific redirection, so every time a new program is loaded, we’re wasting a disk read in trying to read the non-existent pointers for the programs that we’re trying to load. So we reasoned that if we could replace the MD file handle with a pointer to our own program, that recognised that it was being called, not to run an event, but to perform as a Base Filing System (BFS) – we could just return the pointer rows ourself and ignore disk reads for all else, thereby providing a small increase in performance overall. 

In theory this was simple, in practice it took a lot of getting right.

The theory behind all file access in OpenInsight (and AREV) is that it is routed via a file handle. The file handle is made up of simply a multi-valued list of MFSs and BFSs, followed by a value mark then any BFS specific data required by the BFS to do its job. 

So, in the case of a normal OpenInsight file with no indexing this would be for example, 

RTP57²000000000062 MP_GPL\DATA\REV75001.LK

for the same file with indexing it would be 


When an OpenInsight program tries to read using a file handle, the system calls the first program in the file handle (above RTP57 and SI.MFS respectively) passing it the code for the operation required along with the data needed to support the operation. The program then does what it needs to and returns information accordingly. 

In the first instance, our replacement RUN_EVENT code looked at the first parameter being passed, and if it was a number between 1 and 34 it assumed a BFS operation. If the operation was < 3 (so READ or READO) it would check if we were being asked for RUN_EVENT or RTI_RUN_EVENT, and if so return a structure that duplicated the MD entry. In all other cases, this returned a STATUS of FALSE$ indicating that the operation failed. We reasoned that this was safe to do as it would simply tell RTP27 to carry on to read from SYSOBJ.

With our “virtual” MD file in place via calls to our replacement RUN_EVENT, testing could commence once more. And once again it failed. Our new MD pointers were being ignored, because RUN_EVENT was already in memory.

A Little Bit On The Program Loader (RTP27)

To explain the background behind this, when you first want to run a new program, the program loader – RTP27 – is invoked. It looks in the program stack to see if the program is already in memory, and if not if performs the following lookups (information provided by Revelation):

OpenInsight Mode MD->SYSOBJ-VOC

When the object code is found it is loaded into the program array. All subsequent calls to load the program will find the program in the program array and use this object code without checking to see if any pointers, or even object code, have changed. 

As we want to modify the RUN_EVENT object code in the array , we need to replace what is already there, with our own modified code. 

The way in which we do this is EITHER to call RTP27 explicitly telling it to reload our two routines (RTI_RUN_EVENT and RUN_EVENT), or to issue a GARBAGECOLLECT which tells OpenInsight to drop everything in memory it doesn’t need and reload when requested. We tried both methods and settled on using RTP27 because using GARBAGECOLLECT on a start-up proc caused our OpenInsight to die!

Armed with this new knowledge we were able to have our version of RUN_EVENT forward to the system RUN_EVENT and then check SAVEWARN and update the buttons accordingly. 

Baby Steps

So, we added an INSTALL method to our RUN_EVENT replacement. It was a very straightforward piece of code:

                mdFileHandle@<0, 1> = "ZZ_RUN_EVENT*PRACTICE_LOG" 
                call RTP27(“RTI_RUN_EVENT”)
                call RTP27(“RUN_EVENT”)
                retVal = 1

Now that the first value in the filehandle is the name of our RUN_EVENT replacement, when I/O operations are performed against the MD file, our program will be called to process it. 

We tested this by writing a small program that included the RTP27COMMON and tried to read rows from the “Virtual” MD file. It all worked as expected so we moved to the next stage. Testing it in real life.

Invoking the System Monitor we installed our virtual MD table (by RUN ZZ_RUN_EVENT “INSTALL”) and we test ran the MP_MDI_MAIN window. To our delight, our program was invoked and behaved as expected. There were some minor glitches related to the actual business logic (see discussion later) but the proof of concept was a success.

Buoyed by this, we sent Martyn a text file containing the program with instructions to compile it and to modify the CREATE event of the MDI form to call our install routine. We sat back and awaited the praise. 

Which was not forthcoming. 

Almost immediately the application started crashing in weird places in RUN_EVENT, referencing unassigned variables which clearly were assigned. It became apparent that something was going badly wrong. Yet it still continued to work fine on the development system.

We were baffled, so we modified our copy of Martyn’s code to do the INSTALL call. Predictably our system now fell over. 

Thus began an infuriating round of logging, testing tweaking which achieved nothing other than frustration. Call the INSTALL from System Monitor, all good. Call from a CREATE event, abject failure. 

Finally, in a developer wide Teams chat, Carl made the (with hindsight obvious) observation, that if the object code for a program was in the returnStack (i.e. it's in use) then RTP27 should not be asked to reload the code. There would be pointers in memory to where the code is currently executing and modifying the object code would invalidate those pointers. 

When you’re in a CREATE event you are obviously inside RUN_EVENT so it shouldn’t be replaced in the array. When you’re in the System Monitor you’re not inside RUN_EVENT so it can be replaced. 

Knowing that the System Monitor can now be treated as an Object in the system, we tried to programmatically run our INSTALL:

$systemMonitor->run( "ZZ_RUN_EVENT INSTALL" )

but this didn’t have the desired effect. The engine was too busy processing our code to respond to the System Monitor. 

Then Aaron had the bright idea to run the install from the system Start-up Proc where it would not yet be in event context, et voilà – the pieces all fell into place – nearly.

Code Coda

The final twist was that something we were doing caused RTP27 to go into a recursive loop and abend OpenInsight. To cut a long story short, we replaced our virtual BFS as an MFS and logged all actions against the MD file. This led to a rewriting of the virtual BFS to return TRUE$ for three specific values and FALSE$ for all others and things finally worked as we expected them to.

Business Logic

So, with everything in place we now had to set about ensuring that our buttons were toggled as required in respect to the state of the current entry window. The buttons we were interested in were the save, the clear and the delete buttons. The save button was easy – is SAVEWARN set? If so, then enable the save button.

The delete button was slightly more complicated. In AREV we would have just looked at WC_OREC% and if it was not empty, then the record could be deleted. (Stating the obvious you can’t delete a new row). Unfortunately, in this release, there is no such property. (Spoiler alert – Rev have added a new WINDOW property called NEWRECORD to the next version.  Set to TRUE$ on the READ event if it's a new record, and reset to FALSE$ on a CLEAR and WRITE). 

The Window common variable origResultRow@ looked as though it had potential and we naively assumed that it would be of the structure RowID : @Fm : Row – and on simple windows we tested, it was indeed. But on more complex windows, sometimes the row id would appear in the middle of an otherwise blank origResultRow@. 

It transpired that origResultRow@ is ordered the same as controlMap@. Therefore, all we had to do to find out whether this was a new row, was to loop through the origResultRow@ variable, grabbing each value and if at the end we had a structure with a field mark it wasn’t a new row!

Penultimately, we wanted to know when the Clear button could be enabled, and after experimentation we landed on using the rowLocks@ variable. If this contains a value then the system thinks it is displaying something (it is not set until you tab off the key prompt).

The final piece of the jigsaw was acting on a close event – this initially didn’t work because all of our code happened after the system CLOSE event, so the window was no longer there to query. This was easily resolved by moving the GET_PROPERTIES we needed to before the RTI_RUN_EVENT.

With all of these things taken into account, our solution was complete.

The code is presented below as an exercise for the student 😊. 

The Final Program

With a reminder as to why you should always use bLen in preference to Len...

0001  Function zz_Run_Event(appType,controlID,controlClass,event,arg1,...<more>)
0002      * Copyright (C) 2020 Sprezzatura. All Rights Reserved **
0004     Declare Function rti_Run_Event, get_Property, retStack
0005     $Insert logical
0006     Common /RTP27COMMON/ mdFileHandle@, lastFileHandle@, mdOpenedAlready@
0008     if assigned( appType )       else         appType        = ""
0009     if assigned( controlID )     else         controlID      = ""
0010     if assigned( controlClass )  else         controlClass   = ""
0011     if assigned( event )         else         event          = ""
0012     if assigned( arg1 )          else         arg1           = ""
0013     if assigned( arg2 )          else         arg2           = ""
0014     if assigned( arg3 )          else         arg3           = ""
0015     if assigned( arg4 )          else         arg4           = ""
0016     if assigned( arg5 )          else         arg5           = ""
0017     if assigned( arg6 )          else         arg6           = ""
0019     equ ON$    To 1    ;* Used to enable, or turn on buttons.
0020     equ OFF$   To 0    ;* Used to disable, or turn off buttons.
0022     * install request/BFS Code, if not process as RUN_EVENT
0023     retVal = TRUE$
0024     stack = retStack()
0025     if stack<2> = "INITIALIZE" then appType = "INSTALL" ; *Startup Proc
0027     if appType = "INSTALL" then
0028        goSub modifyMD
0029     end else
0030        if num( appType ) and appType # "" then
0031           if appType >= 1 then
0032              if appType <= 34 then
0033                 goSub pretendToBeBFS
0034              end else
0035                 * if the BFS definition is extended to use higher numbers 
0036                 * react appropriately here
0037              end
0038           end else
0039              * if the BFS definition is extended to use lower numbers 
0040              * react appropriately here
0041           end
0042        end else
0043           winID          =         controlID[1, "."]
0044           objxArray2     =         winId
0045           propArray2     =         "SAVEWARN"
0047           objxArray2     := @Rm :  winID
0048           propArray2     := @Rm :  "MDIFRAME"
0050           dataArray2      =        Get_Property( objxArray2, propArray2 )
0052           bSaveWarn      =         dataArray2[ 1, @Rm ]
0053           mdiFrame       =         dataArray2[ bCol2() + 1, @Rm ]
0055           $Insert OIWIN_COMM_INIT
0057           lenOldRow      =         bLen( origResultRow@ )
0058           oldRow         =         origResultRow@
0059           rowLocks       =         rowLocks@
0061           retVal = RTI_Run_Event(appType,controlID,controlClass,...<more>)
0062        end
0063     end
0065     if event = "TIMER" else
0066        if  controlId[1, 3] = "MP_" and controlId[ 3, 5 ] != "_MDI_" then
0067           * only react to Martyn's windows, except for the MDI Frame
0068           if event _eqc "CLOSE" then
0069               * disable all three buttons, the activated 
0070               * on the next window will set accordingly if there
0071              dataArray2 = OFF$ : @Rm : OFF$ : @Rm : OFF$
0072           end else
0073               * Check whether there was a previous row
0074              ptr = 1
0075              stripped = ""
0076              loop
0077                 nextField = oldRow[ ptr, @Fm, TRUE$ ]
0078              while ptr   <= blen( oldRow )
0079                 /*
0080                    Fun "easter egg" for those who read comments. 
0081                    When evaluating the blen (or the len of an ANSI string) 
0082                    the string length is stored in the descripter so it 
0083                    does not have to be calculated each time. It's arguable 
0084                    that this might be faster than assigning to a variable 
0085                    then using that variable
0086                 */
0087                 ptr       = bcol2() + 1
0089                 if len( nextField ) then
0090                    stripped := nextField : @Fm
0091                 end
0092              repeat
0093              stripped[ -1, 1 ] = ""
0095              bExistingRec   =         (index( stripped, @Fm , 1 ) > 0)
0097              dataArray2     =         bSaveWarn ; * save button
0099              if len( rowLocks ) then
0100                 dataArray2 := @Rm :  ON$ ; * Clear button
0101              end else
0102                 dataArray2 := @Rm :  OFF$ ; * Clear button
0103              end
0105              dataArray2      := @Rm :  bExistingRec ; * delete button
0107           end
0109           targetWindow      =         if len(mdiFrame) then mdiFrame else winId
0111           objxArray2        =         targetWindow : ".BTN_SAVE"
0112           propArray2        =         "ENABLED"
0114           objxArray2        := @Rm :  targetWindow : ".BTN_CLEAR"
0115           propArray2        := @Rm :  "ENABLED"
0117           objxArray2        := @Rm :  targetWindow : ".BTN_DELETE"
0118           propArray2        := @Rm :  "ENABLED"
0120           objxArray2        := @Rm :  targetWindow : ".MENU.FILE.SAVE_ROW"
0121           propArray2        := @Rm :  "ENABLED"
0123           objxArray2        := @Rm :  targetWindow : ".MENU.FILE.CLEAR_FORM"
0124           propArray2        := @Rm :  "ENABLED"
0126           objxArray2        := @Rm :  targetWindow : ".MENU.FILE.DELETE_ROW"
0127           propArray2        := @Rm :  "ENABLED"
0129           dataArray2        := @Rm : dataArray2
0131           call set_Property( objxArray2, propArray2, dataArray2 )
0132        end
0133     end
0135  return retVal
0137  modifyMD:
0139     mdFileHandle@<0, 1>     = "ZZ_RUN_EVENT*PRACTICE_LOG"
0140     call                       rtp27("RTI_RUN_EVENT")
0141     call                       rtp27("RUN_EVENT")
0143  return
0145  pretendToBeBFS:
0147     locate appType In "20,21,22" using "," setting gotIt then
0148        arg3 = TRUE$
0149     end else
0150        arg3 = FALSE$
0151        if appType < 3 then
0152            /*
0153                 READ or READO
0154            */
0155           arg2 = "RBASIC"
0156           arg2 := @Fm
0157           arg2 := @Fm
0158           arg2 := "SYSOBJ"
0159           arg2  := @Fm
0161           begin case
0162              case event = "RUN_EVENT"
0163                 arg2 := "ZZ_RUN_EVENT*PRACTICE_LOG"
0164                 arg3 = TRUE$
0165              case event = "RTI_RUN_EVENT"
0166                 arg2 := "RUN_EVENT"
0167                 arg3 = TRUE$
0169           end case
0170        end
0171     end
0173  return
Post settings Labels No matching suggestions Published on 22/09/2020 16:58 Permalink Location Options
Pixel Footer R1 C1 Pixel