I'm looking to build a routine that is responsible for executing any action via procedure (Tproc) using Tasks, while displaying a loading screen, without freezing the system's main form. An extra detail is that it is also capable of capturing a possible exception that the action may have generated.
I built the code below, which works well, but sometimes something goes wrong and the loading screen ends up not closing, remaining always present on the screen.
Any suggestions will be welcome. Thanks.
type
TLoadingClass= class
strict private
class var FForm: TForm;
public
class procedure ActionAndWait(Action: Tproc);
end;
class procedure TLoadingClass.ActionAndWait(Action: Tproc);
var
aTask: ITask;
vException: Pointer;
begin
vException := nil;
FForm := TLoadingForm.Create(nil);
try
aTask := TTask.Run(procedure
begin
try
try
Action; {Run Action}
except on E: Exception do
vException := @E {Capture Exception}
end
finally
while not FForm.Showing do {Wait for the form to be created, if the action is very quick.}
Sleep(1);
TLoadingForm(FForm).Hide;
TLoadingForm(FForm).Close;
end;
end);
TLoadingForm(FForm).ShowModal; {Show the loading form}
finally
TTask.WaitForAll(aTask);
FreeAndNil(FForm);
if Assigned(vException) then
raise Exception(@vException);
end;
end;
Call example
TLoadingClass.ActionAndWait(
procedure
begin
try
Sleep(5000);
raise Exception.Create('Test');
except on E: Exception do
ShowMessage(E.Message);
end;
end);
Your code has several issues.
First, you are accessing the UI (form) from the background thread which you should never do. Such code always needs to be synchronized with the main thread.
Next, you are not handling task exceptions properly. Exception objects are automatically handled by compiler and you cannot just grab a pointer to the exception object and use it later on. The whole try...except
within task method is useless. If there is unhandled exception within the task, Wait
or WaitForAll
will raise EAggregatedException
and you can catch that exception and handle its inner exception(s) there. If there are multiple tasks that raised exceptions, there will be multiple inner exceptions.
Next, you are already catching the exception inside anonymous method passed to ActionAndWait
so such captured exception will not be propagated as EAgreggatedException
. ShowMessage
within ActionAndWait
will run in the context of the background thread and if you want to use and UI from there it also needs to be synchronized with the main thread.
You have FForm
declared as field in TLoadingClass
. It would be better that you remove that field and use local variable like you are using for the task. Also there is no need to typecast FForm
as TLoadingForm
when you are calling Hide
and Close
. Besides that, calling Close
is sufficient as it will also hide the form.
Cleaned and corrected code would look like:
class procedure TLoadingClass.ActionAndWait(Action: Tproc);
var
LTask: ITask;
LForm: TLoadingForm;
begin
try
LTask := TTask.Create(procedure
begin
try
Action;
finally
TThread.Queue(nil,
procedure
begin
LForm.Close;
end);
end;
end);
LForm := TLoadingForm.Create(nil);
try
try
LTask.Start;
LForm.ShowModal;
finally
TTask.WaitForAll(LTask);
end;
finally
LForm.Free;
end;
except
on E: EAggregateException do
for var i := 0 to E.Count - 1 do
ShowMessage(E.InnerExceptions[i].Message);
on E: Exception do
ShowMessage(E.Message);
end;
end;
And the call example:
TLoadingClass.ActionAndWait(
procedure
begin
Sleep(5000);
raise Exception.Create('Test');
end);