Form management to speedup app

My webapp is too slow to open/close and reopen forms i think due to loading form objects everytime.
My instructions are :

TfrmAccount_IE.CreateForm(DM0.Username, DM0.Password);
frmAccount_IE.ShowModal(@AfterShowModal);

and in the form

procedure TfrmAccount_IE.WebFormClose(Sender: TObject; var Action: TCloseAction);
begin
Action:=TCloseAction.caFree;

Now my idea is to create one time all forms (about 5/6) and to call them with Shomodal();

  1. is a good idea to increase performance with memory in a mobile (smartphone) use ?
  2. strange things : if i close form without Action:=TCloseAction.caFree; and recall showmodal form doesn't show ! (i see multiform sample but never goes if i want to "only" show a created form !)

I hope i'm clear !

Just out of curiosity, how slow is it? It does depend on how big the forms are (how many controls), but I've found that there has to be quite a lot of controls before it slows down. The biggest form I have so far takes about 0.5s to load so not terrible in my case, but not sure what your requirements are.

Also, loading the form may include the time needed to run the "oncreate" functions which may have code that is also slowing it down. I've found it "looks" better if some of that code is moved to a timer event so that the form load happens more quickly (and the form is displayed) and then the rest of the code happens afterwards, particularly if that code is loading data from a remote site or something. May or may not work for your situation.

If your forms are really large and complex it might be better to load them up individually as needed. The tradeoff might be whether there is a delay when the app first loads (when loading all the forms) versus the smaller delay when loading the forms individually when needed later on. I prefer the loading of forms later on because I don't know how many forms are going to be actually used so it seems a bit of a waste to do that if it isn't necessary, particularly important if they each generate remote data requests.

if you understand italian try to run this app to smartphone : RISTORD by Megagest

account is : demo pwd : demo

Do this step :

Login -> "Entro per fare un nuovo ordine"
Press : "Fai un ordine", if messagedlg click "No"
On menù : select categories with arrow or press "Indietro" (top left)
on food list click "+" to add a food then "Conferma e vai al menù" to add others ...
Click "Vai al riepilogo" to summaries orders
Click "concludi ordine" to go to close the order with "Asporto" or "Consegna" choice
"Conferma ed inoltra" end order !

If you do this steps many times (go back and forward) you can see the speed !

Make sure you use begin/end update on controls.

I tried it on an iPhone XR and on a couple of browsers. My Italian is unusally bad, but I didn't see anything that was super-slow.

In this case, though, it might be best to create all the forms ahead of time, paritcularly if your menu accesses different forms. The delay at the beginning is likely to be better than any delay later on in this situation, and the menu isn't going to change for that user while they're in the middle of making an order.

It would also be helpful to know what is causing any delays. In my project, I've got a log (just a TMemo) where I write timestamped events, like when a button is clicked or when an XData request is issued and received and that kind of thing. Kind of a simplistic approach, but helps narrow down what is actually taking up the time. Particularly as I'm kinda new to all this async stuff and I've learned a lot about how some events that should be quick might not be or how events sometimes complete in a different order than expected.

For example, when a person clicks a button in my app, it loads a form which then loads some data and does other things. The log entries might look something like this. Makes it easy for me to understand where any delay is coming from.

