t e m p o r a l 
 d o o r w a y 

Inheriting An Entire Application

 

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:

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

Copyright © 2004 by Mark Cashman (unless otherwise indicated), All Rights Reserved