Home General
New Blog Posts: Merging Reports - Part 1 and Part 2

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

Sign In or Register to comment.