A certain form in our application displays a graphical view of a model. The user can, amongst loads of other stuff, initiate a transformation of the model that can take quite some time. This transformation sometimes proceeds without any user interaction, at other times frequent user input is necessary. While it lasts the UI should be disabled (just showing a progress dialog) unless user input is needed.
Possible Approaches:
- Ignore the issue, just put the transformation code in a procedure and call that. Bad because the app seems hung in cases where the transformation needs some time but requires no user input.
- Sprinkle the code with callbacks: This is obtrusive – you’d have to put a lot of these calls in the transformation code – as well as unpredictable – you could never be sure that you’d found the right spots.
- Sprinkle the code with Application.ProcessMessages: Same problems as with callbacks. Additionally you get all the issues with ProcessMessages.
- Use a thread: This relieves us from the “obtrusive and unpredictable” part of 2. and 3. However it is a lot of work because of the “marshalling” that is needed for the user input – call Synchronize, put any needed parameters in tailor-made records etc. It’s also a nightmare to debug and prone to errors.
//EDIT: Our current solution is a thread. However it’s a pain in the a** because of the user input. And there can be a lot of input code in a lot of routines. This gives me a feeling that a thread is not the right solution.
I’m going to embarass myself and post an outline of the unholy mix of GUI and work code that I’ve produced:
type // Helper type to get the parameters into the Synchronize'd routine: PGetSomeUserInputInfo = ^TGetSomeUserInputInfo; TGetSomeUserInputInfo = record FMyModelForm: TMyModelForm; FModel: TMyModel; // lots of in- and output parameters FResult: Boolean; end; { TMyThread } function TMyThread.GetSomeUserInput(AMyModelForm: TMyModelForm; AModel: TMyModel; (* the same parameters as in TGetSomeUserInputInfo *)): Boolean; var GSUII: TGetSomeUserInputInfo; begin GSUII.FMyModelForm := AMyModelForm; GSUII.FModel := AModel; // Set the input parameters in GSUII FpCallbackParams := @GSUII; // FpCallbackParams is a Pointer field in TMyThread Synchronize(DelegateGetSomeUserInput); // Read the output parameters from GSUII Result := GSUII.FResult; end; procedure TMyThread.DelegateGetSomeUserInput; begin with PGetSomeUserInputInfo(FpCallbackParams)^ do FResult := FMyModelForm.DoGetSomeUserInput(FModel, (* the params go here *)); end; { TMyModelForm } function TMyModelForm.DoGetSomeUserInput(Sender: TMyModel; (* and here *)): Boolean; begin // Show the dialog end; function TMyModelForm.GetSomeUserInput(Sender: TMyModel; (* the params again *)): Boolean; begin // The input can be necessary in different situations - some within a thread, some not. if Assigned(FMyThread) then Result := FMyThread.GetSomeUserInput(Self, Sender, (* the params *)) else Result := DoGetSomeUserInput(Sender, (* the params *)); end;
Do you have any comments?
I think as long as your long-running transformations require user interaction, you’re not going to be truly happy with any answer you get. So let’s back up for a moment: Why do you need to interrupt the transformation with requests for more information? Are these really questions you couldn’t have anticipated before starting the transformation? Surely the users aren’t too happy about the interruptions, either, right? They can’t just set the transformation going and then go get a cup of coffee; they need to sit and watch the progress bar in case there’s an issue. Ugh.
Maybe the issues the transformation encounters are things that could be ‘saved up’ until the end. Does the transformation need to know the answers immediately, or could it finish everything else, and then just do some ‘fix-ups’ afterward?