[ 2021-11-15 00:27:11.916 ] LAUNCH: Config/Configuration Manager (Module #0186) 
[ 2021-11-15 00:27:12.086 ] -- Launched (170ms) 
[ 2021-11-15 00:27:12.425 ] -- Iniitialized Grid [ 18601 ]: Parameter List 
[ 2021-11-15 00:27:12.444 ] -- Iniitialized Grid [ 18602 ]: Parameter 
[ 2021-11-15 00:27:12.449 ] -- Iniitialized Grid [ 18603 ]: Logos 
[ 2021-11-15 00:27:12.455 ] -- Iniitialized Grid [ 18604 ]: HTML Blocks 
[ 2021-11-15 00:27:12.455 ] -- Grids Created [ 50ms delay ] 
[ 2021-11-15 00:27:12.455 ] -- Initializing Grid Navigators 
[ 2021-11-15 00:27:12.457 ] -- Configuration Manager Startup Complete (542ms) 
[ 2021-11-15 00:27:12.467 ] -- Requesting Parameter List 
[ 2021-11-15 00:27:12.470 ] -- Requesting HTML Block List 
[ 2021-11-15 00:27:12.631 ] ----- Retrieved JSON File: 20754 bytes 
[ 2021-11-15 00:27:12.631 ] ----- Loading Parameter List 
[ 2021-11-15 00:27:12.941 ] ----- Parameter List Refreshed (158 Parameters) (483ms) 
[ 2021-11-15 00:27:12.957 ] ----- Retrieved JSON File: 998 bytes 
[ 2021-11-15 00:27:12.957 ] ----- Loading HTML Block List 
[ 2021-11-15 00:27:12.959 ] ----- HTML Block List Refreshed (7 Blocks) (489ms) 
[ 2021-11-15 00:27:13.197 ] -- Requesting Parameter # 000 / Worker Status
[ 2021-11-15 00:27:13.297 ] ----- Retrieved JSON File: 672 bytes 
[ 2021-11-15 00:27:13.297 ] ----- Loading Parameter # 000 / Worker Status 
[ 2021-11-15 00:27:13.333 ] ----- Parameter # 000 Refreshed (5 entries) (138ms) 
[ 2021-11-15 00:27:18.451 ] -- Requesting Parameter # 004 / Country 
[ 2021-11-15 00:27:18.605 ] ----- Retrieved JSON File: 415 bytes 
[ 2021-11-15 00:27:18.605 ] ----- Loading Parameter # 004 / Country 
[ 2021-11-15 00:27:18.659 ] ----- Parameter # 004 Refreshed (3 entries) (219ms) 
[ 2021-11-15 00:27:21.609 ] CLOSE: Configuration Manager

In this example, the "Launched" entry is the time taken for the actual WebFormCreate call to complete (170ms), and the "Startup Complete" (542ms) is how long it takes for the timer event that is initiated by WebFormCreate to complete.

Thank you andrew for your suggestions...
i want to try to create before all navigation forms (about 4) in application starting but i ve difficult to only show a form after its creation.

Can you give me the best way to create all forms in a main procedure or at start application and use these ?

For example i've modified Multiform sample BUT i've error, application crash. Code below :

  public
    newform: TForm2;


procedure TForm1.WebFormCreate(Sender: TObject);  // Create main form
begin
  Application.ThemeColor := clTMSWEB;

  newform := TForm2.Create(Self);
  newform.Caption := 'Child form';
  newform.Popup := WebRadioGroup1.ItemIndex <> 0;
end;

procedure TForm1.WebButton1Click(Sender: TObject);
begin
  case WebRadioGroup1.ItemIndex of
  2: newform.Border := fbDialog;
  3: newform.Border := fbDialogSizeable;
  end;

  // used to manage Back button handling to close subform
  window.location.hash := 'subform';

  // load file HTML template + controls
  await(TForm2, newform.Load());

  // init control after loading
  newform.frm2Edit.Text := WebEdit1.Text;

  try
    // excute form and wait for close
    await(TModalResult, newform.Execute);
    ShowMessage('Form 2 closed with new value:"'+newform.frm2Edit.Text+'"');
    WebEdit1.Text := newform.frm2Edit.Text;
  finally
    //newform.Free;
  end;
end;

Well, I guess there are a lot of options here depending on a few things. Hard to tell from your example which approach would work best for you. And I'm not sure what part of your example is crashing?

I think maybe part of the idea of creating forms dynamically at runtime is so that they can be more easily embedded inside of other forms. By creating them within a particular DIV that is already being displayed on the page, for example.

If your forms are always all fullscreen-type forms then they can just be created in the main project at application start. I think this is the default when adding new forms. Use the Project/View Source menu to bring up the application start code where you can see how the forms are created. LIkewise, the Project /Options > Application/Forms UI makes it easy to adjust which forms are created automatically at runtime. Nothing special about this, this is how Delphi has worked for a very long time. The code looks something like this:

begin
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TForm1, Form1);
  Application.CreateForm(TForm2, Form2);
  Application.CreateForm(TForm3, Form3);
  Application.CreateForm(TForm4, Form4);
  Application.Run;
