views:

116

answers:

4

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 gotos. 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 gotos 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:

  1. 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).
  2. Actually have my tabs/pages all extend a common abstract class (because "next", "back", "exit" buttons and their behavior are common to all).
  3. 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.
  4. 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

A: 

why not create a class that has properties for each input field for each page and keep track of them whilst the user clicks through the wizard, that way you would be able to jump back and forth between several pages and still keep the data that has been added by the user under his session. you could even have the input validation in these page models so when a user clicks next you could do something like

if(!page1model.IsValid)
{
     List<RuleViolation> ruleViolations = page1model.GetRuleViolations();
}

this is if I understood some of your problems.

(for keeping track of the pages you could have the page models implement the same interface and create a List<IPageModel> or something and add the page models to it)

Joakim
A: 

I vote for superstructure as a parameter passed to each page as it's being shown. Second parameter would be the direction with which we came to the page (Next or Back). If it's Back then just show the data that already exist. If it's Next then use data from previous pages (found in superstructure) to show appropriate data in current page. The page should also have the information if it is being shown for the first time. If it is then it should provide default data, and if it's not then it can recycle existing data attributed to that page (provided that they don't contradict the data from previous pages). State is just a number, that gets incremented or decremented after each page. Main code is one small while loop that just shows a page for current state and updates a state when user finishes with the page. When user leaves the last page with Next then exit loop.

If there are optional pages then the main code gets a little complicated, because you then need a logic to decide what is next page (and what were all previous pages), but everything else is still the same.

Dialecticus
Hm ... that way each "page" has to know what all others are. I am thinking of being able to refresh a "page" after it has been constructed with any new important information, and have the page return any important information that was asked for. Then some sort of controller from outside ...
Hamish Grubijan
The page is in the best position to "know" what user needs to see in that page, and to know what information it requires to achieve that. And the page requires all information that it requiers :-). There is no reason to hide bits of information from the page, as there is no need to make the whole thing overly modular. That is, unless you want to reuse "modules" in another project. In this solution you can still refresh the page, and in fact you should refresh each time the page is shown.But, if you have a solution in mind, then just go for it.
Dialecticus
A: 

Create one winform that will host a user control that implements an interface. make all pages user controls that implement this interface, and control the flow from your top-level form. if you like this approach, i can dig up some old code to give more detail.

Alex
@Alex, this looks promising, and yes, I do need code before I can make up my mind. I will not down-vote even if it does not work out. I encourage others not to punish attempts to help either.
Hamish Grubijan
+1  A: 

I have some WInForms wizard code this is the general architecture:

On main wizard have StepList As List(Of baseWizardStep) .. list of all possible steps which is created when the wizard main form is.

Also Next Back and Cancel buttons.

As with Alex above, have a base class (baseWizard) main form with a panel that hosts a user control for the current step. Each wizard step itself is a user control (Inherits from baseWizardStep that inherits from UserControl) baseWizardStep has some built in events (ArriveAtStep, ValiateStep, LeaveStep etc..) Also in an InitliaseStep sub that takes main wizard as a parameter and stores a ref to main wizard form in property. All the steps access the main wizard to store their data in properties/objects/datasets etc.. (Storing of data is usually done on the LeaveStep event) The steps load data into their controls on the ArriveAtStep event.

I sometimes need to share wizard steps amongst many wizards, in which case the mainWizard implements an interface and the wizard steps cast their main wizard property to the interface to access/store data.

I have 2 events top control flow through the wizard.

If you don't handle any of these 2 events, then the wizard goes through from 1st in StepList to the last in order one step at a time.

Event 1: If you handle ShoudlStepOccur this event allows developer to decide should a step occur in the list of steps (so you can skip steps) This seems to cope with most wizards in an easy to undserstand metaphor. Event gives you the step and the diurection (Forward, back)

Event 2: (advanced control) THen there is the other event on the main form NavigateToStep, event gives you the step it was intending to go to, but you can change it to navigate to a completely differeent step. We use this to go round in a loop many times (Eg. In the Enrol on a course wizard, we go throurgh some steps of the wizard many times once for each course)

I also have a grid that lists all steps with the current step highlighted and that the user can click on to jump thourgh the wizard. I use the StepShouldOccur event to know which steps to disdplay in the step list at any time.

One gatchas: -When close wizard, you must dispose the steps that are no currenlty hosted in the panel and just lying around in the StepList becuase they will not releae their windows handles otherwise.

Not sure how much sense that's making so I'm going to leave it there.

Maybe one of these days I'll put this wizard code up on code project or something, I'm quite happy with how it works for us.

MikeG
@MikeG Thanks, could you share the code?
Hamish Grubijan