Disclaimer: In this question tab, page, an dialog actually mean the same thing, sorry. My excuse: I am not sure what the final product should look like - a bunch of separate windows or all in one.
I am looking to improve an existing, hard-to-maintain Wizard baked with WinForms. I need to try to keep the look and feel about the same, but I need to clean up the internal logic. There are 5 dialogs in total, all of which get displayed one after another (after the Next button is clicked of course) inside one giant method. The way to jump back and forth is with ... 5 or 6 labels and GOTOs!
Now, this Wizard is linear, not a tree. From any one dialog/page you should be able to go to at most two others. Somehow doubly0linked list comes to mind. Right now there are 5 * 4 = 20
potential state transitions, while only 2*1 + 3*2 = 8
of them are valid. I do not have to use goto
s. They are usually evil, and in this case they are - it is hard to maintain this already... and I am thinking of adding another, 6th page. The reason why goto
s are in there are most likely because A) time pressure when v. 1.0 was being made, B) It was 5 years ago, so the best examples/tutorials on Wizards available at the time may not have been great.
Now, most pages of the Wizard ask for user's input. The subsequent pages are rendered depending on what the user have entered. If the user is say on page 3 and decided to ht a back button all the way to 1, and has not changed anything, and hits Next twice, then the state should not change. However, changing things on page x will generally invalidate stuff on pages x + 1 and further. There are exceptions to this, however, as some or all settings on a page x might depend on page x-1, x-2, etc., but pages x+1, x+2, etc do not depend on that x for some x.
I hope things are clear so far. We try to help the user by defaulting some stuff for them. The way things are stored is not great either. The dialog has read/write properties, from/to which stuff is copied to/from the actual controls. Then, in the main method, there is a "super-storage" which holds storages for each page. So, when the user is done with page x and hits next, stuff is first being copied from controls into storage that is local to the class, and then that stuff gets saved into the appropriate member of the super-storage.
Arrays (of dialogs/storages) and indices are not being used. There is separate yet similar "create&populate" logic for every goto destination (label). The dialogs objects get thrown away when the page is no longer displayed (they are not disposed, but every time they are to be shown, they get re-created and re-populated anew. I do not believe this is necessary, as only a single handle is needed, and after it has been shown, and closed, I believe it can be shown again in the same state, without having to re-populate controls. If wasting memory was the only issue, I would probably let things slide, but the thing is not very maintainable, so I might as well fix it all up.
I am thinking:
- Store dialogs in a collection, such as an array, but preferably D.L.L. because I can only move forward 1 or backward 1, or only one of the two options I listed (for the first and last dialog).
- Actually have my tabs/pages all extend a common abstract class (because "next", "back", "exit" buttons and their behavior are common to all).
- Each tab/page/dialog (same thing for the purpose of this question, sorry for confusion) will have read-only properties visible to the "conductor" class. These properties will derive from the values in controls (the true source of info), sometimes the properties will massage these values a bit. It will be the responsibility of the "conductor" to grab those and put them into storage. When the conductor wishes to populate the dialog with a single method (let's call it "seed"). I have a bit of difficulty here, since the parameters for each seed method will be different. I want to be able to both take advantage of strong typing, as well as keeping things generic. I suspect that something ought to give. I could pass in a dictionary to each seed method, but that feels too Pythonic, like duck typing. I would not know until the run time if I screwed up. Also, the packing and unpacking of the dictionary better always match, for any given page. This is where you would come in.
- The global storage can be one giant dictionary. I can be disciplined enough to keep all keys different, or prefix their names with "p1_" through "p5_" depending on a page, just to be sure. I am sure that other schemes exist as well. Having on giant dictionary can be convenient at the very end - there the order in which user input was assembled will not matter, as long as it is done correctly. I can also have a state machine ... sort of. This is where I am also getting lost in the design. If I keep things in a dictionary, I would have to perform a lot of conditional logic, such as: if I am on page 2, and I make a change, then I usually (there may be exceptions) need to make old defaults if any for pages 3,4,5 invalid. Depending on how ugly it gets, it might not be much better than the current goto-based design. I think I can do better, however, as I can implement my state or state transition-specific logic with a bunch of delegates, handles to which are stored in two dictionaries (one for next, one for back), where current state is the key.
As you can see, there are a few challenges. I am hopeful, however, because thinking through a good Wizard design is a wheel that surely was [re-]invented before. Perhaps you can recommend an open source C# / mono app which come with a linear, yet non-trivial Wizard, so that I can take a peek at the implementation. Heck, maybe even Java/Swing will probably suit me as long as the Wizard is similar in nature. WPF would be yet another challenge for me, I do not want to have 2 problems instead of 1.
Let me know what you can think of. Should I just keep the gotos but clean up other parts as much as I can? Feel free to ask questions. Thanks,
-HG