Tweaking DFM Loading

One of the most shown feature of Delphi is its Design Editor with its ability to design forms and connect components in an easy and intuitive way. I can’t count the occasions where I have seen a form being decorated with some data controls connected to a TDatasource, which itself is connected to a TDataset descendant (see the docwiki).

Often these controls are placed on the form, but that approach is usually not recommended. While the data controls must reside on the form and the data source(s) should better be on the form, too (I will explain later why), the TDataSet descendants are better placed on a separate data module. This allows the separation of the data logic from the UI and (hopefully) encourages reuse.

This simple example has two data modules and two forms:

    • The data module TDbConnectDM contains the connection component and the wait cursor component required by FireDAC.
    • The second data module TMainDM has the dataset, a TFDQuery with its static fields, and an action with its action list. The query¬† is connected to the TFDConnection from DBConnectDM.
    • The form TMainForm introduces a data source connected to the query from MainDM and a TDBGrid and TDBNavigator connected to the data source. A simple TButton to open an instance of the second form (TEditForm) completes the controls on this form.
    • The remaining form TEditForm also has a data source connected to the query from MainDM, a couple of TDBEdit controls connected to the data source and configured for the different fields of the query and a TButton connected to the action of MainDM.

The real magic happens during runtime where all these connections are re-established when the forms and data modules are instantiated.

The connections between all these components are responsible for the lack of actual code to write. The only code in all data modules and forms is this:

Not sure about you, but selecting a record, clicking Edit Record, closing the dialog before selecting the next record seems not very comfortable to me. Let’s change from a modal Edit window to a non-modal one working always on the current record.

Turns out that is even a bit simpler: We add the Edit Form to the auto-create forms of the application and reduce the button click event to simply show that form:

So what? Nothing new here up to now. I guess, most of you have written similar applications since ages. OK, let’s get a bit mean now.

What about having multiple edit forms open simultaneously , each with a different record?

From a users perspective this might look like a small extension of the program, but as developers we know that a dataset can have only one current record. If each edit form has to work on its own record, we need a separate dataset for each edit form.

Before someone suggests to place the dataset back into the form: No, we don’t want that! In a real world scenario there may be more datasets with complex interactions and business rules to follow. Things never are as simple as shown in a demo.

This seems also a good place to explain the TDataSource should stay in the form rule mentioned in the beginning.

The official one is: One can easily change the dataset in the data source of an individual form instance without interferring with others also using a datasource in a datamodule.

The unofficial one is: Sometimes the IDE looses these connections, f.i. when the data module cannot be opened. In that case it is easier to re-connect the dataset of one data source on the form instead the data sources of several data controls.

A valid approach is to create a new instance of the data module for each form instance and let the form components use that one. For that we declare a new property in the Edit Form of type TMainDM and create an instance in the forms constructor.

We remove the Edit Form from the auto-create forms and create a new instance in the button click event – just like before, but non-modal.

Each instance is owned by the main form, so they will be freed when the main form is destroyed.

Let’s see if that is sufficient.

Not quite! Although we have separate Edit forms, each with its own local data module instance, the current record still follows the dataset of the main form. Why is that?

It’s that magic we saw before, that re-connects the controls at runtime in the same way as we specified in the Designer. For some reason this magic prefers the auto-created instance of the MainDM data module over the local ones in the Edit Form.

So let’s have a quick look at the internals behind this magic.

Peeking in the DFM we see the connection from the data source to the dataset looks like this:

The target of this connection is simply the qualified name of the tblData component in the MainDM data module. So the connections are somehow resolved by the components names.

But wait, the local TMainDM instances are also named MainDM. Why are they just ignored when it comes to finding the MainDM instance to connect the dataset?

The relevant code is located in the method TReader.DoFixupReferences found in System.Classes:

The interesting find is that the reader first tries to find a component inside the root component of the reference to resolve (i.e. TEditForm): FindNestedComponent. Only when no suitable component is found, it asks for the help of FOnFindComponentInstance.

But when nested components are preferred, our local instance of TMainDM should be found first, shouldn’t it? Inspecting the code for FindNestedComponent reveals that it actually uses FindComponent inside the Edit Form to access the TMainDM instance. There are two reasons for not finding it:

    1. The instance is not created yet
    2. The instance has another name as we expect

We can rule out the first one as we create the instance before we call inherited inside the constructor. That puts the blame on the second one.

We can find a hint in TReader.ReadRootComponent, where a call to FindUniqueName is responsible for the name change of our local TMainDM instances. As we are not able to change the reader code (at least not willing to do so – although there are ways), we just have to cope with this name change of our local data module instances.

How can we do that? As often there is more than one way.

The easiest one is to simply rename the instance back to its original name. Unless your application relies on unique names for these data modules there should be no harm with this approach.

We also have to synchronize the dataset in the local data module with the current record in the Main Form (We should have done that before, but it wouldn’t have helped either).

Looks much better now, doesn’t it? And we even haven’t added much new code to make it work.

In case you cannot live with the non-unique naming of the local TMainDM instances (they all appear in the Screen.DataModules list), there is another approach: Change all the references to use the new name of the data module. Unfortunately this requires a bit more coding.

The TReader offers an event OnReferenceName, which is called directly before the FindNestedComponent call in DoFixupReferences. If we could connect a handler to this event, we could change the reference in a suitable way. So how can we get hands on the TReader instance to wire that event?

In their infinite wisdom the creators of the Delphi component architecture gave us the virtual ReadState method with the current TReader as a parameter. So we override the ReadState in TEditForm and inject our event handler.

Compared to our previous experience that looks like quite a bit of code. For a real world scenario one should consider to place most of the code in a common ancestor class, make MyReferenceName virtual  and simply override it in derived classes where necessary.

Finally, we found a way to use the Delphi Form Designer to connect components between forms, frames and data modules without loosing these connections for multiple, dynamically instantiated instances. A benefit of this approach is the minimal amount of code we have to write – and less code means less bugs and easier maintenance.

The sources to the program shown in this article can be downloaded here:
TweakingDFMLoading.zip.
You might have to change the database connection to something you have on your system. The sample code uses the employee.gdb database from the samples folder connected via a standard Interbase developer instance. If you use another database you probably have to change the data fields in the Edit Form.

Cheers.

Author: Uwe Raabe

Addicted to Pascal/Delphi since the late 70's

2 thoughts on “Tweaking DFM Loading”

    1. Hi Ian,
      the number of resources is the same in all shown stages of the program. The amount of additional code is neglectable.

      Obviously the memory consumption increases with every instance of the data module, but as it is a requirement to have multiple records open simultaneously, this is probably not much different to other approaches. One could argue that using an ORM allows to have the records in memory as class instances, but it is no secret that an ORM usually needs more memory than a classic db aware approach.

      Uwe

Comments are closed.