end.

The first form created is (I think?) the main form, so you can use it to hide or show the other forms. Others around here are I'm sure more knowledgeable about the execution order here, but I think it is something like all the WebFormCreate procedures are all called, and then the first form in the list is shown? You can also create DataModules in the same way but as they don't display anything, having them higher in the list (before the forms) doesn't change which form becomes the main form. This approach saves you the trouble of all the runtime form creation stuff with await, but means that it is all loaded up-front which may slow it down initially. Might not notice though.

To create forms at runtime, the sample code they provide is really what should be working. Not sure what is tripping you up exactly. In my code, I've got something similar to what you're trying to do in that a button click triggers the creation of a new form. For example, I've got a Menu form and when something is selected from the menu, it triggers a form to be loaded into a DIV on the same page.

...
public
  ActiveForm :TWebForm;  // Indicates form that has been created
...
procedure TForm1.WebButtonClick(Sender: TObject);
    procedure AfterCreate(AForm: TObject);
    begin
      // Do something after form is created
      // Usually this triggers a timer defined in the form to do initial setup or load data, etc.
      // that runs only after the form has finished fully being initialized
    end;
begin
  // Form appears within WebPanel1
  ActiveForm := TForm2.CreateNew(WebPanel1.ElementID, @AfterCreate);
end;

So... this is different from the examples they give. I actually don't recall why I don't have the await(TModalResult) thing going on, but I do move data back and forth between the controls in the parent and the created form - using ActiveForm as the way to do that. I guess the CreateNew call may be better off enclosed in await but I've not had problems so far. The WebFormCreate procedures in my forms are all setup to be quick though - no loading data or waiting on anything else - just load the controls and do whatever minimal setup is required, leaving the bulk of the work to a timer that is triggered afterwards, so the form is displayed as quickly as possible.

And in my case, I'm not waiting for the form to be closed for any reason - I don't really care about that. If the user clicks on another menu item, I close the form and create a new one in the same way.

