[Closed] Rollout Scope Issue
Here’s a simplified example… Classic issue of working on 2nd run but not the first…
Basically I need my rolloutA to see the checkbox value on rolloutB, which erros because rolloutB isn’t defined… when it tries to access the value from RolloutA… on 2nd run it will
If I try to launch RolloutC it won’t launch as it’s undefined, on 2 run it will
I’ve had these errors before and i can’t remember the ‘correct’ way to prevent them… and would like to understand more…
try(Destroydialog rolloutB)catch()
Rollout rolloutA "" width:500
(
button btn2 "Is tickbox checked"
on btn2 pressed do
(
print rolloutB.chk_C.checked as string
)
)
Rollout rolloutB "" width:500 height:100
(
button btn1 "Test me first"
checkbox chk_C "check me" checked:true
button btn2 "Then me"
on btn1 pressed do
(
Createdialog rolloutA
)
on btn2 pressed do
(
CreateDialog rolloutC
)
)
Rollout rolloutC "" width:500
(
button btn3 "test2"
on btn3 pressed do
(
print rolloutB.chk_C.checked as string
)
)
Createdialog rolloutB
Ok, here we go:
Your 3 rollouts are currently implicitly global (they are all defined in the top scope).
For rollouts, this is quite OK, but in general I am trying to force myself to use EXPLICIT scopes to make it easier for OTHERS to understand what the scope of my variables is.
When you run the code for the first time, the handler trying to access rolloutB will look in the global scope, find nothing there and implicitly create a local variable with the same name. When you peruse the control with that handler, you will get an error because the variable contains undefined.
When you run the code a second time, all rollouts have created implicitly global variables. When the handler looks for rolloutB, it actually finds an existing global variable with value of “undefined” and sets the property access .chk_C.checked to use this variable’s memory address (pointer) for future access.
Then the actual rollout rolloutB … code comes along and set the value of the SAME global variable to an actual rollout value.
When you press btn2, its handler tries to access rolloutB and finds an actual rollout instead of undefined and everything works great the second time ’round.
So what is the correct solution?
There are two general methods, and they can be combined for even more visual clarity.
First and best approach is to pre-declare all variables in the beginning of the code, with explicit scope. Something like
global rolloutA, rolloutB, rolloutC
This makes sure the script creates the three variables with values undefined in the global scope before either one of them is being accessed inside local scopes like event handlers etc. When the actual handler definitions come along, they will find these memory addresses initialized and waiting to store rollout values and property access calls and everything will be cool.
I would also add a top-level local scope around the whole code to ensure that any accidental “spill” of variable declarations will end up in that local scope instead of the global one. Variable with non-unique in global scope could be very bad for debugging. For example, if you type in a in the Listener, you end up with a global variable named ‘a’ with value undefined in global scope. Later on, some script my use variable ‘a’ assuming it is implicitly local somewhere in its code, but it will be actually found as existing global one and will be used instead. Then another script might come along that makes the same wrong assumption and suddenly you could have two scripts sharing data without you or them knowing it… But this is kind of obscure case and rarely happens.
In many cases, for example when starting a body of a MacroScript, of a Rollout etc, you could have Local variables pre-declared the same way. The variable will be named early on before being used anywhere else in the scope, and then you can be sure it will be visible in any nested sub-scopes without running danger of getting new implicit local variables creating in the lower scopes. See the end of this post…
The second approach is to prefix global variable access attempts with a double-colon ::
This would force your property access attempts to skip any local scope business and head straight for the global scope. If there is no such global variable waiting there, they will actually make it, as if you called ‘global rolloutB’ explicitly, but without you doing so.
In other words, your first rollout could look like
Rollout rolloutA "" width:500
(
button btn2 "Is tickbox checked"
on btn2 pressed do
(
print ::rolloutB.chk_C.checked as string
)
)
Finally, you can mix the two approaches. This means that the global variables will be pre-declared in the very beginning, but when somebody reads your code, he will also see that you ask MAXScript to read straight from the global scope when he sees :: in front of rolloutB and there will be no question in his mind whether that variable is global or not.
So the whole could look like
(--start new local scope
--declare variables in top scope, outside of the current one
global rolloutA, rolloutB, rolloutC
try(Destroydialog rolloutB)catch()
Rollout rolloutA "" width:500
(
button btn2 "Is tickbox checked"
on btn2 pressed do
print ::rolloutB.chk_C.checked as string
)
Rollout rolloutB "" width:500 height:100
(
button btn1 "Test me first"
checkbox chk_C "check me" checked:true
button btn2 "Then me"
on btn1 pressed do Createdialog ::rolloutA
on btn2 pressed do CreateDialog ::rolloutC
)
Rollout rolloutC "" width:500
(
button btn3 "test2"
on btn3 pressed do
print ::rolloutB.chk_C.checked as string
)
Createdialog ::rolloutB
)--end local scope
One thing that is obvious from this example – only the rolloutB needs to be global, because that’s the only rollout you are trying to open and close within your code. The rollouts A and C could remain local to the scope of your script since they are manipulated by the “main” rolloutB, but there are no attempts to clean them up when running the script again.
Here is how it would look like. I used rolloutA1 and rolloutC1 to make sure I am not hitting the existing global variables from the previous runs. rolloutB is still global and accessed as such.
Also note that we can make B close A1 and C1 when it closes to clean up behind us before the local scope is discarded.
(--start new local scope
local rolloutA1, rolloutC1 --declare as local to this scope
global rolloutB --declare as global
try(Destroydialog rolloutB)catch() --try to close if already open
Rollout rolloutA1 "" width:500
(
button btn2 "Is tickbox checked"
on btn2 pressed do
print ::rolloutB.chk_C.checked as string
)
Rollout rolloutB "" width:500 height:100
(
button btn1 "Test me first"
checkbox chk_C "check me" checked:true
button btn2 "Then me"
on btn1 pressed do Createdialog rolloutA1
on btn2 pressed do CreateDialog rolloutC1
on rolloutB close do --try to close the two local "child" dialogs
(
try(destroyDialog rolloutA1)catch()
try(destroyDialog rolloutC1)catch()
)
)
Rollout rolloutC1 "" width:500
(
button btn3 "test2"
on btn3 pressed do
print ::rolloutB.chk_C.checked as string
)
Createdialog ::rolloutB
)--end local scope
As always Bobo a thoroughly conclusive and extremely helpful post! I was unaware of the :: I’ve got around it before declaring my Rollouts as Globals as a ‘hack’(/correct?) but didn’t think this was correct and wondered the reason why it was working on the 2nd run.
Very helpful!
Bobo+=1