Introduction
One of the most powerful features of C++Builder is the ability to use the C++
inheritance mechanism to create facilities which are like - but not the same
as - their ancestors. In most C++ Builder texts, this is discussed in the context
of inheriting the characteristics of single components and creating new components
based on the characteristics of existing components. This document, however,
shows how to use "inheritance in the large" - how to inherit an entire application
as a basis for a new stage in development.
Fundamentals
Every object in C++ Builder is based on a class.
C++ Builder is a little different from most C++ systems in that every new form
is typically the only instance of its class. For instance, if you create a form,
you do not get an instance of TForm, you get an instance of TForm1, which is
descended from TForm. However, this makes complete sense, since the moment you
add a control to the form, it will become different from TForm.
Since forms and data modules are classes, they can participate in inheritance.
And this is the basis on which you can inherit an entire application.
Application Structuring for Inheritance
It is essential to lay the ground work for inheritance with a solid substructure
of well-named components in a well-designed directory structure. This is critical,
because it is hard to move things around once you have inherited from it.
Your application will consist of forms and data modules. There will be cross
references between forms, data modules, and the components in each. It is essential
to use a specific method of attaining this cross referencing if inheritance
is to succeed - you must have used the File / Include Unit Hdr menu item to
make one form / data module visible in another. Passing forms and data modules
in the constructors of other forms or data modules, or using some other method
to establish this connection may not work under inheritance.
You must take care to ensure that things are created in the appropriate order
in the Project Source. Otherwise, you will get access violations. I recommend
creating all data modules before any forms.
Post Inheritance Naming
When you inherit from the base class application, the components will have
new names generated for them. If your base class form was called MainForm, then
its class is TMainForm, and after inheritance you will get MainFormn
and TMainFormn, where n is some arbitrarily selected number. You
will want to be able to prefix your derived classes with your derived application
name (for instance TVideoLibraryMainForm) rather than use the arbitrary number,
so create names in the base class which make sense to do this with.
Special Measures For Inheritance
C++ Builder apparently sets the global variables shared in the File / Include
Unit Hdr (which are in the .h files for your forms and data modules) when the
components are instantiated. However, when you inherit from a form or data module,
that form or data module is not instantiated - therefore the global variables
which may be used by internal methods of that level of the data module to refer
to other forms or data modules, will cease to work after the form or data module
has been inherited. Therefore, the constructor for each form and data module
must contain a statement assigning "this" to the global variable. For instance,
take the class TMainForm from my application:
__fastcall TMainForm::TMainForm(TComponent *Owner)
{
MainForm = this;
}; |
However, even this is not enough. All data aware controls which refer to data
sources in data modules will lose that connection after inheritance. You must
reassign those connections in the form constructor, or the data aware controls
will no longer be data aware after inheritance:
__fastcall TMainForm::TMainForm(TComponent *Owner)
{
MainForm = this;
MyTableGrid->DataSource = MyDataModule->Source;
}; |
The same is true if you place popup menus in a data module - the connection
to the popups will be lost, so you must assign them in the constructor, just
like data sources.
And also important, though easily neglected, are fields on tables in data modules
which lookup fields in tables in another data module. These need to have the
LookupDataSet reestablished in the constructor of the data module...
__fastcall TMyDataModule::TMyDataModule(TComponent *Owner): DataModule(Owner)
{
MyDataModule = this;
MyTable->Active = FALSE; // Redundant, since table
must be inactive at design time or DataModule constructor will throw exception
on incomplete lookup information
MyTableAccountName->LookupDataSet = OtherDataModule->Table;
MyTable->Active = TRUE;
}; |
Failure to do this causes a "Lookup information for field 'blah' is incomplete"
exception, which you will find nearly impossible to track down, since every
field will appear to have its LookupDataSet properly pointing to the
correct data module.
Note also that every table with such fields must be Active=FALSE at design
time, since otherwise the constructor for class DataModule will discover the
missing lookup information before your code to reload it is called.
Getting Ready To Inherit
The first step you must take is to place all of the forms and the data modules
in the Object Repository. Make sure that you have named everything properly,
and that your directory structure has been established in such a way that you
can live with it - it will be difficult to move anything once this step is complete.
You might think this would be a simple step - simply add the project to the
repository. Unfortunately, C++ Builder does not allow you to inherit from projects
in the repository, only to copy them.
So, create a page in the repository for your application. Then go to each form
and data module and pick "Add to repository" from its popup. Give it a name
that corresponds to its name in the base application and assign it to your application's
page. Do not do this from the Project Manager - it only allows you to add the
entire project to the repository.
Creating a Derived Application
Now, what you have been waiting for. First, create a new application. Then,
pick File / New, and introduce the first component from the base application
page in the repository. Remember to click "Inherit" on the repository page
- the default is to copy. Delete the original starter form. Rename the first
component as <appname><oldname>. Save the first component in the
appropriate place in your directory structure as <oldname><appname>
- for instance, if the old file name was MainForm.cpp, then save as VideoLibraryMainForm.cpp.
This ensures that the names will not conflict - C++ Builder will not allow you
to save the component with the same name as the component uses in the IDE. Then
save the project as <appname> in its own directory. Now repeat - adding
from the repository (click Inherit first), renaming the component, and saving
the component - until all of the components from the base application page have
been added.
Also note that you will probably need to go into the Project Source (View |
Project Source) and reorder the CreateForm calls so that the data modules are
created first, the main form is created as the first non-data module form, and
the rest of the forms are created after the main form.
Getting Ready For Compilation
At this point, you are ready to compile the new application. However, be ready
for the possibility that some important paths to component header files may
be missing from your project. Make sure to move the ancestor header #include
before any other header files - this usually gets rid of the problem; if that
is not sufficient, then add the path to the offending include, save the file,
and then delete the line with the path - this action, for some reason, is sufficient
to readd the path to the project.
Modifying the Derived Application
You may add controls, override event handlers, reconnect data sources, change
popups - in short do anything you like - in any derived application component,
without disturbing the base class application. Conversely, if you modify the
base class application, those modifications, unless overridden, appear immediately
in the derived application.
You can also add components or fields to data modules or forms.
One thing you cannot do is to delete components from the base class. But, for
controls, you can make them invisible. Note that turning off visibility is not
sufficient for TTabSheet derived components - to make them truly invisible,
you must set TabVisible to false.
Note also that by default the old event handlers function - however, if you
override them, the base class event handlers WILL NOT FIRE. Therefore, you should
call them from within your override handler:
void __fastcall TVideoLibraryMainForm::FormShow(void)
{
TMainForm::FormShow();
// My stuff
}; |
Inheriting From the Derived Application
This simply requires repeating the same procedure you performed for the base
application. You must add all of the derived application components (not just
ones you changed) to the repository on a page of their own, and then you follow
the same steps you did when creating the first level derived application, but
you inherit the components from the derived application page.
However, remember that the derived application must, in its constructors, assign
form and data module global variables, and it must also connect any new popups
or sources from data modules to their form counterparts, just as in the base
application.
Maintaining Base and Mid-Level Derived Applications
Add a data source to a data module and you must add the connection code to
the constructor. Add a popup, and you must add the connection code. Add a grid
that uses an existing data module and you must connect them in the constructor
also. Add a new form or data module, and you must not only follow the basic
rules, but you must also add them to the repository and visit all derived application
to add the components at each level; if in a mid-level derived application,
you must then add the new derived component to the repository and then carry
it down to the next level.
Maintaining Multi-Level Derived Application Cross Form Links
As mentioned, it is essential to make certain that tables are closed when the
constructor for the ancestor data module is called, because otherwise the ancestor
class sees the lookup information for persistent fields as incomplete, that
information not having been reset by the descendant constructor.
However, once a descendant class adds its own lookup fields, the situation
becomes more complex.
Let's imagine an application family consisting of two levels. The top level
has a data module and some lookup fields in that data module. The second level
adds some additional lookup fields to a table which exists in the top level
module.
The following sequence occurs at the second level:
- The most descendant class is instantiated; its constructor is called. Before
the body of its constructor is executed, the constructor invokes the constructor
of the ancestor.
- The ancestor class constructor sets up its lookup fields and activates the
tables. Unfortunately, the lookup information for the descendant lookup fields
hasn't been filled in yet, so the activation of the tables causes the application
to abort.
This is basically a reprise of the normal scenario that causes the annoying
"Lookup information for field 'blah' is incomplete" error message at run time
(see above). The problem is that this error recurs at every level when you add
lookup fields. Thus, it is essential to have a pattern for dealing with it.
The pattern needed is essentially this. Every data module on the inheritance
path has two constructors - the default with only the Owner parameter, and a
second one with a bool parameter "IsAncestor". Every data module also has two
functions, one to set the lookup information and one to activate the tables.
At the top level, it goes:
__fastcall TBaseClassDataModule::TBaseClassDataModule(TComponent
*Owner):TDataModule(Owner)
{
BaseClassDataModule = this;
SetLookupFields();
ActivateTables();
}
__fastcall TBaseClassDataModule::TBaseClassDataModule(TComponent *Owner,bool
IsAncestor):TDataModule(Owner)
{
BaseClassDataModule = this;
SetLookupFields();
if (!IsAncestor) ActivateTables();
} |
While in the derived class it goes like this:
__fastcall TDerivedClassDataModule::TDerivedClassDataModule(TComponent
*Owner): TBaseClassDataModule(Owner,TRUE)
{
DerivedClassDataModule = this;
SetLookupFields();
ActivateTables();
}
__fastcall TDerivedClassDataModule::TDerivedClassDataModule(TComponent
*Owner,bool IsAncestor): TBaseClassDataModule(Owner,TRUE)
{
DerivedClassDataModule = this;
SetLookupFields();
if (!IsAncestor) ActivateTables();
}
void __fastcall TDerivedClassDataModule::ActivateTables(void)
{
Inherited::ActivateTables();
} |
Note the ActivateTables function in the descendant, which in this case is just
a placeholder. However, if the derived class should have tables of its own to
activate, that it where it would be done, as well as the calling of the ancestor's
ActivateTables. (Note: "Inherited::ActivateTables" is based on a typedef at
the top of the data module class definition "typedef TBaseClassDataModule Inherited"
- which I believe is a convention used by Calvert in his books, and which is
well worth adopting).
Note also that, if there is a class that inherits from TDerivedClassDataModule,
but which does not add anything of its own, that derived class can simply use
the constructor of its ancestor that requires nothing more than "Owner".
Cautions
If your system which is to be inherited consists of forms and data modules,
and the data modules refer to the form, the object repository requires that
they be inherited separately - you cannot inherit a project.
If you have such a project, it is possible for the order of loading of forms
and data modules by your project to cause access violations and other errors
when your project is being loaded, including the disconnection of the data modules
from form components. This can be dealt with by preventing autosave of the project,
and ordering the [Modules] section of the .dsk file by hand so that data modules
are loaded before forms. There is also a set of sections below [Modules] which
has [Form1] etc. These specify the state of the form as 0 (invisible), 1 normal,
and 3 minimized. You must reorder / renumber these to match the position of
the forms in the [Modules] section, or some of your forms will not appear in
the correct state.
Conclusion
This is not a simple or completely graceful process, but it is probably one
of the most powerful application development methods available. Have fun, and
be careful.
|