Hi andrew indeed is what i do now with CreateNew, but i must everytime (i need) create and destroy form and in CreateForm event i do many things... I want to try to create all form in the application start as you mentioned (Application.CreateForm(TForm1, Form1) and only show and close it without create and destroy repeatedly. I can do that ?

Took quite a bit of fiddling but I've put together a demo project for you (or anyone else) to show forms that are created at runtime (once) and then swapped in and out of a main div.

In the demo, all the forms are visible (and functioning) at the same time. So when you click on the button at the bottom, it is actually the button on the form itself - just displayed in a smaller div that acts as a placeholder. Clicking on each of the buttons basically just swaps the forms between their original placeholders and the main form viewing div. Naturally in a real project the placeholders don't need to be visible and the display div can take up the whole screen.

The code for the buttons is pretty much the same on each form, so it could be turned into a common function easily enough I'm sure. And there are likely a thousand ways to do this kind of thing, or even do it the same way with prettier code. But it works well enough I think.

Hopefully it gives you some ideas as to how this kind of approach can work in your project:

FormTest.zip (224.7 KB)

And a video in case anyone wants to see what it looks like without the hassle of running the project:

formtest.mpeg

The main form code looks like this.

unit UnitMain;

interface

uses
  System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls,
  WEBLib.Forms, WEBLib.Dialogs, Vcl.Controls, Vcl.StdCtrls,
  WEBLib.StdCtrls, WEBLib.ExtCtrls, WEBLib.WebCtrls;

type
  TFormMain = class(TWebForm)
    WebPanelMain: TWebPanel;
    WebMemo1: TWebMemo;
    WebPanelTop: TWebPanel;
    WebLabelMain: TWebLabel;
    WebButtonMain: TWebButton;
    WebPanelCollection: TWebPanel;
    WebGridPanel1: TWebGridPanel;
    WebPanelHolder: TWebPanel;
    Panel1: TWebPanel;
    WebHTMLDiv1: TWebHTMLDiv;
    Panel2: TWebPanel;
    WebHTMLDiv2: TWebHTMLDiv;
    Panel3: TWebPanel;
    WebHTMLDiv3: TWebHTMLDiv;
    Panel4: TWebPanel;
    WebHTMLDiv4: TWebHTMLDiv;
    procedure WebFormCreate(Sender: TObject);
    [async] procedure WebButtonMainClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    Form1 :TWebForm;
    Form2 :TWebForm;
    Form3 :TWebForm;
    Form4 :TWebForm;
    CurrentForm: TWebForm;
    CurrentFormElement: String;
    CurrentFormSource: String;
  end;

var
  FormMain: TFormMain;

implementation

uses
  Unit1, Unit2, Unit3, Unit4;

{$R *.dfm}


procedure TFormMain.WebButtonMainClick(Sender: TObject);

  procedure AfterCreate(AForm: TObject);
  begin
    WebMemo1.Lines.Add((AForm as TWebForm).Caption+' Created');

    if (AForm as TWebForm).Caption = 'Form1' then
    begin
      WebMemo1.Lines.Add('Showing Form1');
      CurrentForm := Form1;
      CurrentFormElement := 'WebHTMLDiv1';
      CurrentFormSource := 'FormHolder1';
      asm
        var form = document.getElementById("WebHTMLDiv1");
        document.getElementById("FormHolderMain").append(form);
        setTimeout(() => { window.dispatchEvent(new Event('resize')); }, 0);
      end;
    end;
  end;

begin

  if not(Assigned(Form1)) then
  begin
    WebMemo1.Lines.Add('Creating Form1');
    Application.CreateForm(TForm1, WebHTMLDiv1.ElementID, Form1, @AfterCreate);
  end;

  if not(Assigned(Form2)) then
  begin
    WebMemo1.Lines.Add('Creating Form2');
    Application.CreateForm(TForm2, WebHTMLDiv2.ElementID, Form2, @AfterCreate);
  end;

  if not(Assigned(Form3)) then
  begin
    WebMemo1.Lines.Add('Creating Form3');
    Application.CreateForm(TForm3, WebHTMLDiv3.ElementID, Form3, @AfterCreate);
  end;

  if not(Assigned(Form4)) then
  begin
    WebMemo1.Lines.Add('Creating Form4');
    Application.CreateForm(TForm4, WebHTMLDiv4.ElementID, Form4, @AfterCreate);
  end;

end;

procedure TFormMain.WebFormCreate(Sender: TObject);
begin
  WebButtonMainClick(Sender);
end;

end.

And the forms (they're all the same basically) look like this.

unit Unit1;

interface

uses
  System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls,
  WEBLib.Forms, WEBLib.Dialogs, Vcl.StdCtrls, WEBLib.StdCtrls,
  Vcl.Controls, WEBLib.ExtCtrls;

type
  TForm1 = class(TWebForm)
    WebPanel1: TWebPanel;
    WebButton10: TWebButton;
    WebMemo10: TWebMemo;
    procedure WebButton10Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

uses
  UnitMain;

{$R *.dfm}



procedure TForm1.WebButton10Click(Sender: TObject);
var
  Element: String;
  Source: String;
begin
  if Assigned(FormMain.CurrentForm) then
  begin
    if (FormMain.CurrentForm <> FormMain.Form1) then
    begin

      FormMain.WebMemo1.Lines.Add('Hiding '+(FormMain.CurrentForm as TWebForm).Caption);

      // Local variables to use in JS
      Element := FormMain.CurrentFormElement;
      Source  := FormMain.CurrentFormSource;

      // Assignments for new form
      FormMain.CurrentForm        := FormMain.Form1;
      FormMain.CurrentFormElement := 'WebHTMLDiv1';
      FormMain.CurrentFormSource  := 'FormHolder1';

      // Hide currently shown form
      asm
        var oldform = document.getElementById(Element);
        document.getElementById(Source).append(oldform);

        var newform = document.getElementById('WebHTMLDiv1');
        document.getElementById('FormHolderMain').append(newform);

        setTimeout(() => { window.dispatchEvent(new Event('resize')); }, 0);
      end;

     FormMain.WebMemo1.Lines.Add('Showing '+(FormMain.CurrentForm as TWebForm).Caption);
    end;
  end;
end;

end.

The main project source looks like this (updated from the zip file, but functionally the same).

program Project1;

uses
  Vcl.Forms,
  WEBLib.Forms,
  UnitMain in 'UnitMain.pas' {FormMain: TWebForm} {*.html},
  Unit1 in 'Unit1.pas' {Form1: TWebForm} {*.html},
  Unit2 in 'Unit2.pas' {Form2: TWebForm} {*.html},
  Unit3 in 'Unit3.pas' {Form3: TWebForm} {*.html},
  Unit4 in 'Unit4.pas' {Form4: TWebForm} {*.html};

{$R *.res}

begin
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TFormMain, FormMain);
//  Application.CreateForm(TForm1, Form1);
//  Application.CreateForm(TForm2, Form2);
//  Application.CreateForm(TForm3, Form3);
//  Application.CreateForm(TForm4, Form4);
  Application.Run;
end.

So none of the subforms are created until needed, but they only need to be created once.

1 Like

Like Andrew I create my forms as needed and don't experience any slowness, even on some complex ones taht open several Xdata datasets. One difference is that I have a main form that is permanently displayed and all other forms either load into a div or are popups.

Slowness I think might be a relative thing. In my app, forms are created when a user clicks on a menu item, which is a pretty infrequent thing, compared to the time they spend on the form itself. So the < 1s delay in creating the form is inconsequential and likely goes unnoticed. And having access to the most current data at any time is kind of the point of the app, so any minor delay in refreshing that data is acceptable.

In his app though, navigating a collection of tableviews (is that what they're called?) going back and forth in a restaurant menu hierarchy, anything close to a 1s delay would be very noticeable and would be considered a poor user experience I'm sure. And the data presented (restaurant menu items) is likely to not change at all for the user from the time they start the app until their order is placed, so having anything happen dynamically inbetween is likely of little to no benefit. Basically caching as much as possible upfront is a better approach. Even if they have to wait 5s as the app loads, as long as there are zero delays afterwards, will likely lead to a better experience overall. And if he makes a PWA app running on iOS, there's a substantial built-in delay when the app loads anyway.

Hi andrew thank very much for your suggestions, very helpful BUT now i'm rearranging all form managent and now i ve this problem :

In the code below in form Login 2) i create the form main (that create others forms) after ALL creation i must frmMenuOrd.Call_MenuOrd(); ... now i've errors because form creation is async . How to wait for all creation ? i ve tried await(TForm2, newform.Load()); of multiform but no result !

Thx

1)
procedure TfrmMain.WebFormCreate(Sender: TObject);
begin

  Application.CreateForm(TfrmMenuOrd,  pnlMenuOrd.ElementID, frmMenuOrd, Nil);
  Application.CreateForm(TfrmDetProd,  pnlDetProd.ElementID, frmDetProd, Nil);
  Application.CreateForm(TfrmRiepOrd,  pnlRiepOrd.ElementID, frmRiepOrd, Nil);
  Application.CreateForm(TfrmConfOrd,  pnlConfOrd.ElementID, frmConfOrd, Nil);

