EComponentError "A component named _1 already exists" in TdaJoinTableDialog when adding a second tab
Dear Digital Metaphors team,
Dear Digital Metaphors team,
while investigating a customer issue we tracked down a reproducible exception in the
DADE Query Designer. Below is the analysis, a reproduction recipe and a suggested fix.
## Environment
- ReportBuilder 23.04 Enterprise, RAD Studio 11 Alexandria (BDS 22.0), Win32, VCL
- DADE end-user reporting (TppReportExplorer / TppDesigner), Firebird via a custom TdaSession descendant
- Not driver specific - the failing code path is in the generic DADE dialogs
## Summary
In the Query Designer, adding a second table raises
```
EComponentError: A component named _1 already exists
```
as soon as the link/join dialog is about to be shown. The report cannot be created.
The same build worked with earlier ReportBuilder versions in the same application.
## Cause
`TdaJoinTableDialog.FormCreate` creates three list views at run time without assigning
a `Name`, all owned by the dialog form:
```
daJoinTableDlg.pas:186 FlvwFields := TppReportListView.Create(Self);
daJoinTableDlg.pas:201 FlvwJoinFields := TppReportListView.Create(Self);
daJoinTableDlg.pas:219 FlvwJoins := TppListView.Create(Self);
```
`TCustomListView` round-trips itself through a memory stream whenever its window handle
is recreated (`Vcl.ComCtrls.pas:19113`, `FMemStream.ReadComponent(Self)`).
`TReader.ReadRootComponent` then assigns a name:
```
System.Classes.pas:11798 Result.Name := FindUniqueName(ReadStr);
System.Classes.pas:11767 while not IsUniqueGlobalComponentName(Result) do
System.Classes.pas:11770 Result := Format('%s_%d', [Name, I]);
```
With an empty base name, `FindUniqueName` yields `_1`. `Initialize` builds `FlvwFields`
first, which therefore becomes `_1`; `LoadJoinFields` then builds `FlvwJoinFields`,
which requests `_1` as well - and the dialog form already owns a component with that name.
`FindUniqueName('')` only produces `_1` instead of keeping the empty name when
`IsUniqueGlobalComponentName('')` returns False. That is decided by
`Vcl.Forms.FindGlobalComponent` (`Vcl.Forms.pas:2717-2721`), which compares the requested
name against every form:
```pascal
if not (csInline in Result.ComponentState) and
(CompareText(Name, Result.Name) = 0) then Exit;
```
An empty search name matches **any form whose `Name` is empty**. Note the asymmetry:
`TComponent.FindComponent` deliberately returns `nil` for an empty name, while
`FindGlobalComponent` does not make that exception.
So the bug only surfaces when some unrelated form with an empty `Name` happens to be alive
in `Screen.Forms`. In our case that is `TDropDownForm` from the TMS VCL UI Pack - a popup
form legitimately created without a name. It never interacts with ReportBuilder; its mere
presence flips the global lookup.
## Call stack
```
System.Classes.TComponent.ValidateRename (System.Classes.pas:17367)
Vcl.Forms.TCustomForm.ValidateRename (Vcl.Forms.pas:4764)
System.Classes.TComponent.SetName (System.Classes.pas:17461)
System.Classes.TReader.ReadRootComponent (System.Classes.pas:11798)
System.Classes.TStream.ReadComponent (System.Classes.pas:9260)
Vcl.ComCtrls.TCustomListView.CreateWnd (Vcl.ComCtrls.pas:19113)
Vcl.Controls.TWinControl.CreateHandle (Vcl.Controls.pas:10036)
Vcl.ComCtrls.TListItems.BeginUpdate (Vcl.ComCtrls.pas:18027)
ppTreeVw.TppReportListView.SetCurrentObject (ppTreeVw.pas:4548)
daJoinTableDlg.TdaJoinTableDialog.LoadJoinFields(daJoinTableDlg.pas:733)
daJoinTableDlg.TdaJoinTableDialog.Initialize (daJoinTableDlg.pas:328)
daJoinTableDlg.TdaJoinTableDialog.EditTableJoin (daJoinTableDlg.pas:150)
daQueryDesigner.TdaTablePage.JoinClickEvent (daQueryDesigner.pas:3056)
```
## How to reproduce
1. In any VCL application using DADE, make sure a form with an empty `Name` exists in
`Screen.Forms`. A one-line stand-in is enough:
```pascal
with TForm.CreateNew(Application) do Name := '';
```
2. Open the Query Designer, add a table, then add a second table so that the link dialog
is shown.
## Suggested fix
Assign explicit names in `TdaJoinTableDialog.FormCreate`:
```pascal
FlvwFields.Name := 'lvwFields';
FlvwJoinFields.Name := 'lvwJoinFields';
FlvwJoins.Name := 'lvwJoins';
```
This is exactly the pattern ReportBuilder already uses in `daQueryDesigner.pas:1614`,
where `FBottomList` is given a name while `FTopList` (line 1566, same owner) is not.
## Other places with the same pattern
Run-time created list views without a `Name`, several of them sharing one owner:
- `daJoinPanel.pas:460, 491, 539` - `FlvwTable1Fields`, `FlvwTable2Fields`, `FlvwJoins`,
all `TppListView.Create(Self)` on the same owner. This is not merely a similar pattern:
it reaches the identical entry point. `LoadFieldsForTable` (line 762) starts with
`aListView.Items.BeginUpdate` (line 775) and is called for both list views in direct
succession (lines 663 and 674); `SelectTable` does the same at line 826. We expect the
same exception here as soon as a handle recreation occurs.
- `daQueryDesigner.pas:1566` - `FTopList`
- `daDataDictionaryBuilder.pas:1396`, `ppCTDsgn.pas:640` - single instances, no collision today
## Secondary issue
`TdaJoinTableDialog.EditTableJoin` (`daJoinTableDlg.pas:144-160`) calls `lDialog.Free`
without a `try/finally`. When `Initialize` raises, the dialog is never freed and stays as a
component of `Screen.ActiveForm`. It is destroyed later with the owner, and `FormDestroy`
(`daJoinTableDlg.pas:388`) then runs its unconditional cleanup over a half-initialised
object, producing a follow-up `EInvalidPointer` in `TCustomForm.DoDestroy`.
## Current workaround
Before opening the designer, we give every unnamed form in `Screen.Forms` a name, which
keeps `FindGlobalComponent('')` from matching. That removes the trigger but not the cause.
Best regards