Jump to content
Sign in to follow this  
jarto

Waiting for controls to be ready

Recommended Posts

Waiting for controls to be ready for action is an interesting topic, which resurfaces from time to time. It's pretty challenging as there is not one simple right way to handle the problem.

Try this little test program. It creates a panel which has three labels

unit Form1;

interface

uses 
  System.Types, System.Types.Convert, System.Objects, System.Time,
  System.IOUtils, System.Device.Storage,
  SmartCL.System, SmartCL.Time, SmartCL.Graphics, SmartCL.Components,
  SmartCL.FileUtils, SmartCL.Device.Storage, SmartCL.Forms, SmartCL.Fonts,
  SmartCL.Theme, SmartCL.Borders, SmartCL.Application, SmartCL.Controls.Panel,
  SmartCL.Controls.Label;

type
  TForm1 = class(TW3Form)
  private
    {$I 'Form1:intf'}
  protected
    procedure InitializeForm; override;
    procedure InitializeObject; override;
    procedure Resize; override;
  end;

  TTestPanel = class(TW3Panel)
  protected
    procedure InitializeObject; override;
    procedure ObjectReady; override;
  end;

implementation

{ TForm1 }

procedure TForm1.InitializeForm;
begin
  inherited;
  // this is a good place to initialize components
  var Pan:=TTestPanel.Create(Self);
  WriteLn('Panel created');
  Pan.SetBounds(0,0,200,150);
  WriteLn('Panel SetBounds done -> Creating Label3');
  var Lab3:=TW3Label.Create(Self);
  Lab3.Caption:='Can you see me three?';
  Lab3.SetBounds(10,70,150,30);
  WriteLn('All done');
end;

procedure TForm1.InitializeObject;
begin
  inherited;
  {$I 'Form1:impl'}
end;
 
procedure TForm1.Resize;
begin
  inherited;
end;

{ TTestPanel }

procedure TTestPanel.InitializeObject;
begin
  inherited;
  WriteLn('InitializeObject called -> Creating Label');
  var Lab:=TW3Label.Create(Self);
  Lab.Caption:='Can you see me?';
  Lab.SetBounds(10,10,150,30);
  WriteLn('InitializeObject done');
end;

procedure TTestPanel.ObjectReady;
begin
  inherited;
  WriteLn('ObjectReady called -> Creating Label2');
  var Lab2:=TW3Label.Create(Self);
  Lab2.Caption:='Can you see me too?';
  Lab2.SetBounds(10,40,150,30);
  WriteLn('ObjectReady done');
end;

initialization
  Forms.RegisterForm({$I %FILE%}, TForm1);
end.

When you run this, we get the following debug:

