0

The good (and bad) old Delphi taught us the "classic" way of building application because of the way we write code "behind" the IDE.

Based on this paradigm, I built some time ago a library that allows me to save/load the GUI to INI file with a single line of code.

LoadForm(FormName)

BAM! That's it! No more INI files!

The library only saves "relevant" properties. For example, for a TCheckBox it saves only its Checked property not also its Color or Top/Left. But it is more than enough.

Saving the form has no issues. However, I have "problems" during app initialization. They are not quite problems, but the initialization code is not that nice/elegant.

For example when I assign Checkbox1.Checked := True it will trigger the OnClick event (supposing that the checkbox was unchecked at design time). But assigning a False value (naturally) will not trigger the OnClick event.

Therefore, I have to manually call CheckBox1Click(Sender) after SaveForm(FormName) to make sure that whatever code is in CheckBox1Click, it gets initialized. But this raises another problem. The CheckBox1Click(Sender) might be called twice during application start up (once by SaveForm and once my me, manually).

Of course, in theory the logic of the program should be put in individual objects and separated from the GUI. But even if we do this, the initialization problem remains. You load the properties of the object from disk and you assign them to the GUI. When you set whatever value you have in your object to Checkbox1, it will (or not) call CheckBox1Click(Sender) which will set the value back into the object.

On app startup:

procedure TForm.FormCreate (Sender: TObject);
begin
  LogicObject.Load(File); // load app logic
  Checkbox1.Checked := LogicObject.Checked; // assign object to GUI
end;

procedure TForm.CheckBox1Click(Sender: TObject);
begin
  LogicObject.Checked := Checkbox1.Checked; 
end;

Probably the solution involves writing stuff like this for EVERY control on the form:

OldEvent := CheckBox1.OnClick;
CheckBox1.OnClick := Nil;
CheckBox1.Checked := something;
CheckBox1.OnClick := OldEvent;

Not elegant.

Question:
How do you solve this specific problem OR what is your approach when saving/restoring your GUI to/from disk?

Triber
  • 1,525
  • 2
  • 21
  • 38
Gabriel
  • 20,797
  • 27
  • 159
  • 293
  • 1
    It may be that this is what [ComponentState=csDesigning](http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Classes.TComponent.ComponentState) is for. – Stijn Sanders Jan 10 '18 at 10:09
  • ComponentState is readonly! How can I make use of it, in my case? – Gabriel Jan 10 '18 at 10:23
  • Solve this problem by separating the GUI and the business logic. In other words, don't design your interface the way the IDE leads you, but do it properly. – David Heffernan Jan 10 '18 at 10:56
  • 2
    Why would executing events be 'wrong'? What if the `CheckBox1Click` event will disable another control based on the Checkboxes's `Checked` property? How would your library handle that? – nil Jan 10 '18 at 11:19
  • 1
    @nil - CheckBox1Click could execute some code that does something with another control that was not YET initialized ! – Gabriel Jan 10 '18 at 12:12
  • @Alaun - That seems implausible, you're loading the setup at form create - all controls are created. I'd be interested in a mcve that provides some ground to your answer to nil. – Sertac Akyuz Jan 10 '18 at 13:10
  • @SertacAkyuz - During initialization, the LogicObject was fully loaded. Now it has to set up the GUI. First it sets Checkbox1.Checked to True. This calls Checkbox1Click. Checkbox1Click reads the value of Checkbox2, which was not YET initialized. As Triber said, the Checkbox1Click should not be executed while the app is initializing. – Gabriel Jan 10 '18 at 13:21
  • Then initialize checkbox2 before checkbox1. As far as I understand this is a weakness in your library. Correct the problem there, where it belongs, do not bloat code elsewhere. – Sertac Akyuz Jan 10 '18 at 13:28
  • @SertacAkyuz - If you have a GUI with over 50 controls on it, the inter-dependencies between controls could be strong. – Gabriel Jan 10 '18 at 13:31
  • @SertacAkyuz - How do you save your GUI to file? Which approach do you use? – Gabriel Jan 10 '18 at 13:32
  • @Alaun - I leave it to built-in dfm streaming, I use frames/panels/sheets where there is some complexity and when there is a form that can change its appearance at run time. I use the registry to save the states of Gui controls. Clicking the checkbox, in this scenario, is necessary to complete initialization. Some controls do not take action when their state changes (e.g. if the checkbox wouldn't click itself), in that case I write code that does the click if the read state requires it. – Sertac Akyuz Jan 10 '18 at 13:38
  • I think I understand why the time of event execution is important for you. What I don't completely understand is what you mean with _saving the GUI_. You aren't interested in things like colors or positions, you said. What exactly are you saving? The 'data', like checkbox states, editor texts? – nil Jan 10 '18 at 13:40
  • 2
    Why do you insist on doing it the wrong way. Separate GUI and logic and data. Write a GUI that presents a view of the data. There's no best practise for what you are doing. – David Heffernan Jan 10 '18 at 14:28

1 Answers1

2

This is one of the things which botthered me in some components from the beginning. What I know the are 3 options, except separating GUI and the business logic as @David said, which is not always an option.

  1. As you wrote above, always unassign the events so they don't get triggered
  2. Use non-triggered events such as OnMouseDown or OnMouseUp
  3. Or a similar solution that I use and I think is the most elegant

Create a global variable FormPreparing which is set during initialization and check its value at the beginning of the events like below.

procedure TForm.FormCreate (Sender: TObject);
begin
  FormPreparing := True;
  try    
    LogicObject.Load(File); // load app logic
    Checkbox1.Checked := LogicObject.Checked; // assign object to GUI
  finally
    FormPreparing := False;
  end;
end;

procedure TForm.CheckBox1Click(Sender: TObject);
begin
  if FormPreparing then
    Exit;
  LogicObject.Checked := Checkbox1.Checked;
end;
Triber
  • 1,525
  • 2
  • 21
  • 38
  • Why is "separating the GUI and the business logic" not always an option? What are non-triggered events? ISTM that OnMouseDown and OnMouseUp are triggered events too. – Rudy Velthuis Jan 10 '18 at 15:01
  • 1
    When I have complex legacy form with bad design, it's not the best idea to recreate him from scrach just to implement some function, or there is not enough time. – Triber Jan 10 '18 at 16:01
  • 1
    About events, I meant non-triggered based on situation. If you use `OnMouseUp` instead of `OnClick` with `TCheckBox` then this problem won't happen. – Triber Jan 10 '18 at 16:02
  • You mean that changing the value of Checked causes OnClick to be called. OK. – Rudy Velthuis Jan 10 '18 at 16:14
  • 2
    Ok, legacy code is different. But I don't accept the "there is no time to do things properly" argument. – Rudy Velthuis Jan 10 '18 at 16:17