Why fires the AfterScroll event of a nested (detail) dataset more than once when its master dataset scrolled to another record? How to deal with it? - delphi

I have a Delphi program with 3 successive nested (master-details) FireDAC datasets namely: SuperMaster, Master and Details dataset.
In the AfterScroll event of the Details dataset only, new records are inserted into the database.
My program logic needs the AfterScroll event of the Details dataset to fire only once: when any of the parent datasets (SuperMaster or Master) scrolled to another record. The AfterScroll event should not trigger more than once because this will cause my logic to insert wrong records.
The problem is that when scrolling occurs to any of the parent datasets (from the current record to another one, even to the adjacent one), i.e. moving a single record only, the Details dataset AfterScroll event fired more than once and sometimes more than twice!
I do not want to mess with the original FireDac classes by overriding its AfterScroll event to force it to do my logic only once, nor to add a Boolean flag to my logic so that it runs only once.
I tried to put all the tables into one view, but it costs me to change the program logic and make future updates more demanding.
I searched and read many articles and a book ("Delphi in Depth: FIREDAC" by Cary Jensen), but could not find a solution nor know why or how it occurs!
Why does this happen? I want to understand the basics please. Is there any elegant solution?

Short of changing the FireDAC source (which I think would be a thoroughly bad idea - beware of opening Pandora's box) I don't think your goal of avoiding the
use of a Boolean flag is achievable. It should however be trivial to achieve if you do use a Boolean flag.
I also wonder whether you observation that the Detail's AfterScroll event occurs more than once per detail dataset
is correct, if that is what you are saying. If it is, you should edit your q to include a minimal, reproducible example, preferably based on the code below.
TDataSet and its descendants, including all the FireDAC datasets operate to a very tightly designed
state machine which includes the handling of master-detail behaviour that's been tested by all the TDataSet
usage since Delphi was first released. Trying to mess with that would invite
disaster.
If you try the minimal VCL project below, I think you'll find it's not very hard to satisfy yourself that
the Detail's AfterScroll event behaves exactly as you would expect from the coding of TDataSet's and FireDAC's source.
Running the code you will find that the breakpoint in DetailAfterScroll trips 4 times before
the first breakpoint on MasterFirst. The next time the BP trips, observe the call stack via
View | Debug Windows | Call stack; if you look down the cal stack, you'll find that the call to Detail.AfterScroll
was ultimately called via the penultimate line of
procedure TFDMasterDataLink.DataEvent(Event: TDataEvent; Info: NativeInt);
An FDMasterDataLink is automatically created to handle the triggering of Detail data events based on
the operation of the Master. And that, really, is the end of the story. because however much you might
disagree with this behaviour , you can't really do anything about it short of using a Boolean flag
in your own code.
I think it would be wise to verify that DetailAfterScroll is only being called once per dataset that's
on the detail side of the Master. If it's happening more than once, it would be worth checking that
it isn't your own code (or linking together of DataSets) that's causing it.
As you'll see, nearly all of what the example does is defined in the OnCreate event
to avoid having to use the Object Inspector to set up the components, so that is should "just work".
Code:
type
TForm1 = class(TForm)
Master: TFDMemTable;
Detail: TFDMemTable;
DataSource1: TDataSource;
procedure FormCreate(Sender: TObject);
procedure DetailAfterScroll(DataSet: TDataSet);
public
DetailsScrolled : Integer;
end;
[...]
procedure TForm1.DetailAfterScroll(DataSet: TDataSet);
begin
// Place debugger breakpoint on the following line
Inc(DetailsScrolled);
end;
procedure TForm1.FormCreate(Sender: TObject);
var
AField : TField;
begin
AField := TIntegerField.Create(Self);
AField.FieldName := 'MasterID';
AField.DataSet := Master;
Master.CreateDataSet;
AField := TIntegerField.Create(Self);
AField.FieldName := 'DetailID';
AField.DataSet := Detail;
AField := TIntegerField.Create(Self);
AField.FieldName := 'MasterID';
AField.DataSet := Detail;
Detail.CreateDataSet;
DataSource1.DataSet := Master;
Detail.MasterSource := DataSource1;
Detail.MasterFields := 'MasterID';
Detail.IndexFieldNames := 'MasterID;DetailID';
Master.InsertRecord([1]);
Master.InsertRecord([2]);
Detail.InsertRecord([1, 1]);
Detail.InsertRecord([2, 1]);
Detail.InsertRecord([3, 2]);
// Place debugger breakpoint on EACH of the following three lines
Master.First;
Master.Next;
Master.First;
end;

Related

Delphi DBGrid date format for Firebird timestamp field

I display the content of a Firebird database into a TDBgrid. The database has a 'TIMESTAMP' data type field that I would like to display with date/time format:
'YYYY/MM/DD HH:mm:ss'. (Now its displayed as 'YYMMDD HHmmss')
How to achieve this?
I tried this:
procedure TDataModule1.IBQuery1AfterOpen(DataSet: TDataSet);
begin
TDateTimeField(IBQuery1.FieldByName('timestamp_')).DisplayFormat := 'YYYY/MM/DD HH:mm:ss';
end;
But this causes some side effects at other parts of the program, so its not an alternative. For example at the 'IBQuery1.Open' statement I get the '...timestamp_ not found...' debugger message in the method that I clear the database with.
function TfrmLogger.db_events_clearall: integer;
begin
result := -1;
try
with datamodule1.IBQuery1 do begin
Close;
With SQL do begin
Clear;
Add('DELETE FROM MEVENTS')
end;
if not Prepared then
Prepare;
Open; //Exception here
Close;
Result := 1;
end;
except
on E: Exception do begin
ShowMessage(E.ClassName);
ShowMessage(E.Message);
Datamodule1.IBQuery1.close;
end;
end;
end;
I get the same exception message when trying to open the query for writing into the database.
*EDIT >>
I have modified the database clear as the following:
function TfrmLogger.db_events_clearall: integer;
var
IBQuery: TIBQuery;
IBTransaction: TIBTransaction;
DataSource: TDataSource;
begin
result := -1;
//Implicit local db objects creation
IBQuery := TIBQuery.Create(nil);
IBQuery.Database := datamodule1.IBdbCLEVENTS;
DataSource := TDataSource.Create(nil);
DataSource.DataSet := IBQuery;
IBTransaction := TIBTransaction.Create(nil);
IBTransaction.DefaultDatabase := datamodule1.IBdbCLEVENTS;
IBQuery.Transaction := IBTransaction;
try
with IBQuery do begin
SQL.Text := DELETE FROM MSTEVENTS;
ExecSQL;
IBTransaction.Commit;
result := 1;
end;
except
on E : Exception do
begin
ShowMessage(E.ClassName + ^M^J + E.Message);
IBTransaction.Rollback;
end;
end;
freeandnil(IBQuery);
freeandnil(DataSource);
freeandnil(IBTransaction);
end;
After clearing the database yet i can load the records into the dbgrid, seems like the database has not been updated. After the program restart i can see all the records been deleted.
The whole function TfrmLogger.db_events_clearall seems very dubious.
You do not provide SQL_DELETE_ROW but by the answer this does not seem to be SELECT-request returning the "resultset". So most probably it should NOT be run by ".Open" but instead by ".Execute" or ".ExecSQL" or something like that.
UPD. it was added SQL_DELETE_ROW = 'DELETE FROM MEVENTS'; confirming my prior and further expectations. Almost. The constant name suggests you want to delete ONE ROW, and the query text says you delete ALL ROWS, which is correct I wonder?..
Additionally, since there is no "resultset" - there is nothing to .Close after .Exec.... - but you may check the .RowsAffected if there is such a property in DBX, to see how many rows were actually scheduled to be deleted.
Additionally, no, this function DOES NOT delete rows, it only schedules them to be deleted. When dealing with SQL you do have to invest time and effort into learning about TRANSACTIONS, otherwise you would soon get drown in side-effects.
In particular, here you have to COMMIT the deleting transaction. For that you either have to explicitly create, start and bind to the IBQuery a transaction, or to find out which transaction was implicitly used by IBQuery1 and .Commit; it. And .Rollback it on exceptions.
Yes, boring, and all that. And you may hope for IBX to be smart-enough to do commits for you once in a while. But without isolating data changes by transactions you would be bound to hardly reproducible "side effects" coming from all kinds of "race conditions".
Example
FieldDefs.Clear; // frankly, I do not quite recall if IBX has those, but probably it does.
Fields.Clear; // forget the customizations to the fields, and the fields as well
Open; // Make no Exception here
Close;
Halt; // << insert this line
Result := 1;
Try this, and I bet your table would not get cleared despite the query was "opened" and "closed" without error.
The whole With SQL do begin monster can be replaced with the one-liner SQL.Text := SQL_DELETE_ROW;. Learn what TStrings class is in Delphi - it is used in very many places of Delphi libraries so it would save you much time to know this class services and features.
There is no point to Prepare a one-time query, that you execute and forget. Preparation is done to the queries where you DO NOT CHANGE the SQL.Text but only change PARAMETERS and then re-open the query with THE SAME TEXT but different values.
Okay, sometimes I do use(misuse?) explicit preparation to make sure the library fetches parameters datatypes from the server. But in your example there is neither. Your code however does not use parameters and you do not use many opens with the same neverchanging SQL.text. Thus, it becomes a noise, making longer to type and harder to read.
Try ShowMessage(E.ClassName + ^M^J + E.Message) or just Application.ShowException(E) - no point to make TWO stopping modal windows instead of one.
Datamodule1.IBQuery1.close; - this is actually a place for rolling back the transaction, rather than merely closing queries, which were not open anyway.
Now, the very idea to make TWO (or more?) SQL requests going throw ONE Delphi query object is questionable per se. You make customization to the query, such as fixing DisplayFormat or setting fields' event handlers, then that query is quite worth to be left persistently customized. You may even set DisplayFormat in design-time, why not.
There is little point in jockeying on one single TIBQuery object - have as many as you need. As of now you have to pervasively and accurately reason WHICH text is inside the IBQuery1 in every function of you program.
That again creates the potential for future side effects. Imagine you have some place where you do function1; function2; and later you would decide you need to swap them and do function2; function1;. Can you do it? But what if function2 changes the IBQuery1.SQL.Text and function1 is depending on the prior text? What then?
So, basically, sort your queries. There should be those queries that do live across the function calls, and then they better to have a dedicated query object and not get overused with different queries. And there should be "one time" queries that only are used inside one function and never outside it, like the SQL_DELETE_ROW - those queries you may overuse, if done with care. But still better remake those functions to make their queries as local variables, invisible to no one but themselves.
PS. Seems you've got stuck with IBX library, then I suggest you to take a look at this extension http://www.loginovprojects.ru/download.php?getfilename=uploads/other/ibxfbutils.zip
Among other things it provides for generic insert/delete functions, which would create and delete temporary query objects inside, so you would not have to think about it.
Transactions management is still on you to keep in mind and control.

Delphi: Repaint form in FormShow

I open Form2.ShowModal in FormMain. I want the application to show Form2 intact while doing some database access (this is not about the new data to be shown). However, while FormShow is executed, just the outer border and some broken parts are displayed, some broken parts of FormMain show through. It's ugly.
I have not been able to find a way to make Delphi repaint the Form immediately and then doing the time-consuming MyOpenData procedure. After concluding MyOpenData everything is fine.
procedure TForm2.FormShow(Sender: TObject);
begin
Invalidate;
Refresh;
MyOpenData; { needs some seconds of database accesses }
end;
Alternative:
procedure TForm2.FormShow(Sender: TObject);
begin
Invalidate;
Refresh;
SendMessage(Handle, wm_paint, 0, 0);
PostMessage(Handle, wm_OpenMyData, 0, 0); { executes well, but no solution)
end;
This doesn't work either. I thought SendMessage() waits for the message being done. But no Paint is done before MyOpenData. The form always looks broken till the procedures finishes. Besides this, the routines are executed fine. I tried all these commands combined or separately.
What am I missing? Thanks in advance!
How do you start time-consuming routines that need to run when opening a form?
(Delphi XE7 on Windows 7 64 bit)
uses
WinApi.Windows;
const
WM_AFTER_SHOW = WM_USER + 1; // custom message
WM_AFTER_CREATE = WM_USER + 2; // custom message
private
procedure WmAfterCreate(var Msg: TMessage); message WM_AFTER_CREATE;
procedure WmAfterShow(var Msg: TMessage); message WM_AFTER_SHOW;
procedure TForm1.WmAfterCreate(var Msg: TMessage);
begin
DoSomeThingAfterCreate();
ShowMessage('WM_AFTER_CREATE received!');
end;
procedure TForm1.WmAfterShow(var Msg: TMessage);
begin
DoSomeThingAfterShow();
ShowMessage('WM_AFTER_SHOW received!');
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
// Some code...
PostMessage(Self.Handle, WM_AFTER_CREATE, 0, 0);
end;
procedure TForm1.FormShow(Sender: TObject);
begin
// Some Code...
PostMessage(Self.Handle, WM_AFTER_SHOW, 0, 0);
end;
There simply isn't enough information given to make any specific recommendations here, IMO.
I'm going to guess that MyOpenData() sets up some kind of data state that Form2 relies upon. If this is the case, then you probably want to call it BEFORE calling Form2.ShowModal. In no case should you be calling either invalidate or refresh inside of the OnShow handler because they both trigger OnShow.
Watch the video I made for CodeRage 9 entitled, "Have You Embraced Your Software Plumber Lately?" (search YouTube for 'coderage software plumber') as this is the exact topic I'm addressing in this video -- the whole plethora of issues involved in initializing forms and objects, along with the timing issues specific to forms.
I don't discuss issues with data-aware controls specifically, but they're pretty much the same. It can be problematic setting up the DB state from inside the form that depends upon that state in order to properly initialize itself. There's an inherent race-condition that is easily avoided by doing your dependent initialization first, then injecting that dependency into the form.
If DBs are involved, you have to inject SOMETHING into the form: either the DB reference (usually through a global variable); a table (either a global variable or a form variable); or a current record (usually a form variable). The nice part about using DB-aware controls is that the initialization is always implicit, and you don't have to inject anything. The bad part about using DB-aware controls is, the initialization is ALWAYS IMPLICIT, and you have no EXPLICIT control over initialization sequences. By doing EXPLICIT injection of DB dependencies, you side-step the timing issues. It's a little more work (not much), but you don't have to deal with stuff like this.
In any case, if the form needs a current record to initialize its fields, you can't display the form until the record has been selected, and you can't make the record selection part of the form's initialization process without risking concurrency issues. It can be done, but you're making one heck of a mess out of it.

Delphi: TreeView elements vs. Form position changing

We have found something is seems to be a bug(?), and cause bug in our code.
Delphi XE3, Win32. Two forms, the main have button:
procedure TForm4.Button1Click(Sender: TObject);
begin
with TForm1.Create(Application) do
begin
ShowModal;
Release;
end;
end;
The Form1 is doing this:
procedure TForm1.FormCreate(Sender: TObject);
var
i, j: integer;
mn: TTreeNode;
begin
for i := 1 to 10 do
begin
mn := TreeView1.Items.Add(nil, 'M' + IntToStr(i));
for j := 1 to 10 do
begin
TreeView1.Items.AddChild(mn, 'C' + IntToStr(j));
end;
end;
Position := poDesigned;
beep;
Caption := IntToStr(TreeView1.Items.Count);
end;
After this I get 0 elements in caption.
But when I have a button in that form with this code...
procedure TForm1.Button1Click(Sender: TObject);
begin
Caption := IntToStr(TreeView1.Items.Count);
end;
...
Then I can see the good number (110 elements).
If I write TreeView1.Handleneeded after Position changing, the count is also good.
The problem is based on RecreateWnd, which calls DestroyHandles.
But they will be repaired only in Show (in the Activate event I can see good result too).
TreeView is a special control, because the tree elements are children, and the count is calculated by on them, no matter it have real sub-object list.
The main problem that ReCreateWnd called often by another methods to, so it can cause problems in another sections too, and I cannot put HandleNeeded before all .Count calculation.
(We have special base form that correct the Position to poDesigned if it was poScreenCenter to it can be positionable later. This happens after FormCreate call, in an inner method. We found this problem only with these kind of forms, but later we could reproduce it in a simple code too)
So the question is - what is the global solution to this problem?
(Did you experience this in XE5 too?)
Thank you for all help, info, doc.
The Form's HWND is getting destroyed when you set the Position. That also destroys all child HWNDs. The TreeView's HWND has not been re-created yet when you read its Count, which is why it reports 0. Calling TreeView.HandleNeeded after setting the Position forces the TreeView to re-create its HWND immediately, which will re-load any TreeNodes that had beencached internally when the TreeView's HWND was destroyed (but only if the TreeView.CreateWndRestores property is True, which it is by default).
A TreeView stores its child nodes inside of its HWND. Reading the Items.Count merely asks the HWND how many nodes it has, so if there is no HWND then the Count will be 0. TTreeView does not keep its own list of TTreeNode objects, it merely assigns them as user-defined data in the physical nodes themselves. When nodes are removed from the tree, TTreeView frees the associated TTreeNode objects. In the case of HWND recreation, TTreeView caches the TTreeNode data and then re-assigns it back to new nodes when the HWND is re-created. But again, it does not keep track of the TTreeNode objects.
What TTreeView could have done is store the current number of nodes during HWND destruction, and then have Items.Count return that value if the HWND has not been re-created yet. But alas, TTreeView does not do that. But you could implement that manually by subclassing TTreeView to intercept the CreateWnd() and DestroyWnd() methods, and then write your own function that returns the actual Items.Count when HandleAllocated is true and returns your cached value if it is false.
If the Form is visible to the user then its HWND (and the HWND of its children) will be available, since a control is not visible without an HWND, so Items.Count will be available if the TTreeView is visible to the user. If its HWND is ever destroyed while visible, the VCL will re-create the HWND immediately so the user does not see a missing control. However, if the Form (or TreeView) is not visible to the user when the HWND is destroyed, the HWND will not be re-created until it is actually needed when the Form/TreeView is made visible again.
Since you are forcing the Position to always be poDesigned at the time of Form creation, why not just set the Position to poDesigned at design-time? Otherwise, you can simply set the Position before populating the TreeView instead of afterwards, at least:
procedure TForm1.FormCreate(Sender: TObject);
var
i, j: integer;
mn: TTreeNode;
begin
Position := poDesigned; // <-- here
for i := 1 to 10 do
begin
mn := TreeView1.Items.Add(nil, 'M' + IntToStr(i)); // <-- first call to Add() forces HWND recreation if needed
for j := 1 to 10 do
begin
TreeView1.Items.AddChild(mn, 'C' + IntToStr(j));
end;
end;
Beep;
Caption := IntToStr(TreeView1.Items.Count); // <-- correct value reported
end;
This happens in all versions of Delphi. This is simply how things work in the VCL.
On a side note, you should use Free() instead of Release(). Release() is meant to be used only when a Form needs to Free() itself, such as in an event handler, by delaying the Free() until the Form becomes idle. Since the form is already closed and idle by the time ShowModal() exits, it is safe to Free() the Form immediately.
I think you are over-reacting. You state:
The main problem that ReCreateWnd called often by another methods to, so it can cause problems in another sections too, and I cannot put HandleNeeded before all .Count calculation.
But then in comments you say that these other scenarios are:
CMCtlD3Changed CMSysColorChange BORDERSTYLE SetAxBorderStyle SetBorderIcons Dock SetPosition SetPopupMode set_PopupParent RecreateAsPopup ShowModal SetMainFormOnTaskBar
Indeed these methods will cause window recreation. But why does that matter for you? Do you routinely assign to MainFormOnTaskBar and then immediately request the count of items in your tree view? Do you change Ctl3D regularly? Are you changing BorderIcons dynamically? I very much doubt it.
So I think you need to tackle the immediate problem which relates to the timing of the setting of Position. I would deal with that by making sure that Position is set before you populate the tree view. Do that by either setting Position at design time, or simply before you populate the tree view.
Of course, there may well be other problem related to window creation. You'll need to treat them on a case by case basis. I suspect that you are hoping for some sort of magic switch that just makes this issue disappear. I don't believe that there is one. If you try to read the item count before the handle is created, then you will suffer from this problem. The solution is not to read the item count before the handle is created.

Delphi. How to Disable/Enable controls without triggering controls events

I have a DataSet (TZQuery), which has several boolean fields, that have TDBCheckBoxes assigned to them.
These CheckBoxes have "OnClick" events assigned to them and they are triggered whenever I change field values (which are assigned to checkboxes).
The problem is that I do not need these events triggerred, during many operations i do with the dataset.
I've tried calling DataSet.DisableControls, but then events are called right after i call DataSet.EnableControls.
So my question is - is there a way to disable triggering Data-aware controls events.
Edit (bigger picture):
If an exception happens while let's say saving data, i have to load the default values (or the values i've had before saving it). Now while loading that data, all these events (TDBCheckBoxes and other data-aware controls) are triggered, which do all sorts of operations which create lag and sometimes even unwanted changes of data, i'm looking for an universal solution of disabling them all for a short period of time.
Building on Guillem's post:
Turn off everything:
Traverse each component on the form with the for-loop, shown below, changing the properties to the desired value.
If you want to later revert back to the original property values, then you must save the original value (as OldEvent is used below.)
Edit: The code below shows the key concept being discussed. If components are being added or deleted at run-time, or if you'd like to use the absolutely least amount of memory, then use a dynamic array, and as Pieter suggests, store pointers to the components rather than indexing to them.
const
MAX_COMPONENTS_ON_PAGE = 100; // arbitrarily larger than what you'd expect. (Use a dynamic array if this worries you.
var
OldEvent: Array[0.. MAX_COMPONENTS_ON_PAGE - 1] of TNotifyEvent; // save original values here
i: Integer;
begin
for i := 0 to ComponentCount - 1 do
begin
if (Components[i] is TCheckBox) then
begin
OldEvent[i] := TCheckBox(Components[i]).OnClick; // remember old state
TCheckBox(Components[i]).OnClick := nil;
end
else if (Components[i] is TEdit) then
begin
OldEvent[i] := TEdit(Components[i]).OnClick; // remember old state
TEdit(Components[i]).OnClick := nil;
end;
end;
Revert to former values
for i := 0 to ComponentCount - 1 do
begin
if (Components[i] is TCheckBox) then
TCheckBox(Components[i]).OnClick := OldEvent[i]
else if (Components[i] is TEdit) then
TEdit(Components[i]).OnClick := OldEvent[i];
end;
There may be a way to fold all of the if-statements into one generic test that answers "Does this component have an OnClickEvent" -- but I don't know what it is.
Hopefully someone will constructively criticize my answer (rather than just down voting it.) But, hopefully what I've shown above will be workable.
One way to do this is following:
var
Event : TNotifyEvent;
begin
Event := myCheckbox.OnClick;
try
myCheckbox.OnClick := nil;
//your code here
finally
myCheckbox.OnClick := Event;
end;
end;
HTH
The internal design of the TCustomCheckBox is that it triggers the Click method every time the Checked property if changed. Be it by actually clicking it or setting it in code. And this is happening here when you call EnableControls because the control gets updated to display the value of the linked field in your dataset.
TButtonControl (which is what TCustomCheckBox inherits from) has the property ClicksDisabled. Use this instead of (or in addition to) the DisableControls/EnableControls call. Unfortunately it is protected and not made public by TCustomCheckBox but you can use a small hack to access it:
type
TButtonControlAccess = class(TButtonControl)
public
property ClicksDisabled;
end;
...
TButtonControlAccess(MyCheckBox1).ClicksDisabled := True;
// do some dataset stuff
TButtonControlAccess(MyCheckBox1).ClicksDisabled := False;
Of course you can put this into a method that checks all components and sets this property if the control inherits from TCustomCheckBox or some other criteria.