InitializeObject called -> Creating Label  [line #472]
InitializeObject done  [line #472]
Panel created  [line #472]
Panel SetBounds done -> Creating Label3  [line #472]
All done  [line #472]
ObjectReady called -> Creating Label2  [line #472]
ObjectReady done  [line #472]

All three labels are created but only two are visible. Fire up DevTools and you'll see that the first label (the one created at InitializeObject) does not have a size - even though we did use SetBounds on it.

The reason this happens is inside of SetBounds where the code adjusts left, top, width and height according to the object's margins and parent's padding. The SetBounds happens too early, so the computed styles for the element return NaN as values.

Now, there is a simple trick to fix this: Change w3_getPropertyAsInt to return 0 if the value is NaN. I'll surely do that but that won't solve the whole problem. If the styles actually had margins or the parent has padding, we will still move the control into wrong position.

Another way to prevent this problem is to not create subcontrols inside InitializeObject and only do that in ObjectReady. And yes, this does work most of the time. However, once you start creating more and more complicated apps, the chance of you accessing or changing a not-initialized element increases.

There are some ways to wait. You can actually create that 1st label inside InitializeObject lilke this:

procedure TTestPanel.InitializeObject;
begin
  inherited;
  WriteLn('InitializeObject called -> Creating Label');
  var Lab:=TW3Label.Create(Self);

  TW3Dispatch.WaitFor([Lab], procedure
    begin
      WriteLn('Wait done!');
      Lab.Caption:='Can you see me?';
      Lab.SetBounds(10,10,150,30);
    end);

  WriteLn('InitializeObject done');
end;

Here we create the Label early but wait for it to be ready before setting the propeties. However, code like this is not pretty, which made me think about various ways of solving this in a more beautiful way. When we've discussed this within the team, we've been thinking about using a Promise or a MutationObserver. But before we get there, lets have a look at when an element is actually ready for action:

The code in SetBounds actually does properly check the parent's handle:

      if (Parent.Handle) then
      begin
        var PCSHandle:=TW3CustomBrowserAPI.GetComputedStylesFor(Parent.Handle);
        if (PCSHandle) then
        begin
          LLeft+=w3_getPropertyAsInt(PCSHandle,'paddingLeft');
          LTop+=w3_getPropertyAsInt(PCSHandle,'paddingTop');
        end;
      end;

However, that does not stop paddingLeft from being returned as NaN for the 1st label. Testing it like this works:

if (Parent.Handle) and (Parent.Handle.Ready) then

Digging deeper, we'll find that TW3Dispatch.WaitFor checks also the display and visibility of the computed styles. One page I found ( https://browsee.io/blog/puppeteer-how-to-check-visibility-of-an-element/ ) even checks element.boxModel

So, the challenge is to find a beautiful and simple way for us to wait for the objects to be ready. And as we need to check more thant just the Handle, I wonder if it's even possible to solve this with a Promise or a MutationObserver.

 

 

 

Share this post


Link to post
Share on other sites

To make this work using MutationObservers I would probably do something like this: demo, project

Essentially the TestPanel is put here under the surveillance of a MutationObserver, which triggers when all changes to the panel have been applied (including the original sizing). After that it is ready to accept the label etc.

procedure TTestPanel.InitializeObject;
begin
  inherited;
  WriteLn('InitializeObject called -> Creating Label');
  self.observe;
  self.handle.addEventListener('readytogo', procedure(e:variant)
  begin
    writeln('heard readytogo');
    var Lab:=TW3Label.Create(Self);
    Lab.Caption:='Can you see me?';
    Lab.SetBounds(10,10,150,30);
    WriteLn('InitializeObject done');
  end);
end;

The code in this demo works, but is a bit ugly. It would be better to include the observe method standard in a constructor, put it in the TControlHandle rather than the TPanel, rewrite the eventlistener similar to the standard ReadyExecute method etc. But hey, demo.

As a matter of fact, using the standard ReadyExecute method instead of MutationObservers works here as well.  I think the latter is a bit better though as observers don't rely on timeouts and a single observer does handle multiple changes.

/*
  self.Handle.ReadyExecute(procedure()
  begin
    var Lab:=TW3Label.Create(Self);
    Lab.Caption:='Can you see me?';
    Lab.SetBounds(10,10,150,30);
    WriteLn('InitializeObject done');
  end);
*/

Maybe the rtl could be made to handle all child inserts in this manner

 

 

 

Share this post


Link to post
Share on other sites

The ReadyExecute uses timeouts and it does an even weaker check than TW3Dispatch.WaitFor. I suppose it'd be a good idea to unify these to use the same methods.

I had a little stab at testing MutationObservers. The RTL supports them in SmartCL.Observer.pas

When the Observer reports the new control, it is already ready for action, which is nice. However, in my tests the current method we use in the RTL (ReadySync using timeouts) is faster.

Share this post


Link to post
Share on other sites

Some interesting decisions to be made. 

side note : the native/shoestring rtl uses MutationObservers exclusively, which worked well for me. Interested to see what the speed difference is 

Share this post


Link to post
Share on other sites
4 hours ago, lynkfs said:

Some interesting decisions to be made. 

side note : the native/shoestring rtl uses MutationObservers exclusively, which worked well for me. Interested to see what the speed difference is 

I've used a test case where I create a panel which creates a about a thousand labels/checkboxes/divs inside itself, sets captions and resizes them. It gives the browser quite a lot of work to do.

Share this post


Link to post
Share on other sites

Time to make a small update here. The aim I have here is to make it as easy as possible to wait properly and to make this a standard way of creating components and setting properties. When we have that, we can improve the Visual Designer tremendously.

Here's my latest suggestion:

    Pan:=TW3Panel.Create(Self);
    Pan.OnObjectReady:=lambda
      Pan.SetBounds(10,500,200,200);
      //... and any other properties
      Lab:=TW3Label.Create(Pan);
      Lab.OnObjectReady:=lambda
        Lab.SetBounds(10,10,150,30);
        Lab.Caption:='Hello world!';
        //... and any other properties
      end;
    end;

The idea is to always use that OnObjectReady to initialize controls. Especially in code that the compiler generates for visual components. Looks pretty clean and neat to me.

Share this post


Link to post
Share on other sites
1 hour ago, lynkfs said:

could the 'readyexecute' method be repurposed so that code changes to existing projects could be avoided ?

Sure, but this does not force any existing code to be changed. Anyone creating controls by code can continue to use any method.

This new method I'm working on is mainly for code that the compiler generates and adds to the form's InitializeForm -section.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

×