Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Keep app responsive during long task

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:

  1. 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.
  2. 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.
  3. Sprinkle the code with Application.ProcessMessages: Same problems as with callbacks. Additionally you get all the issues with ProcessMessages.
  4. 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?

like image 409
Uli Gerhardt Avatar asked Nov 30 '22 07:11

Uli Gerhardt


2 Answers

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?

like image 149
Rob Kennedy Avatar answered Dec 09 '22 15:12

Rob Kennedy


Definitely go for a threaded option (even after your edit, saying you find it complex). The solution that duffymo suggests is, in my opinion, very poor UI design (even though it's not explicitly about the appearance, it is about how the user interfaces with your application). Programs that do this are annoying, because you have no idea how long the task will take, when it will complete, etc. The only way this approach could be made better would be by stamping the results with the generation date/time, but even then you require the user to remember when they started the process.

Take the time/effort and make the application useful, informative and less frustrating for your end user.

like image 36
James B Avatar answered Dec 09 '22 14:12

James B