accessing components of frame1 with frame2, with frame2 being on frame1, in delphi, frames are created dynamically

Originally the question had to be how to access the components in the first place, however I somehow managed to figure it out. I am just learning Delphi so I am prone to dumb and obvious questions. I am also at a stage when I'm not actually writing anything useful, but just messing with random stuff to see how it works and maybe learn something.
Wall of text incoming, i want to explain what i am exploring at the moment...
Basically i have a form1 with a button1, pressing it creates a frame2, that frame2 has a button2, pressing button2 creates a frame3 within frame2 (it being frame3's parent and owner). Each frame has an another freeandnil-button. Upon pressing each button1/2/3, it gets disabled to prevent creating multiple instances. My original problem was that after using freeandnil-button, I couldnt access the button on the previous frame (it worked fine for forms, form1.button1.enabled:=true works fine from within frame2) that got disabled in order to re-enable it (frame2.button1.enabled:=true from within frame3 creates an access violation, I think).
Suppose I write something in the future that requires such communication? So I added an editbox to each frame, with a button on the other to change the editbox text, this is my current working solution:
procedure TFrame2.Button3Click(Sender: TObject);
var i,z:integer;
begin
for i := 0 to ComponentCount - 1 do
if components[i] is tframe3 then
for z := 0 to (components[i] as tframe3).ComponentCount - 1 do
if (components[i] as tframe3).Components[z] is TEdit then
((components[i] as tframe3).Components[z] as TEdit).Text:='ping';
end;
and
procedure TFrame3.Button3Click(Sender: TObject);
var i:integer;
begin
for i := 0 to parent.ComponentCount-1 do
if parent.components[i] is TEdit then
(parent.Components[i] as TEdit).Text:='pong';
end;
If I have a bunch of editboxes or whatever the hell, I suppose I could use Tag property to identify them, however, this much component counting and passing something AS something doesn't really look right or efficient enough to me.
My questions at the moment are: can it be done in a better way? and can someone provide the reasoning why cant I access "parent-frame" components from a "child-frame" in a dumb way (ie: frame2.button1.enabled:=true from within frame3)?
A couple of points:
Controls/Components are usually set to be owned by the form/frame that controls their lifetime and parented to the control that shows them. So when you create a TEdit to sit on a TPanel on a TForm, you would set TForm as the owner and TPanel as the parent of the TEdit. With frames you do the same: the frame is the owner of all controls on it, controls are parented to whatever container on the frame (can be the frame itself) is holding and and is thus reponsible for showing (painting) it.
When you iterate over the children of a control, iterating over Components uses the Owner relation; iterating over Controls uses the Parent relation. So your code above would already do a lot less if it were to iterate over the Controls.
Of course you can reference other controls in a "dumb" way, but you will have to provide for the access method. If you want others (not just child frames) you will at the very least have to declare a method to retrieve that control. Many ways to do this. One is to "ask" the form when you need it (using a method on the form), or have the form set a property on the frame when it is created...
Avoid accessing form's and frame's published properties. They are mainly there for Delphi's streaming system. When you tie your application together using your approach described above it can very soon become one incredibly tangled mess...
Example coming up (sometime this evening), it will take me a bit of time to also explain and I have work to do...
The following example deals only with the ping-pong game between the frames. Freeing any form or frame from one of its own event handlers is not such a good idea. Use the Release method for that as it prevents the form/frame from processing messages after is was freed. (Plenty of questions about this on SO by the way). Also, when you do release a frame from one of its own buttons, you will need to take care that the frame that created it has a chance to nil the references it may have held to that frame otherwise you are setting yourself up for some interesting to debug mayhem. Look into "Notification" and "NotifyControls" and the automatic notification by forms and frames that is sent to their owner/parent so these can remove the control from their components/controls collection. In your example, if you were to release Frame3 from its own "FreeAndNil" button's OnClick event handler, you would have to make sure that the Frame2 responds to the deletion (I think) notification message and nil's any references it holds to Frame3 (besides the ones that will already be cleared automatically in the components/controls collections).
Now, the ping pong game. There are a couple of ways to go about this.
Method 1
The first way is what you already tried with a loop over the components of the other frame. While it certainly is a way to avoid having to "use" the other frame, it is cumbersome and not very concise. Plus when you get more controls on your forms/frames, you will have to add a check on the name to know that you have the correct TEdit. And then you might just as well use the name directly, especially as one frame already has the other frame in its uses clause because it is creating it.
// PingFrame (your Frame2)
uses
...
Pong_fr;
type
TPingFrame = class(TFrame)
...
procedure CreateChildBtnClick(Sender: TObject);
procedure PingPongBtnClick(Sender: TObject);
private
FPong: TPongFrame; // This is the "extra" reference you need to nil when
// freeing the TPongFrame from one of its own event handlers.
...
end;
procedure TPingFrame.CreateChildBtnClick(Sender: TObject);
begin
CreateChildBtn.Enabled := False;
FPong := TPongFrame.Create(Self);
FPong.Parent := ContainerPnl;
FPong.Align := alClient;
end;
procedure TPingFrame.PingPongBtnClick(Sender: TObject);
begin
if Assigned(FPong) then
FPong.Edit1.Text := 'Ping';
end;
And on the other end:
// PongFrame (your Frame3)
type
TPongFrame = class(TFrame)
...
procedure PingPongBtnClick(Sender: TObject);
end;
implementation
uses
Ping_fr;
procedure TPongFrame.PingPong1BtnClick(Sender: TObject);
begin
(Owner as TPingFrame).Edit1.Text := 'Pong called';
end;
This method seems all nice and dandy, but it has drawbacks:
You need to use the unit that holds the TPingFrame (Ping_fr in this example). Not too bad I guess, but... as Ping_fr already uses Pong_fr (the unit holding TPongFrame), you can't have Ping_fr using Pong_fr in the interface section and have Pong_fr using Ping_fr in the interface section as well. Doing so would have Delphi throwing an error because of a circular references. This can be solved by moving one of the uses to the implementation section (as done in the example for the use of Ping_fr in the Pong_fr unit.
A bigger drawback is that there is now a very tight coupling between the two frames. You cannot change the TEdit in either one to another type of control (unless that happens to have a Text property as well) without having to change the code in the other unit as well. Such tight coupling is a cause of major headaches and it is good practice to try and avoid it.
Method 2
One way to decrease the coupling between the frames and allow each frame to change it controls as it sees fit is not to use the controls of another form/frame directly. To do so you declare a method on each frame that the other can call. Each method updates its own controls. And from the OnClick event handlers you no longer access the other frame's controls directly, but you call that method
type
TPingFrame = class(TFrame)
...
public
procedure Ping;
end;
implementation
procedure TPingFrame.PingPongBtnClick(Sender: TObject);
begin
if Assigned(FPong) then
FPong.Ping;
end;
procedure TPingFrame.Ping;
begin
Edit1.Text := 'Someone pinged me';
end;
And on the other end:
type
TPongFrame = class(TFrame)
...
public
procedure Ping;
end;
implementation
procedure TPongFrame.PingPongBtnClick(Sender: TObject);
begin
(Owner as TPingFrame).Ping;
end;
procedure TPongFrame.Ping;
begin
Edit1.Text := 'Someone pinged me';
end;
This is better than Method 1 as it allows both frames to change their controls without having to worryabout "outsiders" referencing them, but still has the drawback of the circular reference that can only be "solved" by moving one "use" to the implementation section.
It is a good practice to try and avoid circular references altogether instead of patching them by moving units to the implementation section's uses clause. A rule of thumb I use is:
Any form/frame may know and use the public interface of the forms/frames it instantiates (though it should avoid the controls in the "default visibility" part of that interface), but no form/frame should not have any knowledge of the specific form/frame that created it.
Method 3
One way of achieving this (there are many) it to use events just like the TButton's OnClick event.
On the TPongFrame side of things you can remove the use of Ping_fr. You no longer need it.
Then you need to declare a property and the field it references and declare a method to fire the event.
type
TPongFrame = class(TFrame)
private
FOnPingPongClicked: TNotifyEvent;
protected
procedure DoPingPongClicked;
public
property OnPingPongClicked: TNotifyEvent
read FOnPingPongClicked write FOnPingPongClicked;
end;
The Ping method stays and its implementation is unchanged. The PingPongBtnClick event handler also stays, but its implementation now becomes:
procedure TPongFrame.PingPongBtnClick(Sender: TObject);
begin
DoPingPongClicked;
end;
procedure TPongFrame.DoPingPongClicked;
begin
if Assigned(FOnPingPongClicked) then
FOnPingPongClicked(Self);
end;
You could put the code from DoPingPongClicked straight in here, but it is a good practice to fire an event in a separate method. It avoids duplicating the exact same code if you would have an event that can be fired from multiple parts of your code. And it also allows descendants (when you get those) to override the "firing" method (you'll have to mark it virtual in the ancestor) and do something specific all the times the event is fired.
On the TPingFrame side of things you need to code a handler for the new event:
type
TPingFrame = class(TFrame)
...
protected
procedure HandleOnPingPongClicked(Sender: TObject);
Note that the signature of this handler needs to be what the TNotifyEvent specifies. And of course you need to ensure that this event handler gets called when the event fires in TPongFrame:
procedure TPingFrame.CreateChildBtnClick(Sender: TObject);
begin
CreateChildBtn.Enabled := False;
FPong := TPongFrame.Create(Self);
FPong.Parent := ContainerPnl;
FPong.Align := alClient;
// Listen to event fired by PongFrame whenever it's PingPingBtn is clicked
FPong.OnPingPongClicked := HandleOnPingPongClicked;
end;
In the handler code you can do what you need to do. It can be general:
procedure TPingFrame.HandleOnPingPongClicked(Sender: TObject);
begin
Edit1.Text := 'OnPingPongClicked event was fired';
end;
And of course you can also use the fact that the event passes a reference to the instance firing the event as well:
procedure TPingFrame.HandleOnPingPongClicked(Sender: TObject);
var
Pong: TPongFrame;
begin
// This checks that Sender actually is a TPongFrame and throws an exception if not
Pong := Sender as TPongFrame;
// Use properties from the Pong instance that was passed in
Edit1.Text := 'OnPingPongClicked event was fired by ' + Pong.Name;
end;
Enjoy!

Resources