end;
2)
procedure TfrmLogin.btSfogliaMenuClick(Sender: TObject);
  procedure AfterCreate(AForm: TObject);
  begin
    With TfrmMain(AForm) Do Begin
      frmMenuOrd.Call_MenuOrd();
    End;
  End;
begin
  Application.CreateForm(TfrmMain, 'body', frmMain, @AfterCreate);

end;


Not quite sure I understand your question. The best way, generally, to wait for something to happen is with await() if possible. So if there is a function or procedure defined with the [async] attribute, you can use await to call it and then continue on from there.

Another less desirable approach is to just use a TWebTimer. The general idea is something like this.

  1. Add a TWebTimer to your form. Call it, say, tmrFormCreation or something.
  2. Set a short interval, like 50ms.
  3. Set it to disabled by default.
  4. Call whatever procedures or functions that you want executed.
  5. Enable tmrFormCreation.

In tmrFormCreation's OnTimer event, you do something like the following.

  1. Check if what you're waiting for is ready. Like, do all the forms exist (are they assigned?) or has each form set some value in your main form that you can use to confirm that the thing your waiting for is ready? This check should be very quick and simple if possible. Maybe the forms load data, so you can set a flag once you've loaded the data.
  2. If it is ready, then disable the timer and then call whatever you want to do next.
  3. If it is not ready, then do nothing - the timer will fire again in 50ms and check if it is ready again.

