I am looking for a way to create a wizard-like program in pure 4GL. In the past I have created things like this by defining a window, drawing multiple frames on it and showing/hiding them at the right time. This works ok if you have a few frames but if the frames are larger or when you have more than just a few frames, it becomes cumbersome very fast.
Alternative way would be to use includes, but I only use includes for definitions and stopped using includes for real code somewhere at the end of the previous century.
Yet another method would be to use a smartfolder with ADM2, but ADM2 seems a bit - well - old.
Even another way would be to use a tabstrip control, but that feels not good as well.
So what remains? I have been playing a bit with separate windows, setting the attribute 'Suppress Window' in the windows that form the elements of the wizard. This makes sure that the frame of the program you start is created inside the current window. So it behaves a bit the way I want it, but not entirely; program flow is held in the program you start. This can be overcome by starting the elements of the wizard as persistent programs, but then you end up having multiple programs active in the same window and if you close the window, you need to click the close-window cross (upper right of the window itself) as many times as there are elements. Not ideal.
Is there a better way to do this?
Our wizard actually works very good. We have been using it for over 10 years.
We have a generic "wizard"-dialog. That dialog can load a list of .W-files (smart frames) one after the other and scales accordingly. Every "wizard" has an API, that manages temp-tables and sets the order of the frames that ask for data. Validations and the final write-to-database are also performed inside the wizard-specific API.
To an enduser it's exactly the same as a normal wizard in other Windows programs.
After my post I dived in again and explored the option with suppress-window. Actually, in my first attempts I was trying to overcomplicate things so it could be done simpler. And more simpler is more better :)
I have now created a window to serve as the host for the frames that make up the steps of the wizard. In my test I used 3 steps, but it is plain easy to extend it. The window is wizard.w, the individual steps wizardsub1.w to 3.w. In the window, I start the underlying .w files persistently. These are set up to suppress-window = yes and have an output parameter that gets filled with the frame handle. This handle is saved in a temp-table in the window and is used to hide/view/move the frame around.
I added a counter to the temp-table and two extra buttons for 'next' and 'previous' that use this counter to walk through the screens. A simple FOR-EACH will hide or view the steps. To overcome the problem of having to click the close button multiple times, I let the windows accept a handle to the parent procedure (the main window). A close event is then applied to the parent instead of to the child, so if you close the parent window, it closes at once.
It works quite well. Actually, I should have used a dialog instead of a window, but that's a minor thing. Next thing is how to handle updates to the database. Since the steps are individual programs, they need to have a common source for the data if they need to react on each others state. A database will do, but it means that changes by the user cannot be cancelled; if you do something in step 1, it must be recorded in the database otherwise step 2 will not know of it. But it implies that every step you do as a user is immediately recorded in the database. But you might want to undo what you did in the wizard, so I need a better solution. One may be a dataset that is controlled in the window and shared with the children.
If anyone is interested, I can throw it on GitHub
What do you mean you can't undo something just because it was immediately recorded to the database? If you put the whole wizard in one big transaction, you can undo everything that was updated in the database at any time until the transaction ends. That's what UNDO of transactions does.
Until the transaction is undone, the updates are there in the database, and can be found. (Just make sure that you really have assigned values to any indexes that you want use to find hem.)
Yes, but having a transaction open while having UI at the same time doesn't sound right. If I wanted to create a wizard that did something with the order table, it would mean that I needed a lock on the order table, which does not give a good user experience.
Sounds a lot like we set up our wizard (see previous comment).
In order to keep the locking to a minimum, we let every "wizardsub.w"-frame populate a temp-table. Every time a user presses Next, the temp-table is send to a wizard-specific API, using dynamic code.
After pressing Finish (we rename the label of the Next button in the last step), this wizard-specific API start the write-out to the database.
Because our wizard-framework is set up dynamically, we can load in a list of .w's and a API dynamically, making the framework usable to every possible wizard we want to make
Thats more or less along the lines I am experimenting now. The main program holds a temp-table that is sent to the subwindows when they get focus and fetched when they lose it. This works, but is not dynamic. Is it possible for you to share some code?
I would use a data structure (be it a dataset of a temptable) per wizard page. Every page (.w) should have a Save procedure which re-validates if persisting the dataset/tt is still possible. At the push of the finish button a transaction should be started. Then sequentially call all the Save procedures and then either everything is persisted to the database or the transaction is undone. Easy :-)
Which is basically the same as Onno suggested I see now...
I'd use an xml or json in memory to store the data while working through the wizard.