Source Code
The source code in the form of a Delphi Project Group is available at:
The project was developed using Delphi RAD Studio Tokyo (10.2.2)
The README file contains additional useful information.
MODEL Details
The MODEL functions as the main mechanism to manipulate the data for the application. In the case of an application that makes use of SQL or other persistent databases, we would expect to find FireDAC components managed by this class. In the present case, we are not using any kind of database that FireDAC supports, but never-the-less our MODEL will have data objects to manage.
The Model Is a Data Module
The file MDULBO.pas
specifies the MODEL part of the system. The model is a class named TMDLBO
and descends from a TDataModule
class. This permits the dropping of components on the MODEL design surface.
Instantiation
Normal Delphi instantiation has been disabled for this class. As a singleton class, a different mechanism was used that hides the customary Create constructor and provides instead a function named MDLBOGet
that checks to see if the class has already been instantiated. If not yet instantiated, the class is instantiated and a pointer retained for future requests. In any case, the pointer for the instantiated class is returned to the caller.
The GDLBOGet
routine invokes the Create
constructor as needed. During the execution of the constructor, the database class TCorp
is instantiated. As we described earlier, this results in a chain of constructor calls to the various classes of the database; the final result is an instantiation of the database data along with the instantiation of the MODEL (this class.)
Pay special attention to the TMDLBO.Create
constructor. When the MODEL is instantiated, a call is made to this constructor. As a singleton, this occurs only once. Note that the constructor first calls the constructor for the database classes, to instantiate the entire database class structure prior to processing anything for the MODEL class. Following this it calls the inherited Create
for itself that completes the instantiation of the MODEL.
It is important that this order be maintained because the OnCreate
events of the TAdapterBindSource
components must be able to access the already-instantiated database classes. Since the TAdapterBindSource
components are created by the inherited Create
call, instantiating the database data classes prior to the call to inherited Create
satisfies this requirement.
LiveBinding Objects
LiveBinding Objects and ObjectLists behave similarly to the bindings for database objects. We note that the database components connect to a TAdapterBindSource just as do Objects and ObjectLists, but that is the extent of our involvement with database components in this discussion. We are only considering Objects and ObjectLists at this time, so any further discussion will not include references to database components (FireDAC, dbExpress, etc.)
In our current application we have three data sources to consider:
TCorp
is a single object.
TBranch
occurs multiple times and is managed by a TObjectList<TBranch>
; property of TCorp
.
TEmployee
occurs multiple times and is managed by a TObjectList<TEmployee>
property of its parent TBranch
.
We customarily use a
TDataGeneratorAdapter
to create arbitrary data for use at design time. A corresponding
TAdapterBindSource
is needed for each generator to allow us to use LiveBindings to connect the data sources to other components, typically display components. With that in mind, the
MDULBO.pas
file that contains the
MDLBO
class appears as follows:
|
MDLBO Data Module from MDULBO.pas |
Values for the
TCorp
display components are generated by the
dgCorp
component. Values for the
TBranch
display components are generated by the
dgBranches
component. Finally, values for the
TEmployee
display components are generated by the
dgEmployees
component. Each of the data generator components has a corresponding
TAdapterBindSource
component that connects to its corresponding data generator. To force the
TAdapterBindSource
components to be a property of the data module, you must drop them on the form from the Tool Pallet and connect them each to the proper
TDataGenerator
component. If you use Visual Live Bindings to automatically create the
TAdapterBindSource
components, they will likely end up on the user interface Form—the VIEW. We want them as a part of the MODEL, not the VIEW, to achieve a separation of concerns. Hence, they should be manually placed to achieve our desired result.
In general, Objects and ObjectLists are connected to LiveBindings as illustrated in the following diagram (a full-sized PDF of this diagram is also to be found in the Docs folder of the Github project or can be downloaded
here):
|
LiveBindings Using Objects |
So far we have connected the data generators to their respective
TAdapterBindSource
components. Data generators are a special case, and the component contains both the generator and the
TDataGeneratorAdapter
needed to complete the configuration. This is the case illustrated by the rose colored combination in the diagram. Objects (goldenrod) and ObjectLists (blue) are different. The developer must provide both the data source (an Object or an ObjectList) and the developer must provide a
TObjectBindSourceAdapter
(goldenrod) or
TListBindSourceAdapter
(blue) as appropriate.
Only one of the paths to the
TAdapterBindSource
can be active at any given time; we begin by using the
TDataGenerator
to assist the design effort. But now, at run time, we are faced with replacing the design setup by the actual run-time data sources, either from a
TObjectBindSourceAdapter
or a
TListBindSourceAdapter
. This is where the Type Aliases defined in the
DBLBO.pas
file help. We can easily refer to
CorpObjectBSWrapper
, or
BranchListBSWrapper
, or
EmployeeListBSWrapper
and be confident of referencing the correct kind of components. The actual substitution of data sources (from
TDataGenerator
to
TObject
or
TObjectList
) takes place in the
OnCreateAdapter
event handler for each of the
TAdapterBindSource
components in the application.
In the following example, be sure to understand the different classes involved. Prior mention has been made of the draconian naming failures of LiveBindings. Refer to the previous diagram and the Type Aliases mentioned so that you understand what types are being instantiated. Examine the
absCorpCreateAdapter
event handler code in order to follow the following steps:
- You must first create a
TObjectBindSourceAdapter
for the TCorp
instance. See the diagram above. This object behaves just like a component, but cannot be dropped on a form because of its generic nature. The code uses the Type Alias to create the object and save it as a property of the form, just as though it had been created at design time. Notice that during the create process, you must identify the specific object you're connecting, in this case the Corp
instance of the TCorp
class. The fact that it's a TCorp
instance is inherent in the Type Alias, where TCorp
is specified.
- After the
CorpWrapper
is created, it must be passed back to the TAdapterBindSource
event so that creation of the TAdapterBIndSource
can be completed.
These same two steps must be performed for each data source we want to connect to LiveBindings. There is an OnCreate
event handler for each of the three data sources we are working with. Note that for Branches and Employees we specify the TObjectList
properties as the data source rather than just a single object.
You should now also understand why instantiation of the actual data objects must precede the creation of the LiveBinding objects themselves; the data objects are referenced by the LiveBinding objects and hence must be available at the time the OnCreate
event handler fires.
Handling Scrolling, Deleting and Inserting
We now consider how to handle child ListView
displays when the parent list's current item changes. A change in the currently selected Branch occurs when the item focus changes. This can be caused by
- Clicking on another list item or moving to another item using the
TNavigator
component.
- Inserting a new Branch. The focus will change to the newly inserted Branch.
- Deleting a Branch. The focus will change to one of the remaining Branches or, if the list has become empty (zero Branches) will not have any currently focused entry.
The
TListBindSourceAdapter
has an extensive set of event handlers that are used for handling these conditions. In our case, the particular component we are interested in is identified by the Type Alias
BranchListBSWrapper
. When we create this object, we must also provide method pointers to the object so that the needed events are handled. In addition, we must provide the actual event handler code that will be executed when the event fires. First, take a look at the event handler code. Note that the signature is a requirement of
BranchListBSWrapper
.
Two things must take place when the Branch
TListView
Item focus changes:
- We must identify the current
TListView
object, that contains the TObjectList<TEmployee>
property for the currently selected Branch.
- We must update the Employee wrapper to point to the
TObjectList<TEmployee>
property identified in the first step.
Note that the special case of an empty Branch list results in setting the Employee wrapper to a nil value. (No items are displayed.)
The first step is satisfied by locating the TBranch
object using an expression with two casts to "dig down" into the Branch TBindSourceAdapter
that contains a pointer to the current entry.
The second step updates the EmployeeWrapper
(saved as a property of the TMDLBO
class) with a pointer to the current Employee TObjectList<Employee>
extracted from the Branch obtained in the first step. The SetList
method is used to do this. It changes the pointer to the data that will be displayed by the TListView
.
Finally, we reposition the Employee List selection to the first item and set the
EmployeeWrapper
to
Active
to display the Employee List contents.
There are some additional considerations when a master/detail relationship between two
ObjectLists
exists. When the focus of the master changes, LiveBindings must be updated to reflect the new detail list that is related to the master. This is shown by the following
CreateAdapter
event for our Branches list. After the
BranchListBSWrapper
has been created, three Event Handlers are specified so that synchronization will take place when the Branch Item focus changes. In all other respects, this
OnCreate
Event is identical to the one previously described, except, of course, it refers to objects appropriate to Branches.
This completes the examination of the MODEL code. There are quite a few "moving parts" to the entire solution:
- Data classes
- Data generators
- LiveBindings components
- Objects to be displayed by LiveBindings
- ObjectLists to be displayed by LiveBindings
- Considerations about creation order
- Creation of LiveBindings objects at run time and linking them into other LiveBindings components along with specifying needed event handlers
- Handling TListView scrolling in cases of master/detail relationships
- Event handler code to implement desired behaviors when TListView entries scroll, are created or deleted
The number of lines of code required to achieve this is not particularly large (except for the actual data objects in your application, which will vary depending on the complexity of your data.) It does require that you "get organized" and perhaps develop a check list of required steps to ensure that you include all the necessary code.
Next we consider the final (and much simpler) piece of the puzzle, the VIEW.
Using Delphi LiveBindings With TObject and TObjectList<T>—Part 2 of 4—Database Details<==Previous
Next==>
Using Delphi LiveBindings With TObject and TObjectList<T>—Part 4 of 4—View