This is kind of a busy-wait solution, which you don't particularly want to do. But if you set the timer to something reasonably closely matched to how long the wait actually is, then it works pretty well. What I mean is, as long as the event isn't firing hundreds of times, and as long as the check that it performs is only a few operations (like checking if something is assigned, or if an integer value is non-zero or something) then it works pretty well and is mostly harmless.

So in your code, the Timer would be checking for the existance of the forms created in TfrmMain, and if they've all been created, then it would call frmMenuOrd.Call_MenuOrd(). It might be that in the AfterCreate is where you first enable the timer.

In your frmMain WebFormCreate, you could add another AfterCreate procedure in the same way that you have in frmLogin, and it could just update a counter in that form. So the timer just checks if counter=4, and if it does, it disables the timer and then calls frmMenuOrd.Call_MenuOrd(). For example.

excuse me andrew the is no way to Await(Application.CreateForm(TfrmMenuOrd, pnlMenuOrd.ElementID, frmMenuOrd, Nil) ) in the frmMain.WebFormCreate ?

I don't know if you can use await there. Async/Await doesn't necessarily work particularly well in situations where the function already implements some kind of a callback mechanism as the function always returns right away anyway. But I wanted to be clear that it is still very much the preferred mechanism when it can be used as there are no delays in waiting for timers and no extra code being run while waiting.

Proper use of callbacks can also be another approach. So in this case, you could add an AfterCreate callback to the CreateForm(TfrmConfOrd) call and in that callback, call frmMenuOrd.Call_MenuOrd(). This might even be the best option for you. But if you change the order of the CreateForms or if any of them also create anything that has an async nature to it, then you might be better off using the timer approach anyway.

Lots of options. One of the reasons I added the TMemo to the demo I uploaded is to help show the order the function calls are made, so you can see that all the createforms are called before any of them complete, for example. So if you have a chain of forms creating sub-forms which create sub-sub-forms, then it can be pretty confusing about which forms are ready first. It is all deterministic I think, the form creation part, but if they're doing anything else, like loading data asynchronously, then whether or not a form is fully ready is likely not going to be determined strictly by asyc/await or callbacks on the createform calls.

Hi andrew goto on smartphone RISTORD by Megagest
and see now :slight_smile:

Follow my "italian" instructions. I think performance now is very high !

Ah, very good! Glad you managed to get it sorted out. If you've got a moment, maybe add a quick entry to this list that might be helpful for anyone else with a similar problem?

Yes sure, later i will trace it