User:Barre/Workflow
Here is a first stab at a worfklow framework for the EM-Segmentation Wizard project. It's a first pass at a set of classes that are hopefully general enough to be subclassed into something more domain/application specific.
Since a wizard is pretty much a set of steps and some logic/controls to go from one to the other, a "decently robust" framework for this should map, at least, a state machine or a Petri net. I've talked to Luis, and we agreed that even a stab at a simple Petri net engine would not be a trivial task given the time frame. However, we do have some experience in state machines at Kitware, and we can leverage this effort. Such a framework is not going to be as fancy as the one found in IGSTK since we need to remain "wrappable" and we can't use ITK-style C++ templating and other compiler-checks, but this should do the trick and hopefully be high-level enough that people at NA-MIC can use it for more sophisticated workflows. After this section, I introduce a few more classes to constrain this framework to the wizard domain, and make our life a little easier.
- vtkKWStateMachineState: a state
- vtkKWStateMachineTransition: a transition between 2 states
- vtkKWStateMachineInput: an input (which triggers transitions)
- vtkKWStateMachine: a state machine
Nothing too complicated so far, you have your states, you specificy which transitions are triggered when the machine is in a particuliar state and receive particuliar inputs, etc.
vtkKWStateMachineState: a state
- [Get] Id: auto-generated unique-id (int). - [Set/Get] Name (string): simple name (say, to put in a menu) - [Set/Get] Description (string): longer description (say, to display in a help section or bubble) - [Set/Get] EnterCommand (callback): (optional) callback fired when we enter that state, whatever the transition (this can be used, say, to bring the UI of a the wizard step associated to that state) - [Set/Get] LeaveCommand (callback): (optional) callback fired when we leave that state, whatever the transition (this can be used, say, to cleanup any resources used by the UI of the wizard step associated to that state, maybe a 3D renderwidget preview for example)
vtkKWStateMachineInput: an input
- [Get] Id: auto-generated unique-id (int).
vtkKWStateMachineTransition: a transition between 2 states
- [Set/Get] Input (vtkKWStateMachineInput*) - [Set/Get] PreviousState (vtkKWStateMachineState*) - [Set/Get] NextState (vtkKWStateMachineState*) - [Set/Get] Command (callback): (optional) callback fired by the transition
vtkKWStateMachine: a state machine
- [Add] State (vtkKWStateMachineState*): add a state to the state machine - [Add] Input (vtkKWStateMachineInput*): add an input to the state machine - [Add] Transition (vtkKWStateMachineTransition*): add a transition to the state machine - [Add] Transition (vtkKWStateMachineState*, vtkKWStateMachineInput*, vtkKWStateMachineState*, callback): convenience method to create a transition and add it to the state machine - [Set/Get[ CurrentState (vtkKWStateMachineState*): initial/current state - PushInput(vtkKWStateMachineInput*): push a new input in the queue of inputs to be processed - ProcessInputs(): perform the state transition and invoke the corresponding action for every pending input stored in the input queue.
A class that needs a state machine would declare states and inputs as class members, initialize the machine, add the states, add the inputs, then add the transitions (transitions objects don't really need to be created explicitly by the user). This would probably look like this (for the first few steps of the EMseg project: "specify tree", "specify atlas", "specify target images"):
vtkKWEMSegmentationWorkflow.h:
class vtkKWEMSegmentationWorkflow: vtkKWStateMachineState *TreeStep; vtkKWStateMachineState *AtlasStep; vtkKWStateMachineState *TargetImagesStep; vtkKWStateMachineInput *InvalidTreeStepInput; vtkKWStateMachineInput *GoToTreeStepInput; vtkKWStateMachineInput *GoToAtlasStepInput; vtkKWStateMachineInput *GoToTargetImagesStepInput; vtkKWStateMachine *StateMachine; virtual void ShowTreeStep(); virtual void HideTreeStep(); virtual void ShowAtlasStep(); virtual void HideAtlasStep(); virtual void ShowTargetImagesStep(); virtual void HideTargetImagesStep(); virtual void ReportTreeStepInvalidError();
vtkKWEMSegmentationWorkflow.cxx:
// Steps. The callbacks as used to bring up or hide the UIs this->TreeStep = vtkKWStateMachineState::New(); this->TreeStep->SetName("Specification of the tree"); this->TreeStep->SetEnterCommand(this, "ShowTreeStep"); this->TreeStep->SetLeaveCommand(this, "HideTreeStep"); this->AtlasStep = vtkKWStateMachineState::New(); this->AtlasStep->SetName("Specification of the atlas"); this->AtlasStep->SetEnterCommand(this, "ShowAtlasStep"); this->AtlasStep->SetLeaveCommand(this, "HideAtlasStep"); this->TargetImagesStep = vtkKWStateMachineState::New(); this->TargetImagesStep->SetName("Specification of the target images"); this->TargetImagesStep->SetEnterCommand(this, "ShowTargetImagesStep"); this->TargetImagesStep->SetLeaveCommand(this, "HideTargetImagesStep"); // Inputs this->InvalidTreeStepInput = vtkKWStateMachineInput::New(); this->GoToTreeStepInput = vtkKWStateMachineInput::New(); this->GoToAtlasStepInput = vtkKWStateMachineInput::New(); this->GoToTargetImagesStepInput = vtkKWStateMachineInput::New(); // State Machine this->StateMachine = vtkKWStateMachine::New();; // Add the states this->StateMachine->AddState(this->TreeStep); this->StateMachine->AddState(this->AtlasStep); this->StateMachine->AddState(this->TargetImagesStep); // Add the inputs this->StateMachine->AddInput(this->InvalidTreeStepInput); this->StateMachine->AddInput(this->GoToTreeStepInput); this->StateMachine->AddInput(this->GoToAtlasStepInput); this->StateMachine->AddInput(this->GoToTargetImagesStepInput); // Add the transitions // Error control: loop over the 'Tree' step each time it is found // to be invalid, and fire a method that could be used to display an // error message. // Say, pressing the "Next" button in a wizard from the 'Tree' step would // first validate this step, and if it fails push the 'InvalidTreeStepInput' // input to the state machine; the transition would invoke the // 'ReportTreeStepInvalidError' method on 'this' object) this->StateMachine->AddTransition(this->TreeStep, this->InvalidTreeStepInput, this->TreeStep, this, "ReportTreeStepInvalidError") // Go to the next step (no callback needed on the transition) // Say, pressing the "Next" button in a wizard from the 'Tree' step would // first validate this step, and if it succeeds push the 'GoToAtlasStepInput' // input to the state machine, leading us to the 'Atlas' step. this->StateMachine->AddTransition(this->TreeStep, this->GoToAtlasStepInput, this->AtlasStep) // Go back the previous step (no callback needed on the transition) // Say, pressing the "Back" button in a wizard from the 'Atlas' step would // push the GoToTreeStepInput' input to the state machine, leading us back to // the 'Tree' step. this->StateMachine->AddTransition(this->AtlasStep, this->GoToTreeStepInput, this->TreeStep) // Skip a step (no callback needed for the transition) // Say, pressing the "Finish" button in a wizard from the 'Tree' step (or // selecting a step directly from a pulldown menu listing all steps) would // first validate this step, and if it succeeds push the GoToAtlasStepInput // input to the state machine, leading us to the 'Target Images' step // (it is considered to be the last one in this example). Now of course // more logic is required here, for if you skip steps, you probably // want to estimate good default values for the steps in between, etc) this->StateMachine->AddTransition(this->TreeStep, this->GoToTargetImagesStepInput, this->TargetImagesStep)
How can we make things a little easier for the specific case of a simple Wizard. Here are a couple of thoughts:
- a wizard is a linear succession of steps,
- you can navigate from one step to the next one other using a 'Next' and 'Back' button,
- you can navigate from one step to the previoous one using a 'Back' button,
- a 'Finish' may allow you to skip to the last step,
- a menu or a list of steps may allow you to go to a specific step explicitly,
- one need to make sure a step is valid before going to the next one,
- one probably do *not* need to make sure a step is valid before going to the previous one,
- one probably want to be able to guess a reasonable set of defaults values for a given step (ideally, values that make that step already 'valid'),
- one could be interested in knowing how much of a step is 'completed' to make it 'valid' (this could be used in the UI to display step names as 'green' or 'red' and locate visually which steps require to be completed in priority).
Let's introduce the following subclasses:
vtkKWWizardStep: a wizard step, subclass of vtkKWStateMachineState
- [Set/Get] ValidateCommand (callback): callback that will validate the step and push the corresponding error inputs if needed (returns int) - [Set/Get] EstimateDefaultsCommand (callback): callback that will attempt to estimate reasonable defaults value for this step - [Set/Get] EstimateCompletionCommand (callback): callback that can be used to estimate the completion of that step (returns normalized double) - [Get] GoToSelfInput (vtkKWStateMachineInput*): member variable that acts as an input specifying "I want to go to that instance step"
vtkKWWizardWorkflow: a wizard workflow, subclass of vtkKWStateMachine
- AddNavigationTransitions(): assuming the states/steps were added as instance of vtkKWWizardStep, in the right order, create transitions from one step to the next and previous one (if any), and from one step to any other step, using each step's vtkKWWizardStep::GoToSelfInput member variable as input. - vtkKWStateMachineState* GetNextStep(): given the current step, return the next one (assuming they were added in order and as instance of vtkKWWizardStep) - vtkKWStateMachineState* GetPreviousStep(): given the current step, return the previous one (assuming they were added in order and as instance of vtkKWWizardStep) - GoToNextStep(): given the current step 'current_step': invoke 'current_step->ValidateCommand on the current step/state. - if it fails (returns o), do nothing, as it is up to the ValidateCommand to push error inputs that will lead to step-dependent error UI/callbacks - if it succeeds (returns 1), get the next step, push a next_step->GoToSelfInput to go to that step - GoToPreviousStep(): given the current step 'current_step': - get the previous step, push a prev_step->GoToSelfInput to go to that step - GoToStep(vtkKWWizardStep *step): given the current step 'current_step' 'while' we have not reached 'step', validate current step, - if it fails break the loop, - if it succeeds get the next step, invoke EstimateDefaultsCommand and push a next_step->GoToSelfInput the loop will try to go as far as it can, as long as the step could be validated with the defaults that were estimated. - GoToLastStep(): self-explanatory - GetCurrentStep(): convenience method to get the current step as a safe-downcast instance of a vtkKWWizardStep
Note that you can add more steps/states to the machine, but they should not be instances of vtkKWWizardStep, just vtkKWStateMachineState. This is a constraint/assumption.
From a UI point of view, we probably also need something that can tell us *if* we can go to a given step, so that we can disable or grey out the corresponding "Next"/"Back" button, I'll leave that for another time.
Here is the new code.
vtkKWEMSegmentationWorkflow.h:
class vtkKWEMSegmentationWorkflow: vtkKWWizardStep *TreeStep; vtkKWWizardStep *AtlasStep; vtkKWWizardStep *TargetImagesStep; vtkKWStateMachineInput *InvalidTreeStepInput; vtkKWWizardWorkflow *WizardWorkflow; virtual void ShowTreeStep(); virtual void HideTreeStep(); virtual int ValidateTreeStep(); virtual void EstimateTreeStepDefaults(); virtual void ShowAtlasStep(); virtual void HideAtlasStep(); virtual int ValidateAtlasStep(); virtual void EstimateAtlasStepDefaults(); virtual void ShowTargetImagesStep(); virtual void HideTargetImagesStep(); virtual int ValidateTargetImagesStep(); virtual void EstimateTargetImagesStepDefaults(); virtual void ReportTreeStepInvalidError();
vtkKWEMSegmentationWorkflow.cxx:
this->TreeStep = vtkKWWizardStep::New(); this->TreeStep->SetName("Specification of the tree"); this->TreeStep->SetEnterCommand(this, "ShowTreeStep"); this->TreeStep->SetLeaveCommand(this, "HideTreeStep"); this->TreeStep->SetValidateCommand(this, "ValidateTreeStep"); this->TreeStep->SetEstimateDefaultsCommand(this, "EstimateTreeStepDefaults"); this->AtlasStep = vtkKWWizardStep::New(); this->AtlasStep->SetName("Specification of the atlas"); this->AtlasStep->SetEnterCommand(this, "ShowAtlasStep"); this->AtlasStep->SetLeaveCommand(this, "HideAtlasStep"); this->AtlasStep->SetValidateCommand(this, "ValidateAtlasStep"); this->AtlasStep->SetEstimateDefaultsCommand(this, "EstimateAtlasStepDefaults"); this->TargetImagesStep = vtkKWWizardStep::New(); this->TargetImagesStep->SetName("Specification of the target images"); this->TargetImagesStep->SetEnterCommand(this, "ShowTargetImagesStep"); this->TargetImagesStep->SetLeaveCommand(this, "HideTargetImagesStep"); this->TargetImagesStep->SetValidateCommand(this, "ValidateTargetImagesStep"); this->TargetImagesStep->SetEstimateDefaultsCommand(this, "EstimateTargetImagesStepDefaults"); // Inputs this->InvalidTreeStepInput = vtkKWStateMachineInput::New(); // State Machine this->WizardWorkflow = vtkKWWizardWorkflow::New();; // Add the states this->WizardWorkflow->AddState(this->TreeStep); this->WizardWorkflow->AddState(this->AtlasStep); this->WizardWorkflow->AddState(this->TargetImagesStep); // Add the inputs this->WizardWorkflow->AddInput(this->InvalidTreeStepInput); // Add the transitions this->WizardWorkflow->AddNavigationTransitions(); // Error control: loop over the 'Tree' step each time it is found // to be invalid, and fire a method that could be used to display an // error message. // Say, pressing the "Next" button in a wizard from the 'Tree' step would // first validate this step, and if it fails push the 'InvalidTreeStepInput' // input to the state machine; the transition would invoke the // 'ReportTreeStepInvalidError' method on 'this' object) this->WizardWorkflow->AddTransition(this->TreeStep, this->InvalidTreeStepInput, this->TreeStep, this, "ReportTreeStepInvalidError")
Now at this point this vtkKWEMSegmentationWorkflow class could be a subclass of a vtkKWWizardWidget that presents some UI real-estate and "Next" / "Back" / "Finish" buttons at the bottom of the page associated to the GoToNextStep, GoToPreviousStep and GoToLastStep methods of the workflow instance...
Note that this code:
this->TreeStep = vtkKWWizardStep::New(); this->TreeStep->SetEnterCommand(this, "ShowTreeStep"); this->TreeStep->SetEstimateDefaultsCommand(this, "EstimateTreeStepDefaults");
...means that when the 'Tree' step/state is reached (whatever the transition), the ShowTreeStep callback is triggered, which we have set to:
virtual void ShowTreeStep();
In this ShowTreeStep callback, you could reuse the validation callback by doing something like:
this->WizardWorkflow->GetCurrentStep()->InvokeEstimateDefaultsCommand();
which would invoke:
virtual void EstimateTreeStepDefaults();