Sunday, March 25, 2018

Part 3 of 4—Model Details—Using Delphi LiveBindings With TObject and TObjectList

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
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):

Live Bindings Using Objects diagram showing object relationships.
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:


  1. 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.
  2. 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:

  1. We must identify the current TListView object, that contains the TObjectList<TEmployee> property for the currently selected Branch.
  2. 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

No comments:

Post a Comment

FireMonkey String Grid Responsive Columns

FireMonkey String Grid Responsive Columns Code to Cause Column Widths to Change When String Grids Are Resized Overview I have a FireMonke...