![]() |
![]() |
![]()
| ![]() |
Delphi at Work Tabbed Notebook / OOP / Memory Management / Visual Inheritance
Embedded Forms Putting Visual Inheritance to Work
A tabbed notebook presents information to the user in an organized fashion (see Figure 1). Unfortunately, this interface complicates the coding effort by mixing page navigation logic with business application logic. Every page presented to the user introduces more business application logic concentrated into one place - making maintenance issues a growing concern.
Embedding forms in the pages of a notebook is a technique that allows individual forms to contain discrete business logic, while another totally separate form contains the page navigation and common function logic. By dividing the logic into manageable units of page navigation and user interface, the following advantages are realized:
Embedding forms will give the user the perception of working with one form containing a notebook full of controls. Only the developer realizes that each page in the notebook is a separate form, allowing for improved team development efforts. Two descendant classes of TForm are created to accomplish the effect: TEmbeddedNotebook to manage page navigation, and TEmbeddedForm to handle user interface. (All source code referenced in this article is available for download; see end of article for details.)
TEmbeddedForm is a pure virtual descendant of TForm, and is used to represent each page in the notebook. The abstract methods within TEmbeddedForm provide the functionality needed to support generic behaviors, such as save, cancel, and print, invoked from TEmbeddedNotebook. TEmbeddedNotebook is an abstract descendant of TForm that contains a TPageControl and several TBitButton objects used to manage a collection of TEmbeddedForm objects.
To embed a form within a TPageControl, follow these steps (see Figure 2):
// Size, position, initialize, and show the form // associated with the page. procedure TFormEmbeddedNoteBook.ShowForm( Form: TEmbededForm); begin Form.BorderStyle := bsNone; // Eliminate border. // Set Parent as the active page. Form.Parent := PageControl1.ActivePage; Form.Align := alClient; // Make form fit the page. Form.InititalizeForm; // Display form. SetButtons(Form); // Call to set print btns. Form.SetFocus; end; Figure 2: Embedding one form within another.
Embedding a form occurs only when the user selects a tab on the notebook. No embedded form is loaded into memory until selected by the user, resulting in improved memory management.
Divide and Conquer Tabbed notebooks divide user interface elements into logical groups to make understanding easier for the user. Embedded forms do the same for developers, delivering on the promise of improved team development. Dividing or structuring code into logical groups is a key factor in writing maintainable code.
The TEmbeddedForm class has five abstract methods: InitializeForm, Print, SaveForm, CancelForm, and CloseForm; and one property, CanPrint (see Figure 3). These form the functional interface between the classes TEmbeddedForm and TEmbeddedNotebook.
// Embedded form pure virtual class definition. TEmbeddedForm = class(TForm) protected FCanPrint: Boolean; // True if form has print capability. public property CanPrint: Boolean read FCanPrint default False; // Housekeeping logic. procedure InitializeForm; virtual; abstract; procedure Print; virtual; abstract; // Print form. // Save data changes. function SaveForm: Boolean; virtual; abstract; // Cancel data changes. function CancelForm: Boolean; virtual; abstract; // Close the form. function CloseForm: Boolean; virtual; abstract; end; Figure 3: The TEmbeddedForm class definition.
InitializeForm is a procedure invoked every time the user selects a page. InitializeForm handles any setup logic, such as performing calculations or queries before displaying the form.
Print is a procedure that will execute whatever method of printing the developer has selected. The CanPrint property is used by the TEmbeddedNotebook to disable the user's ability to invoke the print method when no print functionality is needed.
SaveForm is a function that returns True if the save was successful. SaveForm is invoked when a user selects Save. If SaveForm returns False, the page is focused and the user should be prompted to fix the condition that is causing the save to fail (it could be that the embedded form is failing an edit or information is missing).
CancelForm is a function that returns True if the cancel was successful. CancelForm is invoked when a user selects Cancel. In the event CancelForm returns False, the page is focused and the user should be prompted to fix the condition that is causing the cancel to fail.
CloseForm is a function that returns True if the user has handled all pending changes. CloseForm, as can be seen from the supplied source code, is best utilized by calling CanCloseQuery, passing in a Boolean variable. Nominally, CloseForm should be used to check for any pending changes. If changes exist, the user should be prompted for the disposition of these changes.
The most effective way to implement TEmbeddedForm is to sub-class an intermediate class. The intermediate class will implement the generic rules for this project, such as behaviors for SaveForm, CancelForm, and CloseForm. Sub-classing all the other embedded forms to be used from the intermediate class will improve code reuse. However, for simplicity's sake, this is not the route I chose to use with the supplied demonstration project.
Implementing the class TEmbeddedNotebook is as simple as overriding one abstract method, FindEmbeddedForm, which returns a TEmbeddedForm (see Figure 4). Taking the time to look over the source for TEmbeddedNotebook reveals quite a bit of code that handles the drudgery of housekeeping.
// Return embedded form based on the notebook page passed. function TfrmDiDemo.FindEmbeddedForm(aPage: Integer): TEmbeddedForm; begin case aPage of efCustList: Result := TfrmCustomer.Create(Self); efOrders: Result := TefOrders.Create(Self); efOrderItm: Result := TefOrderItems.Create(Self); else Result := nil; end; end; Figure 4: Loading an embedded form.
Under the Hood TEmbeddedNotebook is the real workhorse here. It manages a TList of TEmbeddedForm objects, as well as all the generic processing for the TEmbeddedForm objects. TEmbeddedNotebook delivers improved memory management and improved code reuse. The central focus of TEmbeddedNotebook is the TList of TEmbeddedForm objects named FormList. TEmbeddedNotebook handles these tasks:
As previously mentioned, FormList is a TList of TEmbeddedForms. To realize improved memory management, the embedded forms won't be created until they're requested by the user clicking on their respective page. To this end, FormList is initialized and loaded with nil pointers, one for each TTabSheet on the TPageControl (see Figure 5). The reason for loading nil pointers is that retrieving TEmbeddedForms from FormList is dependent upon an exact correlation between the page index of the selected page, and the corresponding TEmbeddedForm.
// Set up the PageControl to contain embedded forms. // Create a TList to hold the TEmbeddedForm Objects. procedure TFormEmbeddedNoteBook.InitializePageControl( Sender: TObject); var Index: Integer; begin // Turn to the first page. PageControl1.ActivePage := PageControl1.Pages[0]; SetPages; // Set Visible property for tabsheets. FormList := TList.Create; // List of available forms. // Create placeholder for each tab. for Index := 0 to PageControl1.PageCount -1 do FormList.Add(nil); // Empty placeholders. PageControl1Change(Sender); // Show 1st form. end; Figure 5: Setup for an embedded form list.
Showing a TEmbeddedForm occurs when TEmbeddedNotebook responds to the TPageControl's OnChange event. GetForm is the function responsible for returning the correct TEmbeddedForm (see Figure 6).
// Return selected form if available; // otherwise raise exception. function TFormEmbeddedNoteBook.GetForm: TEmbeddedForm; var PageNum: Integer; begin PageNum := PageControl1.ActivePage.PageIndex; if not Assigned(FormList.Items[PageNum]) then try // If form doesn't exist. FormList.Delete(PageNum); // Clear nil placeholder. // Insert new form. FormList.Insert(PageNum, GetEmbeddedForm(PageNum)); except on E: EFormNotFound do // New form creation failed. // Replace nil placeholder. FormList.Insert(PageNum, nil); end; // Return contents of FormList. Result := TEmbededForm(FormList.Items[PageNum]); end; Figure 6: Retrieve the selected embedded form.
The page index of the active form is used as the index into FormList. If the pointer is assigned, then TEmbeddedForm is returned. Otherwise, the page index of the active page is passed to the GetEmbeddedForm function, which creates and returns the correct TEmbeddedForm (see Figure 7).
// Call a routine to return the embedded form // for a given page. function TFormEmbeddedNotebook.GetEmbeddedForm( PageNum: Integer): TEmbeddedForm; begin Result := FindEmbeddedForm(PageNum); if Result = nil then Raise EFormNotFound.Create('Form Not Found'); end; Figure 7: Driving logic for requesting embedded forms.
Hiding a TEmbeddedForm is accomplished by responding to the TPageControl's OnChanging event. The OnChanging event has a variable Boolean parameter, AllowChange. As a result of calling TEmbeddedForm's CloseForm function, a Boolean will be returned that's used to set the state of AllowChange. The recommended practice is for TEmbeddedForm to perform its final editing, and post all data changes here. If TEmbeddedForm fails an edit or a post, a message should be displayed to the user explaining how to correct the error, and a Boolean with a value of False is returned. This prevents the user from turning to another page until the error is corrected, or the user cancels all pending changes.
Generic processing comes in two flavors: processing that occurs to all embedded forms currently loaded (ProcessAllForms), and processing that occurs only to the embedded form currently displayed (GetForm). Save, cancel, and close, by default behavior, fall into the "Occurs to all forms" flavor of processing. ProcessAllForms is the method responsible for iterating through the list of loaded forms. It's a method that accepts a procedural variable that points to a method that accepts a TEmbeddedForm as a parameter (see Figure 8).
// Process each EmbeddedForm within FormList, with the // function provided as the parameter aFunction. procedure TFormEmbeddedNoteBook.ProcessAllForms( aFunction: TFunctType); var Index: Integer; begin // For all the forms in the list... for Index := FormList.Count -1 downto 0 do // If form has been assigned... if Assigned(FormList.Items[Index]) then // Call the passed method with the form. aFunction(TEmbeddedForm(FormList.Items[Index])); end; Figure 8: Driving logic for generic processing of all loaded embedded forms.
Printing falls into the other flavor of processing, which simply invokes the selected embedded form's Print method (see Figure 9).
// Print a screen dump report. procedure TFormEmbeddedNoteBook.btnPrintClick( Sender: TObject); begin GetForm.Print; end; Figure 9: An example of specific processing of a selected embedded form.
Safety First To ensure that changes the user has made are handled correctly when the TEmbeddedNotebook is closed, generic processing similar to "Occurs to all forms" is used. Because data integrity is of paramount importance, the user cannot be allowed to close the TEmbeddedNotebook until all changes have been saved or canceled by the user.
The following is the chain of events for ensuring the integrity of the changes the user has made:
When TEmbeddedNotebook's OnCloseQuery event is fired, it invokes CloseEmbeddedNotebook (see Figure 10), which invokes ClearFormList (see Figure 11). ClearFormList iterates though FormList, invoking the CloseForm function for all loaded TEmbeddedForms. If CloseForm returns True, TEmbeddedNotebook frees that TEmbeddedForm. Otherwise, the corresponding page is focused and TEmbeddedNotebook's CanClose variable is set to False, allowing the user to remedy the situation and continue.
// Close all embedded forms. If all forms are closed and // destroyed, the result is True, otherwise it's False. function TFormEmbeddedNoteBook.CloseEmbeddedNotebook: Boolean; var Index: Integer; begin try Screen.Cursor := crHourglass; Result := True; Index := ClearFormList; if Index > -1 then begin Result := False; PageControl1.ActivePage.PageIndex := Index; Exit; end else FormList.Free; finally Screen.Cursor := crDefault; end; end; Figure 10: Ensure all modifications have been processed before the embedded notebook is closed.
// This routine calls the ProcessAllForms with CloseForm // method of all embedded forms. If succesful, the form is // freed and removed from the FormList. function TFormEmbeddedNoteBook.ClearFormList: Integer; var Index: Integer; begin Result := -1; // Set result to all deleted. ProcessAllForms(CloseForm); // Close all forms. // For all the forms in the list. for Index := FormList.Count -1 downto 0 do // If any forms are still loaded... if Assigned(FormList.Items[Index]) then begin // Form cannot be closed, return the index. Result := Index; Exit; // Exit the for loop. end; end; Figure 11: Clean up of loaded embedded forms.
Great care has been taken to ensure the user cannot close the TEmbeddedNotebook without disposing of their changes. Save and Cancel buttons have also been provided for the user's convenience.
The Save and Cancel buttons' Enabled property is set using a user-defined Windows message WM_REMOTEDSCHANGED. Setting the lparm parameter of TMessage with a 1 or 0, depending on the state of the data or the presence of updates, communicates the state of the data (see Figure 12). The definitions needed for WM_REMOTEDSCHANGED are in the TYPES.PAS unit.
// Light data-handling function buttons. procedure TFormEmbeddedNoteBook.SetDataButtons( State: Boolean); begin btnSave.Enabled := State; btnCancel.Enabled := State; end;
// Handle buttons to notify user of the state of the data. procedure TFormEmbeddedNoteBook.DSChanged( var Message: TMessage); begin SetDataButtons(Message.lParam = wmpTrue); end; Figure 12: Data state-handling routines.
Polly Want a Cracker? Polymorphism allows a descendant class to operate differently than its parent class for the same method call. For any framework to be usable, it must allow for its default behaviors to be changed. In nature, this is called evolution. Object-oriented design should always allow for the evolution and maturation of classes to adapt to changing business rules.
While every attempt has been made to default to usable behaviors, "No plan ever survives contact with the enemy." Examples of default behavior are the processing for save, cancel, and close functions, which assume the user wants to operate on all pages generically.
The two most likely behaviors that will need to be changed are: changing tabs forces the user to resolve data changes (save or cancel); and, function button clicks process all pages, or a single page.
At the initial design of embedded forms, certain assumptions were made. The first assumption was that the information contained on the embedded forms was interrelated. Assuming the information was interrelated, the design stance was to force the user to either save or cancel all pending changes when they select a new tab. The second assumption was that when the user clicked Save or Cancel, the action would take effect in all loaded forms.
Altering the behavior of the embedded notebook when pages are turned can be accomplished in the following manner. In the TPageControl's OnChange event, the CanChangeTabs function is invoked. CanChangeTabs returns the result from calling the TEmbeddedForm's Close function. By overriding this behavior to return True, the pages of the notebook can be changed without having to resolve pending data changes.
Altering the function button behaviors of TEmbeddedNotebook can be accomplished in the following manner. When the Save or Cancel button is clicked, ProcessAllForms is invoked passing the Save or Cancel methods, respectively. This behavior can be changed by overriding the btnSaveClick and btnCancelClick event handlers. Both data state buttons, btnSave and btnCancel, invoke ProcessAllChanges by passing a procedural variable. If GetForm were called in its place, the result would be the currently focused TEmbeddedForm, and its SaveForm or CancelForm function could be invoked.
It's very relevant to point out that, because TEmbeddedForm is a pure virtual class, it can very nicely be implemented as an interface. Implementing this scheme as an interface will allow pre-existing forms to be leveraged as embedded forms.
Conclusion Embedded forms provide the following business advantages as stated in the introduction of this article: improved team development by allowing multiple developers to work simultaneously on what the end user perceives as one form; improved memory management by only loading those embedded forms users have selected; and, improved code reuse through the housekeeping and data integrity code supplied in the base class of TEmbeddedNotebook.
The embedded forms described in this article were designed as a team development technique, and as such they came to life through the joint efforts of a team. I would like to credit and thank the following people for all their efforts in making embedded forms a successful technique:
The project referenced in this article is available for download.
John Simonini lives in the beautiful Pocono Mountains of Pennsylvania, with his lovely wife, two adorable sons, and two goofy dogs. John is a Senior Consultant in the employ of Source Technology Corp., and can be contacted by e-mail at mailto:wizware@epix.net or mailto:jsimonini@sourcetechcorp.com.
|
![]() |
|