Contents of global variable become undefined

I have a Popup Menu Button Object in a form. I wish to use it to choose which of the available databases to open. Its Formula panel contains this code:

replace(arraystrip(arrayfilter(listfiles("",".pandb"),cr(),{?(import() contains "biglookup","",import())}),cr()),".pandb","")

and the popup list displays the available databases.

The Special Object Properties panel (aka the “magic” panel) looks like this. The variable, mgPrimaryDatabase, has been declared as a global in the .Initialize procedure:


The Procedure panel contains this code:

if arraysearch(info(“files”),mgPrimaryDatabase,1,cr()) = 0
    ; file is not open
 alertyesno "The " + mgPrimaryDatabase + " database is not open.  Do you want me to open it for you?"
        if info("dialogtrigger") = "No" 
            mgPrimaryDatabase = ""
            showwindowvariables mgPrimaryDatabase
            opensecret mgPrimaryDatabase
            setactivedatabase mgPrimaryDatabase
            call .Initialize
            if error nop endif
            Window mgWindowName1

If I choose a database which is not open, this message is displayed:

If I click “Yes”, this message is displayed"

A message info("files") statement shows that the selected file is open, so it seemed possible that the setactivedatabase mgPrimaryDatabase statement was causing the problem. I inserted a message "1" statement before the setactive statement (without changing anything else) and the error message was no longer displayed - everything was fine. A message after the setactive statement had no effect and the error occurred.

Removing the setactive statement also got rid of the error message.

I’ve tried to give all of the information that might be needed to solve this problem. Can anybody tell me why it’s happening?

It sounds like there are probably two things going on, both of them bugs. I think it is trying to set the active database before it is fully opened, or before it is listed as open. Inserting the message delayed things enough for it to learn that the database was open.

I think it is giving you an erroneous error message. I think the variable probably is defined. You could find out for sure by displaying its contents in a message, in a separate procedure, after the error has occurred. The actual error is more likely to be that it doesn’t think the database is opened.

Of course this is 100% pure guesswork.

I’m sure you’re correct Dave - I have had the same thing happen before, where an inserted simple message changes the way a procedure works.

No, I still get the ‘undefined’ message. I had wondered if the variable had perhaps been redefined as a fileglobal but that’s not the case either.

For the moment I have a work-around so I’ll stay with that until Jim checks it out.

Message for Jim: As noted above, this is not the only instance of needing to delay a procedure to give a statement time to finish its task.

This is probably the problem. If the form is opened when the database opens, this variable will be automatically created as a fileglobal variable when the form opens. This happens before the .Initialize procedure runs. Then when the initialize procedure runs, it creates a global variable with the same name as the fileglobal variable. So the global variable is “hidden” by fileglobal message. The global variable cannot be read or written to as long as the original database is the active database. As soon as the setactivedatabase statement is used, the global variable is now accessible – but it doesn’t have any value! So the error message is exactly correct.

The solution I would recommend is to not use a global variable at all. Get rid of the global statement in the .Initialize procedure (and you need to quit and reopen Panorama so that it is really gone). Then at the start of the procedure triggered by the popup button, put in code like this:

local chosenDatabase
chosenDatabase = mgPrimaryDatabase

Then in the rest of the code, all references to mgPrimaryDatabase should be changed to chosenDatabase.

By the way, I would recommend this solution in Panorama 6 also. In general, if there is any way to avoid a global variable, global variables should be avoided. You absolutely never should have a form element that is linked to a global variable, because unless you very carefully control the circumstances of how the form opens, you’ll run into this exact problem. If you can make sure that your code that creates the global variable runs before the form opens, then it can work, but if the database is saved with the form window open, trying to define the variable as global in the .Initialize code is guaranteed not to work – at that point it is too late, the variable has already been defined as a fileglobal.

OK - this is very useful information. I have always made the simplistic assumption that a .Initialize procedure runs before anything else happens and I may not be alone in that.

I can confirm that you are not alone.

Panorama opens any saved windows first, and then the .Initialize procedure runs. If you think about it, it has to be that way, because your .Initialize procedure commonly interact with the open windows or items in the open windows. So Panorama 6 (and all earlier versions) did the same thing.

A few months ago I added a second procedure called .PreInitialize that is supposed to run before the windows open. The idea was that It could be used for initializing variables before any windows opened. This procedure would not be able to access windows, open windows, etc. I mentioned this in the release notes, but it turns out it doesn’t quite work correctly, so I haven’t documented it further. For now this is on the back burner, but eventually I’ll get it to work. When it does, you could use it to create your global variables. However, personally I would not do this even if I could. There is no reason to use a global variable in this situation, and global variables should be avoided whenever possible due to the possibility of a global variable created by one database interfering with a global variable (or field) in another database, even after the original database is closed. Avoiding global variables is like washing your hands regularly … good programming hygiene.

I frequently use global variables because many of my procedures access other databases and I need universal access to the variable. However, noting your strong cautions, from now on I’ll use the undefine statement to get rid of them when the job is done.

If the variable is only used within the procedure, you can use a local variable instead of a global. Local variables are not tied to a database, they are tied to the procedure. So you can use a local variable to transfer a value from one database to another.

local x
setactivedatabase "some other database"

Here is your original procedure rewritten to use a local instead of a global.

local chosenDatabase
chosenDatabase = mgPrimaryDatabase
if arraysearch(info(“files”), chosenDatabase,1,cr()) = 0
    ; file is not open
    alertyesno "The " + chosenDatabase + " database is not open.  Do you want me to open it for you?"
    if info("dialogtrigger") = "No" 
        chosenDatabase = ""
        showwindowvariables chosenDatabase
        opensecret chosenDatabase
        setactivedatabase chosenDatabase
        call .Initialize
        if error nop endif
        Window mgWindowName1

There’s no need to explicitly undefine anything – local variables are automatically undefined at the end of the procedure.

In a variant on this procedure code, I need to open the mgPrimaryDatabase normally, not secretly, and that runs into other problems of variable types. I need to spend some more time on this.

As a side issue, whilst the ability of the Procedure panel to hold and execute lots of code is attractive on the surface, it can be messy to access frequently, especially if, as I do, you want to group a truck-load of objects in a form to avoid accidental damage. Because of this, I am moving towards having in the Procedure panel a simple call Michaels procedure statement and I can then fiddle with the content much more easily in a procedure than in the depths of the form. It also make the inner workings more open. It mimics the Panorama 6 approach to a large extent.

I certainly understood that.

Nothing wrong with that. I do that all the time, for the reasons you have enumerated. The nice thing is that now there is a choice. For short programs, up to a half dozen lines long, it’s usually easier to just put the code in the object itself. For longer programs, I often call out to a named procedure. And there’s nothing to prevent you from switching from one to the other in mid-stream, as you’re working on a project.

Don’t forget that now you also have the option of adding parameters to the call statement, so you can often have multiple objects that call a single procedure with different options. It also means that you are not restricted to using info(“trigger”) and the object name to determine what path to take.

call "My Button Code","button option 1","button option 2"

Also, don’t forget about the new callwithin statement, which means you don’t have to call into the top of the procedure. Essentially this means that you can now stuff multiple subroutines into one named procedure. And you can still have 1, 2, 3 or more additional parameters.

callwithin "My Button Code","some label","button option 1","button option 2"

All of these techniques can greatly reduce the number of items you need in your View menu. In Panorama 6 and earlier, the View menu would often be cluttered with dozens of one and two line procedures. Now the View menu can be a lot shorter.

Yep, I had already planned on doing all of the above since I discovered the callwithin statement.