CATEGORII DOCUMENTE |
Bulgara | Ceha slovaca | Croata | Engleza | Estona | Finlandeza | Franceza |
Germana | Italiana | Letona | Lituaniana | Maghiara | Olandeza | Poloneza |
Sarba | Slovena | Spaniola | Suedeza | Turca | Ucraineana |
The user sees the forms, the menus, and the reports contained in a system as the entire application. If you dont know theres an entire 'sub-application' built underneath these things that holds the whole show together, you soon will. This chapter investigates the ideas behind these invisible objects and the jobs that they do. It will delve into the architecture of that 'sub-application' and the components that comprise it. In this examination you will discover the powerful tools that are included right in the box with Visual FoxPro 6.0, as well as learn how to enhance these tools for your own development style.
These non-visible objects comprise the beginnings of an application framework. An application framework is the superstructure on which you build your application. It is similar to the frame and foundation of a house. You can put many different faces on a single house frame by using different materials, but the frame and its strength will dictate how well that house will stand.
So it is with applications. The more sturdy the underlying framework, the easier it is to build the application.
You will examine manager classes in this chapter. These classes manage the activity of some aspect of the overall application.
Initially you will review each of these areas of focus in a very general fashion. From this discussion you will gain an overview of what these special classes are all about and what is expected of them.
After the general discussion, you will take a tour of the Visual FoxPro 6.0 application wizard and builder. The wizard creates an application framework that includes objects of the class types you will be examining. Once the wizard-based application is complete, you will examine specific classes in detail. This examination will advance your understanding of the necessity and usefulness of these manager classes.
The next series of sections will discuss some of the areas of concern that you need to address with manager classes.
Form manager
Almost all applications you will build with Visual FoxPro will involve one or more forms for user interface. These forms are often launched by a command in the menu program that issues a DO FORM <Whatever> to run the form. Problems dont arise until you need to manage the forms that are open, either by setting focus to a certain form or cascading all open forms, or some other management requirement. Suddenly you find out that all of these forms are running and your application has no idea what they are or where they came from.
Using a form manager to manage the forms, you can keep track of the forms that are running and manage information about those forms. You can also keep the Windows menu up to date with the name or title of each form, manage toolbars that the forms may need, and perform other services that the forms require.
Menu manager
In the previous section, you saw that menus often call forms, but a menu can be called upon to handle much more. Menus can launch forms, call sub-menus, shut down the application, or any of a number of other actions. This means that a menu action might not be a DO FORM command. Because of the variety of possible actions a menu can request, it is a very good idea to provide a central processing point for all menu 'hits.'
The menu manager is really a communications switchboard that ensures that the request for an action gets to the correct component for processing.
Security manager
Security is one of the least understood aspects of a database application. The word security means everything from a user login and recognition system to Internet firewalls and client/server database access. I have seen security built into systems that provided field-level access restrictions for each user.
The responsibility of a security manager is to provide the level of security required by that application. By encapsulating the implementation of security, you make it easy to change the nature of that security without rewriting the entire application. Simply changing the code inside the security manager changes the security provided.
Error manager
Errors happen! Application development includes testing the code to remove 'bugs'programmer mistakesbut unanticipated situations do occur. A network cable might break, the server could go down, the user might reboot the computer, and so on. The error manager is the object that senses the problem and deals with it.
Some error handlers try to recover from every identifiable error condition; others simply record the error and shut the system down. Having an error manager object in an application simply means that the code for dealing with errors is encapsulated in the error handler. That code may or may not send messages to other application objects in an effort to recover from the condition, depending on the nature of the error.
Developers interface
The key to everything discussed so far is that each of these managers has a developers interface (a set of methods and properties). Communication with those objects occurs through that interface. All other objects in the system call on those managers to provide services.
The behavior for any one of those managers is in the code inside the methods. That implementation can change over time; however, the names of the methods and properties and the arguments for the methods should always remain the same. Following this practice allows evolution of the underlying framework without requiring changes to the other objects in the application.
Remember always: 'Program to the interface and not the implementation.' This statement is true for both application developers who are using a framework, and framework developers who are building a framework. The power of programming to the interface is that it allows the implementation of a behavior to change without requiring any changes in the code that uses that behavior.
The antithesis of programming to interface is programming to implementation. Programming to implementation means that the calling method is directly dependent on how the called method does something rather than on what it does. The only knowledge that a calling method should have of a called method is the parameters required and the value returned. There should be no dependence on how the result is derived.
This section will walk through the process of the Visual FoxPro 6.0 application wizard and builder. The wizard creates an application framework with the manager classes defined. Once the framework project file is in place, the application wizard launches the application builder that allows you to add data, forms, reports, and more to the application.
At this point you might be wondering why, after the previous chapters that gave you class design examples, we have shifted to the application wizard and its output here. The reason is that the application wizard creates a full-featured framework with all of the 'bells and whistles.' By studying this framework and its features, you will gain a better insight to the wholeness of a framework. The previous chapters focused on specific functionality and did not make a serious effort to integrate that functionality into a complete picture. The goal of this chapter is to give you the big picture. Here you will see a complete framework with its components explained.
Some of you might actually use the application wizard to build your projects; others may find the framework interesting to study while developing your own. Still others may choose to use a third-party framework. Regardless, the framework supplied with Visual FoxPro 6.0 is a good tool to use to study and learn about the manager classes and other things that create an application framework. Okay, lets get started with the wizard and builder.
Choose the Application Wizard from the Tools - Wizards menu as shown in Figure 1.
Figure 1. Choosing the Application Wizard from the Tools - Wizards menu.
Once chosen, the wizard presents the dialog shown in Figure 2.
Figure 2. The Application Wizard dialog.
We have already filled in the project name as Time and Billing and changed the current directory in Visual FoxPro 6.0 to the WizApp subdirectory of our Book directory. The only other choice in this dialog is the check box labeled 'Create project directory structure.' This check box is checked by default and we left it that way. The project directory structure created by the application wizard has a number of subdirectories under the projects home directory. In the example, the home directory is BookWizApp and the directories that the wizard created are Data (for the data files), Forms (for form files), Graphics (for icons, bitmaps and other graphics), Help (for the help system), Include (for the .H header files), Libs (for the class libraries), Menus (for the menus), Progs (for the program .PRG files), and Reports (for the report files).
After clicking OK, you get the wait window in Figure 3.
Figure 3. A wait window.
We present this figure here because on our Pentium 166 MHz machine it stayed there for quite awhile, while on our P300 it flashed by so fast we couldnt read it. While the wait window is open, the application wizard creates the directory structure for our new application and the project file for the framework.
Once the wait window disappears, the Application Wizard is finished. The wizard then opens the project file and launches the Application Builder, which displays the dialog in Figure 4.
Figure 4. Our project file with the Application Builder form on top of it.
When the dialog in Figure 4 first appears, the Visual FoxPro help file also appears. We minimized the help window to get it out of our way while working with the builder. From this point on, we'll work within the Application Builder form. You might find it helpful to keep the help window minimized but open, so you can refer to it quickly to find out the details of any particular option.
Notice that the first page in this dialog contains four distinct sections. The first section, to the upper left, asks us for the Name and Image for our application. The name is obvious in its meaning, but what is the image?
In the Image textbox, specify the image you want displayed in your application's splash screen and About box. We filled ours in with an .ICO file of a graph.
The second section in the upper right asks for the
application type you want to build. The three types available are
Table 1. The types of applications available to the Application Wizard.
Application type |
Description |
|
Your application runs inside the main Visual FoxPro screen and your menu replaces the standard Visual FoxPro menu. |
Module |
An application that is intended to be called from other applications. It therefore does not alter the Visual FoxPro environment or issue a READ EVENTS command. The menu for this type of application will be a single pad added to the existing Visual FoxPro System menu. |
Top-Level |
Creates an application that runs on the Windows desktop and not inside the Visual FoxPro screen. |
The third section, on the bottom left, titled Common Dialogs, allows you to choose which of the listed dialogs you want in your application. We chose to include them all. Table 2 lists each dialog with a description.
Table 2. The common dialog types.
Dialog Name |
Description |
Splash Screen |
A screen displayed at system startup that shows the system image and the credits. |
Quick Start |
A dialog that provides access to the forms and reports included in the application. Like a menu screen. |
About Dialog |
The About dialog in one option under the Help menu. This dialog displays the application title and author as well as any copyright statements. |
User Logins |
Including this optional dialog puts a lot more into your application than just a user login screen. It also allows the user to save preferences for the Options dialog and the Favorites menu. |
The fourth and final section of this dialog asks us about our icon. Choosing an icon file here controls the icon that will appear in the main Visual FoxPro screen, or your top-level form. All forms in the application use this icon. We chose the same icon that we used for the image.
The Credits tab, as shown in Figure 5, allows you to enter author and copyright information.
Figure 5. The Application Builder's Credits tab.
The information shown in Figure 5 is displayed in various dialogs exactly as it is entered, so be sure to enter it the way you want it to look.
All applications that deal with data need to store the data somewhere. The Application Builder uses the Data tab for this purpose. Figure 6 shows the Data tab for the Application Builder. We got the files in there by choosing our Time and Billing database with the Select button in the Builder dialog.
Figure 6. The Data tab of the Application Builder.
Notice the Form and Report columns with check boxes. If you check those boxes, the builder will create a form or report for that table. The two combo boxes below the list of tables control the form and report styles used in creating these items. Being lazy, we went ahead and checked every one of them.
The Forms and Reports tabs allow you to add forms and reports that you've already created to the application. The Advanced tab allows you to add a help file and to decide whether to include a Favorites menu option and other issues. We stop here because our focus is not on the Application Wizard or Builder, but rather on what it builds and the classes it uses.
The classes you are about to examine come as part of the Visual FoxPro 6.0 package. You will be examining these classes in some detail to learn how they are constructed and to gain some insight into their design.
Framework class design will vary according to the requirements of that framework. The design of the foundation classes provides extreme flexibility. With flexibility comes complexity. Microsoft has no idea who will use these classes and how they might choose to use them; therefore, the classes must be flexible.
In examining these classes, you won't see a distinct Form Manager or Menu Manager. However, you will find classes that deal with the issues handled by those objects.
The framework provided in the foundation classes is capable of handling applications that use a READ EVENTS environment as well as those that do not. Some examples of the latter are developers' tools or add-on sub-applications (designed to run inside other applications).
How is the Application Wizard different from the builder, and why is this important? The Application Wizard creates the directory structure and the project. When the wizard is finished, the project has nothing in it except the framework class libraries for an application. When the wizard finishes its work, it runs the Application Builder, which allows you to add forms, data, reports, and more to the project.
There is one major difference between a wizard and a builder in Visual FoxProa builder is reentrant and a wizard is not. The word reentrant, as used here, means that you can run the builder on an existing project and it won't make any alterations other than those you indicate. The original project will remain intact except, of course, for those changes you make in the builder.
The wizard will overwrite any existing project with a new one, and all of your previous modifications will be lost (the wizard cannot be reentered to modify an existing project).
Parts of the framework
Figure 7 shows the structure of the framework created by the wizard.
Figure 7. The structure of the Visual FoxPro framework.
Figure 7 does not demonstrate any kind of formal design. It is a simple block diagram depicting the way the various pieces of an application work together within the wizards framework. The abstract class _Application, found in the class library _Framewk, is the center of the structure. This class is sub-classed in the <AppName>_app library as App_<AppName>. The startup program creates an instance of the subclass when the application runs.
In the sections that follow, we'll examine many of these classes in detail. However, before you bury yourself in code, you should have at least a general idea of how this thing works.
The _Application class is a composite class. A composite class contains other classes. The classes contained in the _Application class are cusError (which handles errors), cusDataSession (which handles data sessions), cusWindowHandler (which handles forms and their windows), cusTableSort (which handles table- and cursor-level activities), and cusTableNav (which handles navigation in a table or cursor). Although this seems like a complex structure, it is simplified by the interface provided by the _Application class. The developer needs to interact only with the methods of the application object. For example, if you were to create a form and place a button in the form to move to the next record, you would call a method of the application object. The variable name for that object in a running application is goApp. The code in the Click event of your next button might be this:
GoApp.GoNext()
The GoNext method of the _Application class has the following line of code:
THIS.cusTableNav.GoNext()
This code passes the message to the cusTableNav object inside the goApp object for you. Your code is insulated from the complexity of the internals of the goApp object and can address a simple programmers interface.
This type of structure does something else for you. It allows you to alter the behavior of the cusTableNav class in any way you want without requiring that you go and change all forms that call on that behavior. You only need to keep the _Application class aware of any changes you make in the interface of the cusTableNav class. If the only changes you make to cusTableNav are in the code, and you dont change the method names or parameters passed, you dont even need to alter _Application.
_Application acts as a mediator between the code in the form button and the code that causes the actual behavior. This indirection allows you to create reusable components because it doesn't require any component to have knowledge of the environment in which it exists. The framework hides the implementation of dealing with the current data environment, as in the case of cusTableNav and moving to the next record. This design allows you to have one cusTableNav class that deals with local data and another that deals with remote data. The _Application_Application object could switch the cusTableNav object at runtime according to the type of data you are using.
Keep this concept in mind as you examine the detailed descriptions that follow. The concept of hiding the implementation exists throughout the framework.
Frameworks are abstract things and are therefore somewhat difficult to understand. You will come to understand this framework by examining the classes involved and studying the code in those classes.
The Application Wizard creates a startup program for the application. You can find that program on the Programs tab of the Project Manager. Its name, in our example wizard project, is TimeAndBilling_app.prg. The following listing excerpts some of this program:
* TIMEANDBILLING_APP.PRG
* This file is a generated, framework-enabling component
* created by APPBUILDER
* (c) Microsoft Corporation
* Framework-generated application startup program
* for D:BOOKWIZAPPTIMEANDBILLING Project
#INCLUDE [..TIMEANDBILLING_APP.H]
Notice the #INCLUDE line. This line of code reads an Include file that contains a number of manifest constant definitions. Manifest constants are like compile-time variablesthat is, the compiler will replace the constant name with the constant value when it compiles the code. Unlike regular variables, these constants cannot change value while the code is running.
The next listing shows an excerpt from the TimeandBilling.h file that defines those manifest constants:
* D:BOOKWIZAPPTIMEANDBILLING_APP.H
* This file is a generated, framework-enabling component
* created by APPBUILDER
* (c) Microsoft Corporation
* header file holding framework-generated project data for
* D:BOOKWIZAPPTIMEANDBILLING Project
#DEFINE APP_SUPERCLASS '_Application'
* developer can change this one
* to use a different global reference
* if desired -- a BUILD ALL/RECOMPILE
* is required afterwards to synch up
* references in generated menus and PRGs.
#DEFINE APP_GLOBAL goApp
* This one indicates the member name of the object
* placed on 'framework-enabled' forms:
#DEFINE APP_MEDIATOR_NAME 'app_mediator'
* developer can change these to a different subclass
* of APP_SUPERCLASS if desired:
#DEFINE APP_CLASSLIB 'LIBSTIMEANDBILLING_APP.VCX'
#DEFINE APP_CLASSNAME 'app_Application'
In this header (.H) file you see the definitions of the constants used in the startup program. The #DEFINE compiler directive is used to define these constants.
Another part of the startup program uses other compiler directives:
#IFDEF APP_SPLASHCLASS
IF NOT EMPTY(APP_SPLASHCLASS)
loSplash = NEWOBJECT(APP_SPLASHCLASS, APP_SPLASHCLASSLIB)
IF VARTYPE(loSplash) = 'O'
lnSeconds = SECONDS()
loSplash.Show()
ENDIF
ENDIF
#ENDIF
In the above listing you can see the #IFDEF/#ENDIF compiler directive. The compiler evaluates this directive at compile time for the program and either includes or doesn't include the code inside the structure in the compiled program. The #IFDEF checks to see if there is a manifest constant defined, named APP_SPLASHCLASS. If so, the code inside is included in the program; if not, the code is not compiled into the program.
What benefit does this approach give the developer? It centralizes the location for making modifications to the behavior of the startup program. Instead of editing the program itself, you can edit the header file, changing the constant definitions and causing a change in how the program is compiled.
The next listing shows more of the header file for the wizard application:
* the splash class can be anything you want:
#DEFINE APP_SPLASHCLASS 'app_splash'
#DEFINE APP_SPLASHCLASSLIB 'LIBSTIMEANDBILLING_APP.VCX'
* how long should the splash screen stay up if
* no key is pressed and if the app object initializes
* too quickly? (this figure is in seconds)
#DEFINE APP_SPLASHDELAY 3
Notice the line that defines APP_SPLASHCLASS. With this line in the header file, the #IFDEF in the previous listing would evaluate to .T. and the contained code would be compiled. Placing an asterisk at the start of the #DEFINE line for APP_SPLASHCLASS as seen in the next listing will prevent the program code in the previous #IFDEF construct from being compiled:
* the splash class can be anything you want:
* #DEFINE APP_SPLASHCLASS 'app_splash'
#DEFINE APP_SPLASHCLASSLIB 'LIBSTIMEANDBILLING_APP.VCX'
* how long should the splash screen stay up if
* no key is pressed and if the app object initializes
* too quickly? (this figure is in seconds)
#DEFINE APP_SPLASHDELAY 3
The minor change here, commenting the #DEFINE line for APP_SPLASHCLASS, will stop the compiling of the code inside the program's #IFDEF structure because the manifest constant APP_SPLASHCLASS is not defined in the header file. Consequently, the compiler evaluates #IFDEF APP_SPLASHCLASS to .F. and doesnt compile code inside the construct.
Ultimately, the startup program reaches this code:
APP_GLOBAL = NEWOBJECT(APP_CLASSNAME, APP_CLASSLIB)
IF VARTYPE(APP_GLOBAL) = 'O' ;
AND ACLASS(laCheck,APP_GLOBAL) > 0 AND ;
ASCAN(laCheck,UPPER(APP_SUPERCLASS)) > 0
APP_GLOBAL.cReference =[APP_GLOBAL]
APP_GLOBAL.cFormMediatorName = APP_MEDIATOR_NAME
#IFDEF APP_CD
APP_CD
#ENDIF
#IFDEF APP_PATH
APP_PATH
#ENDIF
#IFDEF APP_INITIALIZE
APP_INITIALIZE
#ENDIF
IF VARTYPE(loSplash) = 'O'
IF SECONDS() < lnSeconds + APP_SPLASHDELAY
=INKEY(APP_SPLASHDELAY-(SECONDS()-lnSeconds),'MH')
ENDIF
loSplash.Release()
loSplash = .NULL.
ENDIF
RELEASE laCheck, loSplash, lnSeconds
IF NOT APP_GLOBAL.Show()
IF TYPE([APP_GLOBAL.Name]) = 'C'
MESSAGEBOX(APP_CANNOT_RUN_LOC,16, ;
APP_GLOBAL.cCaption )
APP_GLOBAL.Release()
ELSE
MESSAGEBOX(APP_CANNOT_RUN_LOC,16)
ENDIF
ELSE
llAppRan = .T.
ENDIF
IF TYPE([APP_GLOBAL.lReadEvents]) = 'L'
IF APP_GLOBAL.lReadEvents
* the Release() method was not used
* but we've somehow gotten out of READ EVENTS
APP_GLOBAL.Release()
ENDIF
ELSE
RELEASE APP_GLOBAL
ENDIF
ELSE
MESSAGEBOX(APP_WRONG_SUPERCLASS_LOC,16)
RELEASE APP_GLOBAL
ENDIF
The startup program launches the application on the first line in the above listing, APP_GLOBAL = NEWOBJECT(APP_CLASSNAME, APP_CLASSLIB). This line creates an object based on the manifest constants for its class name and its class library and stores the reference to that object in the variable referred to by the APP_GLOBAL constant.
The balance of the code responds to various options selected during the app builder run.
Figure 8 shows the Classes tab of the application's project.
Figure 8. The Classes tab of the application project.
The class libraries that begin with an underscore are the Visual FoxPro foundation classes. The library named TimeAndBilling_app includes classes that the wizard created that are specific to this application. In the following sections you will see what each class library is and what each contains.
TimeAndBilling_app
This class library contains classes that are specific to this application. We will show you the first few classes as screen shots from the running application so you can see the value of the application framework classes provided with Visual FoxPro 6.0. Table 3 lists the rest of the classes in this library, with brief descriptions.
App_aboutbox
This form class is used to create the About box from the Help menu. This form shows the chosen graphic, the name of the application, and the credits information entered in the builder. This is a subclass of the _aboutbox class in the library _Framewk. There is no code in this class, which means its entire behavior is inherited from its parent. Figure 9 shows the About box for this application.
Figure 9. The About box in the wizard-generated application.
App_application
This is the main application class. The startup program uses this class to create the application object. Like all other classes in this library, there is no code in this class definition.
App_changepassword
This form class is used to change the password for the application. Figure 10 shows this password-change form.
Figure 10. The change-password form generated by the Application Wizard.
App_errorlogviewer
Use this form to view the error log of the application. The wizard-generated application records errors in an error log file. The framework creates this log file when the first error occurs. Actually, the framework asks you if you want to log the error when it occurs.
To demonstrate this, create a form named Error in the Forms directory. Place a command button in this form and put the following code in the Click event of the button:
ERROR 101
This line of code causes an error (number 101). Run the TimeandBilling application and choose Favorites-Add to Favorites from the system menu. Use the browse button to select Error.scx in the Forms directory. Confirm your choices and then choose the Error form from the Favorites menu to run it.
Click the button in the error form. The dialog in Figure 11 appears.
Figure 11. The first dialog in the error-handling system
of the wizard-generated application.
Click Yes in this dialog to record the error in an error log file. Figure 12 shows the next dialog that appears.
Figure 12. The second dialog in the error-handling
system of the wizard application.
This dialog allows the user to decide whether to continue running the application, suspend the application, or exit the application. Click Yes to continue running the application.
Now select Tools-Error Logs-Display Error Log from the menu. Figure 13 shows the App_errorlogviewer form.
Figure 13. The error log display form.
The error log records a great amount of information about the error and the environment in which it occurred. This information will be invaluable to you, the developer, when you are trying to determine the cause of the error. On the second page of the PageFrame in this dialog, the user can add notes concerning the error, such as what they were doing at the time. See Figure 14.
Figure 14. The User Notes page of the error log dialog.
The last thing to examine in this dialog is the button with the binoculars on it. Use this button to browse all records in the error log. See Figure 15.
Figure 15. Browsing the error log in the wizard-generated application.
App_favoritepicker
This form is used to add/remove a document to/from the Favorites menu. See Figure 16.
Figure 16. The app_favoritepicker form.
The rest of the classes TimeAndBilling_app
The rest of the classes in the TimeAndBilling_app class library are similar to those above. Table 3 lists these classes with a brief description of each.
Table 3. The rest of the classes in the TimeAndBilling_app library.
Class |
Description |
App_navtoolbar |
A navigation toolbar with the Top, Previous, Next, and Bottom buttons, and a spinner to select a specific record number. |
App_newopen |
A form that allows the user to open a form. |
App_options |
A form that allows the users to set system-wide options like SET CONFIRM or the 12/24-hour time display, among others. |
App_reportpicker |
A form used to select a report for processing. |
App_splash |
The splash screen at the startup of the application. |
App_standardtoolbar |
A toolbar with New, Open, Save, and Revert buttons, in addition to Cut, Copy, Paste, and Help. |
App_topform |
A form class used to provide the top-level form. This class is used when the options in the wizard indicated that the application should run outside the Visual FoxPro screen. |
App_userlogin |
The user login form. |
_Framewk
The wizard uses classes in this library to construct other classes. This type of class, one that is never used to directly create an object at runtime, is called an abstract class. Most of the remaining classes you will see are abstract classes.
In the _Framewk library is the _Application parent class for the app_application class you saw in the TimeAndBilling_app library.
_Application
As you saw in the previous section titled 'Parts of the framework,' the _Application class is a composite class - a container that acts as the mediator between the 'outside world' and the objects it contains. The following sections describe some methods of the _Application class.
The Init event fires automatically when the startup program creates an instance of the class. The Init fires as the very last thing in the creation order, meaning that all of the objects contained in the application object exist and are fully addressable.
The code from the Init of the _Application class is as follows.
IF NOT DODEFAULT()
RETURN .F.
ENDIF
The first IF in the code runs any code that might exist in a parent class' Init and respects the return value from that code. If the parent class' Init returns false (.F.) then this code will also return false without attempting anything else. A return of false from the Init of an object prevents the creation of that object.
THIS.cIcon = THIS.GetResourceFileName(THIS.cIcon, '.ico', .T.)
THIS.cImage = THIS.GetResourceFileName(THIS.cImage, '.bmp .ico .gif', .T.)
Once the parent class code returns .T., this code sets two properties of the application object. The cIcon and cImage properties are set to reflect the icon and image selected during the Application Builder run. These properties are set through method calls to the GetResourceFileName method of the _Application class. The GetResourceFileName method accepts a filename and extension and ensures that a file with that name exists.
IF VARTYPE(THIS.cReference) = 'C' AND (NOT EMPTY(THIS.cReference))
THIS.cReference = THIS.cReference
ENDIF
Next, the Init checks the cReference property for validity. The cReference property contains the name of the variable that refers to the application object. If the property has a valid value, it assigns the existing value the same property.
Why assign the property's value to the property itself? Doesn't it already have that value? Yes, it does. However, executing the code to assign that value to the property will fire the Assign method for that property. This listing shows the cReference_assign code:
LPARAMETERS tcReference
IF VARTYPE(tcReference) = 'C'
IF (TYPE(THIS.cReference+'.Name') = 'C' AND ;
EVAL(THIS.cReference) = THIS)
RELEASE (THIS.cReference)
ENDIF
THIS.cReference = tcReference
ENDIF
IF (NOT EMPTY(THIS.cReference)) AND ;
(TYPE(THIS.cReference+'.Name') # 'C' OR ;
EVAL(THIS.cReference) # THIS)
RELEASE (THIS.cReference)
PUBLIC (THIS.cReference)
STORE THIS TO (THIS.cReference)
ENDIF
The code in the Assign method shows clearly that it is validating the value being assigned and making sure that the variable name in that property is an object reference to the application being created. The first IF checks to see that the variable tcReference is a character string.
If it is, the next IF checks to see if the cReference property of the application object is filled and that it refers to the object to which this code belongs. It checks the object references by using Visual FoxPro 6.0's new ability to compare object references with the = sign-if the two object references on either side of the equal sign are exactly the same object, they are considered equal.
If the Assign method finds that cReference contains an object reference to this object, it releases the variable and assigns the new name to the property.
The line IF (NOT (EMPTY(THIS.cReference)) checks to see if the cReference property contains the name of a variable that does not refer to the application object. If the code finds this situation, it releases the variable named in cReference, declares the variable PUBLIC and assigns it an object reference to the application object.
The next occurrence in the Init is a call to the SetAppFileNames method:
IF NOT THIS.SetAppFileNames()
RETURN .F.
ENDIF
The SetAppFileNames method gets the top-level filename and the name of the module-the .EXE or .APP-that is creating this object. This information is needed to handle the IN option of the SET CLASSLIB command. The information is stored in two properties of this object: cAppFileName, which holds the application filename, and cClassContainerFileName, which holds the name of the .EXE, .APP, or .DLL that is running.
The final action in the Init of this object checks a number of conditions. The IF statement checks for an identical class name and application name, which indicates that this application was not called from another application. The IF also checks to see if the application is running with the runtime libraries of Visual FoxPro, and if Visual FoxPro started as a distributed application.
The VERSION(2) = 0 is true (.T.) if the runtime library is being used. _VFP.StartMode = 4 will be true (.T.) if Visual FoxPro was started as a distributed application. If these conditions are all .T., the application needs a read events command regardless of whether the developer set it up that way. The following code sequence shows these tests. You can see inside the IF construct that the lReadEvents property is set to .T. if the conditions are all true.
IF VERSION(2) = 0 AND _VFP.StartMode = 4 AND ;
UPPER(THIS.cClassContainerFileName) == UPPER(THIS.cAppFileName)
* if the running EXE is this file, and this file
* is not set up to be Read Events, make it Read Events anyway.
THIS.lReadEvents = .T.
ENDIF
RETURN (THIS.IsErrorFree())
Finally, the Init returns the value from another method, IsErrorFree, which has one line of code:
RETURN (ISNULL(THIS.iLastError))
If the iLastError property has a value of NULL, this method returns true (.T.); otherwise it returns false (.F.). A return of false from this method will cause the Init to also return false and therefore stop the creation of the application object. The property iLastError is set in the Error event of the object.
Returning to the startup program for a moment, notice that the next step in that program after creating the application object is to call the Show method of that object. The following code shows an excerpt from the startup program:
APP_GLOBAL = NEWOBJECT(APP_CLASSNAME, APP_CLASSLIB)
IF VARTYPE(APP_GLOBAL) = 'O' ;
AND ACLASS(laCheck,APP_GLOBAL) > 0 AND ;
ASCAN(laCheck,UPPER(APP_SUPERCLASS)) > 0
* A bunch of code is not reprinted here
* The code omitted sets paths and other housekeeping actions
IF NOT APP_GLOBAL.Show()
IF TYPE([APP_GLOBAL.Name]) = 'C'
MESSAGEBOX(APP_CANNOT_RUN_LOC,16, APP_GLOBAL.cCaption )
APP_GLOBAL.Release()
ELSE
MESSAGEBOX(APP_CANNOT_RUN_LOC,16)
ENDIF
ELSE
llAppRan = .T.
ENDIF
IF TYPE([APP_GLOBAL.lReadEvents]) = 'L'
IF APP_GLOBAL.lReadEvents
* the Release() method was not used
* but we've somehow gotten out of READ EVENTS
APP_GLOBAL.Release()
ENDIF
ELSE
RELEASE APP_GLOBAL
ENDIF
ELSE
MESSAGEBOX(APP_WRONG_SUPERCLASS_LOC,16)
RELEASE APP_GLOBAL
ENDIF
The NewObject() call actually creates the application object. The next IF statement tests to see if the application object actually succeeded in getting created. If the application did get created, this program calls the Show method for the application object.
The Show code starts out by declaring the variable llSuccess as LOCAL. This variable is used throughout the Show method to store return values from various other methods. The LOCAL declaration prevents any possible conflicts with a variable of the same name in one of the called methods.
The code in the Show method is quite simple. First it calls the SaveEnvironment method and stores the return value in llSuccess. All other actions are in an IF llSuccess construct. If any call returns .F., the rest of the IF clauses prevent other calls from being made and the code falls through to the Return command at the end.
LOCAL llSuccess
llSuccess = THIS.SaveEnvironment()
IF llSuccess
llSuccess = THIS.ValidateMetaTable()
ENDIF
IF llSuccess
llSuccess = THIS.CreateFrame()
ENDIF
IF llSuccess AND NOT EMPTY(APP_LOADING_LOC)
IF VARTYPE(THIS.oFrame) = 'O'
THIS.oFrame.Show()
ENDIF
WAIT WINDOW NOWAIT ;
LEFT(APP_LOADING_LOC,254)
ENDIF
IF llSuccess
llSuccess = THIS.ResetFormsCollection()
ENDIF
IF llSuccess
llSuccess = THIS.CreateCollaborators()
ENDIF
IF llSuccess
llSuccess = THIS.HandleProjectWindow()
ENDIF
IF llSuccess
llSuccess = THIS.SetEnvironment()
ENDIF
IF llSuccess
THIS.cUserTableAlias = JUSTSTEM(THIS.cUserTableName)
llSuccess = NOT EMPTY(THIS.cUserTableAlias)
ENDIF
IF llSuccess
llSuccess = THIS.SetCurrentUser()
ENDIF
IF llSuccess
llSuccess = THIS.ShowStartupElements()
ENDIF
WAIT CLEAR
IF llSuccess
llSuccess = THIS.Activate()
ENDIF
THIS.RestoreEnvironment()
RETURN llSuccess
Table 4 describes the methods called from the Show method.
Table 4. Methods called from the application's Show method.
Method |
Description |
SaveEnvironment |
Saves some current environment settings to properties of the application object. |
ValidateMetaTable |
Validates the structure and contents of the application's meta table. The meta table stores information used by the application while it is running. |
CreateFrame |
Creates a top-level form to contain the application if that is the way things have been set up by the developer. |
ResetFormsCollection |
Empties the forms collection array property of the application. |
CreateCollaborators |
This method is a hook for the developer's use. There is no code in it except for a return command. The developer can create collaborating objects by calling the AddCollaborator method from this method. |
HandleProjectWindow |
Handles the hiding of the project manager window. |
SetEnvironment |
Sets the environment for the application. |
SetCurrentUser |
Sets the current user, calls the login form if necessary, and sets up all properties for the current user. |
ShowStartupElements |
Creates toolbars and menus required at startup of the application. |
Activate |
Calls three methods of the application object. First it calls ClearLastError, which clears any error information. Then it calls BeforeReadEvents, which is an empty hook method for the developer to write any code he may want executed before the read events command. Finally, it calls the ReadEvents method, which actually issues the READ EVENTS command. |
RestoreEnvironment |
Resets the environment that was saved to the properties of the application object. |
The following listing shows the only excerpt of code in the Show method of the application object that is not described in Table 4.
IF llSuccess AND NOT EMPTY(APP_LOADING_LOC)
IF VARTYPE(THIS.oFrame) = 'O'
THIS.oFrame.Show()
ENDIF
WAIT WINDOW NOWAIT ;
LEFT(APP_LOADING_LOC,254)
ENDIF
This code checks to see whether there is a message to be displayed during the application's load. If there is a message, the code shows the top-level form if necessary and finally issues a WAIT WINDOW NOWAIT to display the message.
At this point, after the Show method of the application object has been executed, the application is up and running.
The code you have reviewed so far has been relatively simple. You have seen a number of different methods called during the creation process.
Some questions you may be asking yourself are, 'Why are there are so many different methods? Why not put all of the startup code into one method or even in the startup program?' Breaking up the code into individual methods with very specific responsibilities allows those methods to be called at times other than startup. The specialization of the methods provides the ability for the developer, or the framework, to execute discrete pieces of code designed to provide a specific service at any time. It removes the need to use large numbers of parameters in a single large code block in order to control what the block will and will not do when called.
In the next sections of this chapter we'll examine the Visual FoxPro 6.0 foundation classes involved in form, report, menu, security, and error management.
Form management
The _Application class is the center of all activity in the framework. It carries the responsibility of creating all other objects needed by the application. One method of the _Application class is CreateFormMediator. Its function is to create an instance of the _FormMediator class defined in the _Framewk class library.
The _Application class has a DoForm method that runs a form. The DoForm method of the application object creates the form and the FormMediator object for the form. The application uses an array property to keep track of all of the 'comings and goings' of the forms.
The FormMediator is a 'go-between' for the form and the application object. Each form has its own mediator object. Table 5 lists the methods in the form mediator with a brief description.
Table 5. The methods of the _FormMediator class.
Method |
Description |
Destroy |
Calls the application object to remove menus and /or toolbars that the form may have created. |
DataChanged |
Calls the QueryDataChanged method of the application object to determine if any data change has occurred. |
DoSessionSets |
Sets the data environment setting in the application object to match the form by calling either the application object's SetDataSessionEnvironment or ApplyUserOptsForSession methods. |
GetAppRef |
Returns the variable name that references the application object. |
LoadApp |
Creates any context menus and/or toolbars that a form may need by calling upon the application object to create them. Calls the DoContextMenu and DoToolbar methods of the application object to create the respective menu and/or toolbars. The DoForm method calls LoadApp right after the form has successfully been created. |
OutPut |
Manages table navigation by calling the DoTableOutput method of the application object. The FormMediator mediates table navigation so the application can derive the information it needs from the form. |
OutPutOneRecord |
Identical to the OutPut method except it passes the value of .T. to the DoTableOutput method of the application object. The argument of .T. causes the application object to use a scope of NEXT 1 in its efforts to move the record pointer. |
QueryUnload |
Calls the QueryDataSessionUnload method of the application object. The QueryDataSessionUnload method of the application object ensures that any data changes are handled before allowing the form to be released. |
SetDocumentToNew |
Stores the lAddingNewDocument flag of the application object to the mediator's lAdding property so the form knows which record to work on. |
Except for the name of the method called, the code in all methods of the form mediator is similar. This listing shows the code for the DataChanged method:
LOCAL loApp, llReturn
loApp = THIS.GetAppRef()
IF ISNULL(loApp)
RETURN
ENDIF
llReturn = loApp.QueryDataChanged(THISFORM,THIS.iChangeMode)
loApp = .NULL.
RETURN llReturn
After some local variables are declared, this code calls the GetAppRef method to get the variable name for the application object. The IF ISNULL(loApp) line causes the code to terminate if there is no application object; otherwise the method goes on to make its call to the application object for services. This approach of checking for an application object before calling methods of the application allows you to run a form for testing without running the whole application. When the form runs in the application, the method calls to the application object are called. When the form runs outside the application, the application object methods are not called.
Figure 17 shows the relationships between the form, the form mediator, and the application object.
Figure 17. Relationships between the form, form mediator, and application object.
By having the mediator reside in the form as a member object, you ensure that every time you create an instance of the form, you also create an instance of the mediator. This causes the mediator to go through its initialization process and register the form properly with the application object.
The application object does the actual form management. The application's DoForm method creates forms and enters them into its collection of forms, the aForms array property of the application object. The ReleaseForm method of the application object releases a form and removes it from the collection of forms.
With all of this cooperation going on, where, exactly, is the form manager? The form manager is the application object with the assistance of the form mediator. As you further examine the framework, you'll find that the application object plays many roles. Using one object to play multiple roles is neither a good nor bad approach to framework design. One advantage is that all of the code for supporting those roles is located in one place, making it easy to find and manage. On the other hand, using discrete objects for each manager role allows each of those objects to be specialized to its role, which can simplify the design and the code. The important issue here is not how many objects support the manager roles, but rather how well those roles are supported.
Report management
The application object also handles report management. A number of different methods in the application object relate to report production and management. Table 6 lists the methods that involve themselves with reports.
Table 6. Methods of the application object that handle reports.
Method |
Description |
DoReport |
The report picker dialog calls this method. The code in this method creates an instance of the Options dialog for a report using the class _OutputDialog. The _OutputDialog class is discussed in more detail below. |
DoLabel |
Calls the DoReport method. |
DoReportDialog |
Runs the document picker dialog to allow the user to choose a report. |
DoTableOutput |
Checks the current alias for the current form and produces an instance of the _OutputDialog class for selecting reports. |
ExportErrorLog |
Prints a report of the error log by creating a cursor of the entries in the log and creating an instance of the _OutputDialog class. |
RefreshFavoritePopup |
Refreshes the Favorite popup menu for all favorites. Produces a DoReport method call for the bars of the popup that call reports. |
DoFile |
Called from the Favorites menu, this method examines the requested file to see if it is a report. If the request is for a report, the method calls the DoReport method. |
Table 6 shows that the application object manages reports through a number of different mechanisms. There are a number of different mechanisms for calling reports to provide for flexibility in the ways a user may request a report.
The rest of this section assumes that you are dealing with a report created by the Application Builder. These reports follow a direct path through the application object. Figure 18 shows the Quick Start form in the application.
Figure 18. The QuickStart form in the application built with the Application Builder.
The form in Figure 18 highlights the Clients Report. The QuickStart form is an instance of the class App_FavoritePicker found in the application class library, in this case the TimeAndBilling_app class library. The class in the application library is a subclass of the _FavoritePicker class in the _Application library. The OK button in this form calls the ExecDocument method of the form.
The _FavoritePicker is a multi-purpose dialog. It serves for adding items to the Favorites menu as well as being the QuickStart form. In the ExecDocument method of the class, the code exists for handling both possibilities. The code of interest to us right now is near the end of the method and is excerpted in the following listing:
IF ALLTR(THIS.aDocuments[liRow,10]) = PJX_META_DOC_REPORT_TYPE
THIS.oApp.DoReport(ALLTRIM(THIS.aDocuments[liRow,2]), ;
ALLTRIM(THIS.aDocuments[liRow,1]))
ELSE
THIS.oApp.DoForm(ALLTRIM(THIS.aDocuments[liRow,2]), ;
ALLTRIM(THIS.aDocuments[liRow,3]), ;
THIS.aDocuments[liRow,4], ;
THIS.aDocuments[liRow,5], ;
THIS.aDocuments[liRow,6], ;
THIS.aDocuments[liRow,7])
ENDIF
The form has an array property named aDocuments that holds information about all items in the listbox. That array property is referenced in the IF statement above. The _Framewk.h header file, which is included for the _FavoritePicker class, sets PJX_META_DOC_REPORT_TYPE manifest constant to 'R'. Column 10 of the aDocuments array of the form holds the value of 'R' for reports. If the ExecDocument sees an R in the 10th column for the current choice, it calls the application's DoReport method.
The DoReport method creates an instance of the _OutputDialog class by calling the DoModalDialogClass method of the application object. Figure 19 shows the output dialog for the Clients Report.
Figure 19. The output dialog for the Clients Report.
Figure 19 shows a modification of the _OutputDialog class. The parameters passed to the dialog indicate that source changes are not allowed through the dialog. The disallowance of source changes causes the Init of the form to set the lPreventSourceChanges property of the dialog to .T. Setting this property fires the lPreventSourceChanges_Assign method for the property. The Assign method for the property hides and/or shows the appropriate controls in the dialog. If you open the _OutputDialog class you see that it also contains controls at the bottom for managing the editing of the file being processed. These controls do not show up in Figure 19 because this instance of the dialog was told that changes were not allowed.
The Click event of the OK button calls the Output method of the form. The Output method of the form passes this call to the Output method of the cusOutput object contained in the form. The cusOutput object is an instance of the class _Output in the _Reports class library.
The code in the Output method of the _Output class calls the PrintReport method of the _Output class. After checking out the settings for report production, the PrintReport method uses this code to produce the report (or label set):
IF '.LBX' $ UPPER(THIS.cReport)
* not that I think it makes any difference!!
LABEL FORM (THIS.cReport) &lcClauses &lcDestination NOCONSOLE
ELSE
REPORT FORM (THIS.cReport) &lcClauses &lcDestination NOCONSOLE
ENDIF
In the report management functionality, you see many different possible routes to producing a report. The _Application class of the framework has addressed all possibilities because it must be flexible enough to handle whatever we developers may throw at it. In following the production of a report that was created by the Application Builder, you see the clear path that is followed when the report is framework aware. Framework aware means that the dialogs involved are aware and compliant with the framework's contracts.
A contract, in the scope of a framework, is a set of rules that the components of an application must follow. For components that follow these rules, the framework will provide a high degree of services. The framework produced by the Application Wizard allows for components that do not strictly follow the contracts. However, these components will not get the full benefit of the services of the framework and, therefore, will require the developer to do more work within the component.
You will see the concept of contracts throughout the framework discussion. Previously, in the 'Form management' section, you learned that the form must have a form mediator to be framework-aware. This requirement for the mediator is a contract between a form and the framework.
Menu management
The Application Builder created two menus for you: TimeAndBilling_Main and TimeAndBilling_Go. These two menus are framework-aware in that they comply with the contracts for menus within the framework.
The contracts for menus state that the menu does not execute anything directly, but rather calls a method of the application object for the services needed. For example, on the popup menu for the File menu in the TimeAndBilling_Main menu, there is a Print Reports option. The command for this option is not a direct call to the Reports Selection dialog; rather, it is a call to the DoReportDialog method of the application object. Other options are set up similarly. They call the application object for services rather than executing things directly.
The menu delegates the operation to the application object. This allows the application object to keep track of everything that the user does from the menu.
The Favorites pad of the TimeAndBilling_Main menu not only calls methods of the application object; its contents are managed by the RefreshFavoritePopup method of the application object. The listing below excerpts from this method:
* release all but top bars:
FOR liBarNo = 4 TO CNTBAR(THIS.cFavoritePopupName)
RELEASE BAR liBarNo OF (THIS.cFavoritePopupName)
ENDFOR
This first bit of code clears the Favorite popup of all bars except the first three, which are Add to favorites, Clear favorites, and a separator line, respectively:
liBarNo = 3
* open metatable
THIS.ClearLastError()
IF NOT EMPTY(THIS.cMetaTable)
lcAlias = 'M'+SYS(2015)
USE (THIS.cMetaTable) ALIAS (lcAlias) AGAIN SHARED IN 0
ENDIF
In the above code, the first line sets the bar counter to 3 so that the first three items are not overwritten by the code that follows. The next code opens the meta data table to look up the favorite menu options for the current user:
IF NOT (THIS.IsErrorFree())
SET SKIP OF BAR 1 OF (THIS.cFavoritePopupName) .T.
SET SKIP OF BAR 2 OF (THIS.cFavoritePopupName) .T.
RETURN
ENDIF
liSelect = SELECT()
IF NOT EMPTY(lcAlias)
SELECT (lcAlias)
ENDIF
The above code responds to any error that might have occurred in opening the meta data table. If no error occurred, the code selects the alias for the meta data table as the current work area. The SetCurrentUser method of the application object, which was called on loading the application, makes a call to the SetCurrentUserFavoriteIDs method of the application object. The SetCurrentUserFavoriteIDs method sets the cCurrentUserFavoriteIDs property of the application object with data from the meta data table.
The balance of the code parses each individual favorite from the cCurrentUserFavoriteIDs property and creates a DEFINE BAR command with a corresponding ON SELECTION command and executes them. If necessary, the meta data table is used to look up any required information.
The menu passes any user actions to the application object for handling, and through this approach the application object acts as a menu handler object. Figure 20 shows the interaction of the menus with the application object.
The relationship between a menu option and the resultant behavior is indirect. The menu does not execute the behavior; rather, it sends a message to the application object requesting the behavior. The application object then processes the request and executes the appropriate behavior.
Figure 20. Relationships between menus, the application object,
and the other functionality of the application.
Security management
The _UserLogin class implements the security features of the application framework included with Visual FoxPro 6.0. The application object uses the _UserLogin class to log a user into an application. You enable this feature by checking the User Logins check box on the General tab of the Application Builder dialog, as previously seen in Figure 4.
The security features provided by the framework are minimal. The framework provides for user login only; the developer must add any other security handling by enhancing the framework classes. The section titled 'Extending the foundation classes' later in this chapter discusses the mechanisms for adding functionality to the framework classes.
Figure 21 shows the User Login form.
Figure 21. The User Login form of the
Application Builder application.
As stated earlier, this form is an instance of the _UserLogin class in the _Framewk class library. The Show method of the application object calls the SetCurrentUser method of the same object to set up the current user in the system. The SetCurrentUser method determines by looking at the lUserPreferences property of the application object whether to turn on or off the user login option. The SetCurrentUser method calls a user login form if the lUserPreferences property value is true. The code excerpt below shows this functionality in the SetCurrentUser method of the _Application class:
IF THIS.lUserPreferences
IF EMPTY(THIS.cCurrentUser) OR tlChangeUser
llSuccess = THIS.DoUserLogIn()
ELSE
llSuccess = THIS.SeekCurrentUser()
ENDIF
ELSE
* we are using a global set of preferences
* to cover all users
THIS.SeekDefaultUser()
llSuccess = .T.
ENDIF
In the _UserLogin class, the valid on the password textbox calls the CheckPasswordInfo method of the form. The CheckPasswordInfo method of the form class calls the CheckPasswordInfo method of the application object, which validates whether the password is correct for the user name entered. You can see where this is going - once again, the behavior is provided by the application object.
The Valid event for the password textbox will release the form if everything is okay. If the password is invalid, the attempts counter is incremented and the focus is set to the password textbox. If the number of attempts has exceeded the allowed attempts, reflected in the iTriesAllowed property of the form, the FailLogin method of the form is called. This listing shows an excerpt of the DoUserLogin method of the _Application class:
IF (NOT THIS.lUserPreferences)
RETURN .F.
ENDIF
LOCAL loForm
loForm = THIS.DoModalDialogClass(THIS.cUserLogInClass, THIS.cUserLogInClassLib, .T.)
IF VARTYPE(loForm) = 'O'
loForm.Show(1)
RETURN (NOT EOF(THIS.cUserTableAlias))
ELSE
RETURN .F.
ENDIF
The line RETURN (NOT EOF(THIS.cUserTableAlias)) causes the login to succeed or fail. You can see that this is true by reviewing the next listing of the FailLogin method of the _UserLogin form class:
IF RECCOUNT(THIS.oApp.cUserTableAlias) > 0
GO BOTTOM IN (THIS.oApp.cUserTableAlias)
SKIP IN (THIS.oApp.cUserTableAlias)
ENDIF
This code moves the record pointer in the user table to end of file if the login failed. Being at EOF in the use table will cause the application object to abort running the application.
If the login process is successful, the _UserLogin form leaves the record pointer on the record for the user logging into the application. The DoUserLogin method of the _Application class will then return a value of true and the system will start.
Again, you can see the interaction between the objects in the application and the application object itself. Here the user login form calls on the application object to validate the password for the user. The user login form sets the environment for the DoUserLogin method of the application object and then releases itself. The application object then reacts to the environment set by the form. You see that the application object provides all of the actual security services.
Error handling
The application object also manages the error handling. The SetEnvironment method of the _Application class contains the following lines of code:
lcTemp = THIS.cReference+'.Error(ERROR(),PROGRAM(),LINENO())'
ON ERROR &lcTemp
These two lines set the ON ERROR command that reacts to any error that occurs. The call built by these lines is a call to the Error event of the application object. If the variable referencing the application object is goApp, the macro expansion creates the command here:
ON ERROR goApp.Error(ERROR(),PROGRAM(),LINENO())
This command passes all errors to the Error event of the application object. Here is the Error event code:
LPARAMETERS nError, cMethod, nLine
THIS.iLastError = nError
The first thing the Error event does is record the error number in the application object property named iLastError. All other objects in the framework will examine the iLastError property to see if an error occurred during any particular operation.
IF THIS.lSkipErrorHandling
* special cases -- right
* now this is done when
* trying to USE the error log EXCLUSIVEly
* for purging
RETURN
ENDIF
Next, seen in the listing above, the Error event checks to see if error handling is turned off by the lSkipErrorHandling property of the application object. An example of the error handling being shut off is when the framework tries to open the error log file exclusively. During this attempt to open the file exclusively, the framework sets the lSkipErrorHandling to true. The IF statement above causes a simple return before any of the error handling code is encountered.
THIS.cusError.Handle(nError,cMethod,nLine)
Here again, we see delegation. In the line of code above, the Error event passes the error condition to the cusError object. The cusError Handle event has no code in it and derives its entire behavior from its parent class _error in the _app library. We'll examine the _error class later in this section.
LOCAL llFatal, llUserCancelled, lcCaller, lcProg, ;
llNoCodeExecuting, liLevel, lcTemp
llFatal = THIS.cusError.IsFatal()
llUserCancelled = THIS.cusError.UserCancelled()
After declaring some local variables, the Error event of the application object makes two calls to the cusError object. The first call is to the cusError.IsFatal method and the second to the cusError.UserCancelled method. Neither of these methods has code in its cusError class definition. The code is in the parent of the cusError class, _error, which is in the _app library.
IF NOT (llFatal OR llUserCancelled)
liLevel = 0
lcCaller = ''
lcProg = PROGRAM(0)
DO WHILE .T.
lcCaller = PROGRAM(liLevel)
liLevel = liLevel + 1
lcProg = PROGRAM(liLevel)
IF EMPTY(lcProg) OR (UPPER(lcProg) == UPPER(THIS.Name+'.ERROR'))
EXIT
ENDIF
ENDDO
llNoCodeExecuting = (UPPER(lcCaller) == UPPER(THIS.Name+'.READEVENTS'))
ENDIF
The above code examines the program calling stack to see where the error occurred. If the code encounters the program call to the Error event, it exits the processing. The llNoCodeExecuting variable is set to true if the code encounters the ReadEvents method of the application object just before exiting. The llNoCodeExecuting variable is referred to later in the error processing.
DO CASE
CASE THIS.iLastError # nError
* an error in the error handler!
* this will have been taken care of
* and we don't want to get
* into a recursive situation
The above listing shows the beginning of a DO CASE construct in the Error event of the application object. The first CASE checks to see if the iLastError property is not equal to the error number passed to this event. This will be true if an error occurred in the error handler itself. Under this condition, the Error event allows the code execution to fall through to the end. It does this to prevent recursive actions.
CASE llFatal OR llUserCancelled
IF THIS.lReadEvents
lcTemp = THIS.cLastOnError
ON ERROR &lcTemp
* remove reference to App Object
* in error handler *now* before
* it can start messing about with
* destroying itself
ENDIF
THIS.Release()
The next CASE checks to see if either llFatal or llUserCancelled is true. If either of those variables is true, the code inside the case statement resets the ON ERROR command and releases the application object.
CASE llNoCodeExecuting
RETRY
If the prior code found that there was no code executing, the Error event issues a RETRY command to execute the line that caused this error call again.
OTHERWISE
RETURN
ENDCASE
If none of the other CASEs were true, the OTHERWISE case is executed. This otherwise simply returns to wherever the error occurred on the line following the one that caused the error.
Once this code has finished, either the application has been shut down or the application is still running and the iLastError property contains the number of the error that caused the call.
The application object also has a method named IsErrorFree. You can see a call to the IsErrorFree method in the SetEnvironment method of the _Application class. The SetEnvironment method ends with the following line of code
RETURN THIS.IsErrorFree()
This line simply passes back the return value from the IsErrorFree method of the application object. The IsErrorFree method contains this line of code:
RETURN (ISNULL(THIS.iLastError))
This causes a return of true if the iLastError property has a value of NULL and false if the value is anything but NULL. A non-NULL value indicates that an error occurred during the running of the SetEnvironment method. How do you know the error occurred during the SetEnvironment method and not somewhere else? You know because the first line of code in the SetEnvironment method, shown here, clears the iLastError property:
THIS.ClearLastError()
The ClearLastError method of the application object has one line of code that sets the iLastError property to NULL.
The next class to examine is the _error class in the _app library. The cusError object contained in the _Application class is derived from the _error class.
The _error class has most of the code that deals with errors. Its Error method, listed below, handles errors occurring during the error handling routines:
LPARAMETERS nError, cMethod, nLine
* special case, must override
* any use of ON ERROR which
* might call this object recursively
IF 'setlog' $ LOWER(cMethod)
THIS.cLogDBF = ''
ELSE
ERROR ERROR_IN_ERROR_METHOD_LOC+':'+CHR(13)+ ;
'#'+ALLTR(STR(nError))+CHR(13)+ ;
THIS.Name+' '+cMethod+', '+ALLTR(STR(nLine))+CHR(13)+ ;
THIS.cCurrentMessage
ENDIF
The only time this event executes is if the error occurred in the cusError object. The IF checks to see if the error occurred while trying to set up the error log file for output. If the error log was set up, the error log filename property is blanked out and the event returns to the calling code.
If the error was not in the setup of the error log, this code executes an ERROR command to cause the ON ERROR handler to be called, passing the values that this event received as parameters.
Recall that in the Error event of the _Application object, the Handle method of the cusError object was called. The code that executes in the Handle method is in the _error class. The next listing shows the Handle method of the _error class.
LPARAMETERS tiError, tcMethod, tiLine
THIS.cCurrentMessage = MESSAGE()
THIS.cCurrentErrorParam = SYS(2018)
THIS.iCurrentError = tiError
THIS.cCurrentMethod = tcMethod
THIS.iCurrentLine = tiLine
THIS.cCurrentClass = ''
THIS.lUserCancelled = .F. && it's possible for an outside program
&& to ignore a previous CANCEL instruction
This first section of code sets some properties of the _error object. This allows the object to read its properties later on to display messages to the user and record the error log entry.
THIS.FillArrays()
* note: FillArrays() does an early bail for memory
* errors,which will be messaged by THIS.IsFatal() below
* see FillArrays() for structure
* of aErrorClass array --
* GetErrorAttribute
* gets a particular element by looking
* up error numbers in the first array column and specifying
* what column of the array is needed. This column
* is passed as GetErrorAttribute's first parameter
* (you can also pass a second parameter containing
* a particular error number to look up -- this defaults
* to the iCurrentError contents)
THIS.cCurrentClass = THIS.GetErrorAttribute(2)
* for example,
* THIS.cCurrentLevel = THIS.GetErrorAttribute(3)
* for a property that used a third column of
* the array to store some error severity classification system
In the above listing, the FillArrays method of the _error object is called. The FillArrays method creates, if needed, and fills an array property named aErrorClass with two columns. The first column is a delimited list of error numbers and the second column is a name for the error group. For example, the first row of the array is set to '/21/22/43/1012/1149/1150/1151/1201/1202/1507/1600/1809/1986/2000/' in the first column and 'memory' in the second column. The error numbers listed are all memory errors of some kind.
The above code goes on to call the GetErrorAttribute method of the _error object. The GetErrorAttribute will return the column passed as an argument from the row of the array that has a matching error number to the iCurrentError property, which was previously set. As the comments in the code indicate, you can call the GetErrorAttribute and pass a second argument so it will return the data for a particular error.
Also note in the comments that the GetErrorAttribute has code to bail out if the error is a memory error. This is because populating the aErrorClass could make a memory problem worse.
IF NOT (THIS.IsFatal(.T.) OR ;
THIS.IsTrivial(.T.))
IF THIS.OKToReport()
THIS.LogErrorReport()
ENDIF
IF THIS.OKToContinue()
THIS.UserHandlesError()
ENDIF
ENDIF
The above listing accomplishes much more than it looks like. The IF statement calls two methods of the _error object, IsFatal and IsTrivial. IsFatal checks for memory, disk, program, or resource file errors. If it finds that the current error is one of these, it returns true. The IsTrivial method checks for print or lock errors and returns true if the error is one of these. If either of these methods returns false, the code inside the IF is not executed. If either of these methods returns false, the error is serious and the system should be shut down.
The two IF statements inside this outer IF will check if it is okay to log the error; if so, the application can continue. The two methods called, OKToReport and OKToContinue, are abstract in the _error class. That means they contain no code and always return true. Developers can use these methods by adding appropriate code to the methods in a subclass of _error.
Inside the two IF statements are calls to LogErrorReport and UserHandlesError methods. The LogErrorReport method records the error in the error log file. The UserHandlesError method presents the user with a dialog to allow them to decide whether to continue running the application.
Figure 22 shows a diagram of the error object and its relation to the other objects in the application.
Figure 22. Relationships during error handling.
So far in this chapter, you have read about the concepts behind non-visible manager classes and application frameworks. You have examined the application framework created by the application wizard in Visual FoxPro 6.0 in some detail. You have seen forms management, reports management, menu management, security management, and error management. Regardless of whether you agree or disagree with the framework's approaches, it's good to be aware that it provides these services.
What happens if you have your own form classes that you've built over time to provide exactly what you want? Can you include forms from these classes in an application that uses the built-in framework? How about report dialogs? Can you use your own instead of the ones that are included?
The answers to these questions are the target of this section of the chapter. Here you will see a variety of ways to enhance the framework classes to make use of additional behavior and to allow the use of your own classes within the framework.
We'll examine the following mechanisms of enhancement:
In the sections that follow, you'll see some examples of modifications to the framework foundation classes using the methods in the above list. Unfortunately, space limitations preclude examining all of the possible modifications that you might want to make, so we have selected some general examples. These examples should give you a good start on making your own modifications.
The first mechanism of modifying the framework classes is direct modification of the class code. While this mechanism is direct and easy to understand, it is the least desirable of all of the mechanisms. The downside of this approach is that when a new version of the foundation classes becomes available, you cannot simply replace the libraries and run with it. You have to redo all of your modifications before you can use any features of the newer version of the framework. Even more important, though, modifying the framework classes requires that you fully understand the implementation of each method you change. The other mechanisms for enhancing the framework require less understanding of the implementation of the behaviors.
To demonstrate this technique, we'll modify the DoForm method of the _Application class so it will be able to pass additional parameters to the form called. The listing below shows the code of the DoForm method, excerpted to show only the code where changes are made:
LPARAMETERS tcFileName,tcClass,tlNoMultipleInstances,tlNoShow, tlGoMenu, tlNavToolbar
IF NOT EMPTY(lcClass)
THIS.aForms[THIS.nFormCount] = ;
THIS.Instantiate(lcClass, lcFileName)
ELSE
DO FORM (lcFileName) NAME THIS.aForms[THIS.nFormCount] LINKED NOSHOW
ENDIF
RETURN (THIS.IsErrorFree())
The following listing shows the same code segments with our changes made to them:
LPARAMETERS tcFileName,tcClass,tlNoMultipleInstances,tlNoShow, tlGoMenu, ;
TlNavToolbar, tcExtraParms
IF NOT EMPTY(lcClass)
THIS.aForms[THIS.nFormCount] = ;
IF EMPTY(tcExtraParms)
THIS.Instantiate(lcClass, lcFileName)
ELSE
THIS.Instantiate(lcClass, lcFileName,, tcExtraParms)
ENDIF
ELSE
IF EMPTY(tcExtraParms)
DO FORM (lcFileName) NAME THIS.aForms[THIS.nFormCount] LINKED NOSHOW
ELSE
DO FORM (lcFileName) NAME THIS.aForms[THIS.nFormCount], &tcExtraParms LINKED NOSHOW
ENDIF
ENDIF
RETURN (THIS.IsErrorFree())
Making this change here requires that the Instantiate method accept the parameter. However, examining the Instantiate method shows that it already addresses this requirement in its fourth parameter, tcParamString. The LPARAMETERS line is from the Instantiate method of the _Application class:
LPARAMETERS tcClass, tcClassLib, tcClassContainerFileName, ;
tcParamString, toParent, tcMemberName
The contents of tcParamString are macro-expanded to pass them on to the object being created. This is a good example of the flexibility of the framework classes. Youve modified the Instantiate method without needing to modify the DoForm method because the DoForm method already had the capability you wanted.
In this particular example of modifying the framework classes directly, you did not encounter a cascading modificationone that requires changing other methods, which in turn require changing still other methods. This is another reason that direct modification of the framework classes is a less desirable way to add features to the framework.
This technique, subclassing the framework classes, is better than direct modifications, though it still has it limitations. Subclassing to modify behavior requires that you have an intimate knowledge of the methods you are overriding and their functionality. You must know all of the requirements that the framework places on a particular method, or else you stand the chance of causing the framework to function incorrectly.
As an example of subclassing, we will create a subclass of the _FormMediator class in the _Framewk library. We will add this class to the TimeAndBilling_app library as MyFormMediator.
You might encounter a problem while editing the MyFormMediator class on your machine. This is because the VCX stores the path to the _Framewk class library. Therefore, if you installed Visual FoxPro into a different directory than we did, you'll encounter an error when you try to open the class for editing because Visual FoxPro cannot find the library. You can fix this by following these steps:
1. USE LibsTimeAndBilling_app.vcx form the WizApp directory.
2. BROWSE the class library file.
3. Find the record with the value of _formmediator in the Class field.
4. Update the Classloc field to point to your Visual FoxPro installation directory.
5. Close the class library by issuing a USE command in the command window.
The following listing shows the code in the OutPut method of the _FormMediator class:
LOCAL loApp, llReturn
loApp = THIS.GetAppRef()
IF ISNULL(loApp)
RETURN
ENDIF
llReturn = loApp.DoTableOutput()
loApp = .NULL.
RETURN llReturn
In MyFormMediator, you will enhance this code to check first for a custom form property to see if output is a valid option. This listing shows the code in your MyFormMediator class:
LOCAL llRet
IF VARTYPE(THISFORM.OkToOutPut) = 'L'
IF THISFORM.OkToOutPut
llRet = DoDefault()
ENDIF
ELSE
llRet = DoDefault()
ENDIF
RETURN llRet
First, the code declares a local variable for holding the return value. It then checks to see if there is a property of the form named OkToOutPut and that it is a data type of logical. If the property exists and is logical, it calls the default code of the parent class (_FormMediator) if the value of the property is true. If the form property does not exist or is some other data type than logical, this method calls the default code in _FormMediator. In the default calls, the return value from the _FormMediator method is stored in the variable and eventually returned by this code to the caller.
To accomplish the modifications above, you need to know that the OutPut method of the _FormMediator class returns a logical value. This is not a lot of information to know about the method; however, it does show you that subclassing requires some knowledge of the parent class implementation. Other subclassing modifications might require a great deal of knowledge of the parent class code.
The framework class code in many places calls a 'hook method' before executing the default behavior. These hook methods return a logical value. A return of false will stop the default behavior, while a return of true will allow the default to occur.
One such place is in the _Application class DoForm method. A BeforeDoForm method provided in the _Application class has no code in it. The developer can use this BeforeDoForm method to decide whether the DoForm operation should occur.
To prevent making modifications to the _Application class and therefore creating a problem if a newer version of the library becomes available, you can write the code in the BeforeDoForm method of the App_Application class in the TimeAndBilling_app library. By writing your code in the subclass, you can then install a newer _Framewk library without needing to redo your code in the BeforeDoForm method.
The following listing shows some code from a possible BeforeDoForm method:
LPARAMETERS tcfilename,tcclass,tlnomultipleinstances,tlnoshow,tlgomenu,tlnavtoolbar
LOCAL llRet
llRet = .T.
IF TYPE('_screen.activeform.name') = 'C'
IF TYPE('_screen.activeform.Allownewform') = 'L'
llRet = _screen.activeform.allownewform
ENDIF
ENDIF
IF NOT llRet
Messagebox('The active form does not allow running other forms', ;
0,'New form not allowed')
ENDIF
RETURN llRet
The first thing to notice is that, when you start to modify the BeforeDoForm method, the parameter statement is already there. This is new functionality in Visual FoxPro 6.0. If the parent class has a parameter statement, that statement automatically appears in the child class methods. You no longer need to keep looking up the parameters when working with subclasses.
The code we added to the BeforeDoForm method is simple. It checks to see if there is a currently active form; if so, it checks to see if the form has an AllowNewForm property. If it does, the code returns the value of that property. We modified our Error form from earlier in the chapter to give it the AllowNewForm property and set it to false for testing this code.
To see this behavior, you can run the application TimeAndBilling.app and choose the Clients form in the QuickStart form that is displayed at startup. While the Clients form is visible, select the Error form from the Favorites menu. The Error form will run because the Clients form does not have an AllowNewForm property. If you choose the Error form on the Favorites menu again, a message box will tell you that new forms are not allowed.
Using the hook methods provided by the framework classes is a very good mechanism for altering the behavior of the framework. It is a good approach because the designer of the framework intended those methods to be used that way. As long as you are writing your code in a subclass and not making modifications to the classes in the foundation class libraries, this approach can be very useful and will not present problems for you later.
Creating a decorator class is another good way to modify the behavior of the framework classes. As an example of this, you will see a class named MyFormReleaseDecorator. The purpose of this decorator is to stop the release of a form when there is an edit in progress.
To make this work, we created a subclass of the _form class in the _base library named App_form and placed it in the TimeAndBilling_app class library. To the form class, we added a property named lEditing that is logical and holds a true if an edit is in progress in the form.
Then we modified the Release method of the form class to the code in the following listing:
IF NOT THISFORM.MyFormReleaseDecorator.Release()
NODEFAULT
RETURN .F.
ENDIF
IF NOT DoDefault()
NODEFAULT
RETURN .F.
ENDIF
This code calls the MyFormReleaseDecorator before it calls the default release code of the _form class. If the decorators Release method returns false, this code returns .F. after issuing a NODEFAULT to stop the release of the form. If the decorator returns true, this code goes on to call the form class default behavior with a DoDefault(), and if the class Release method returns false, this code issues a NODEFAULT to stop the release of the form and then returns .F.. MyFormReleaseDecorator is in the TimeAndBilling_app library as a subclass of the custom base class. To the MyFormReleaseDecorator class, we added a method named Release. This line of code shows the Release method of the new class:
RETURN NOT THISFORM.lEditing
This simple return statement will cause the App_form class release to stop if the lEditing property is true. If the lEditing property is false, the releasing will continue in the _form class code.
The last thing we did was to add an instance of MyFormReleaseDecorator to the App_form class and changed its name from MyFormReleaseDecorator1 to MyFormReleaseDecorator, without the '1' on the end.
You could now create forms within the application using the App_form class and get the behavior of disallowing release when the lEditing property is true.
This simple example of a decorator class shows you the mechanism involved. You can create decorators for almost anything you like. Using decorators is a good way to enhance the functionality of the framework. One advantage of this method is that you arent making any changes to the framework classes themselves, thereby leaving them easy to update with new versions.
A decorator is different from a mediator in that the decorator does not have a two-way conversation between objects. A decorator simply alters the behavior of one class alone. In the next example, we'll use a mediator class to modify the framework behavior.
For a mediator example, let's return to the MyFormMediator class we created earlier. The first thing you need to do is to get the framework to use the new MyFormMediator. To do so, change two properties in the app_Application class in the TimeAndBilling_app class library. Figure 23 shows the change made to the cMediatorClass and cMediatorClassLib properties.
Figure 23. The property sheet for the app_Application class showing the
changes made to the cMediatorClass and the cMediatorClassLib properties.
Let's focus on the first two properties in Figure 23. The cMediatorClass holds the name of the mediator class to use as a form mediator, and the cMediatorClassLib holds the name of the class library that contains the cMediatorClass class.
By making the above changes, the application will now use your form mediator instead of the default mediator for all forms launched. You can now modify the behavior of MyFormMediator as you like, and all forms in the application will gain the new behavior.
Using mediators is another effective mechanism for modifying the behavior of the framework. The real advantage of using mediators is that the mediator is already part of the framework design. You only need to subclass it and alter the behavior. In other situations, you might actually be adding a mediator in a place where the framework doesnt have one. In these situations, you only need to deal with the mediator and the class that uses it. There is no need to modify the framework, because the mediator only calls the framework methods and doesnt need to be called by those methods. The framework doesnt need to even know that the mediator is there. The object using the mediator calls the mediator, and the mediator, in turn, calls the framework. Whatever value is returned by the framework is simply returned back to the object using the mediator.
By using mediators, you can even create objects that work well in different frameworks. You can do this by writing code in the mediator that figures out which framework it is running in and makes the appropriate framework calls. The objects using the mediator dont even know which framework they are running in.
In the first section of this chapter, you saw the abstract concepts of non-visible manager objects. You examined the idea of the application framework acting like the frame of the application, upon which you hang your forms, menus, reports, and other components. The framework, through its manager objects, handles the housekeeping issues and provides services to the other components.
Programming to interface may be new to you. In this chapter, you saw that there are very powerful benefits to using this approach to system design because it allows you to use interchangeable components much more easily. Since you program to the interface, which is composed of methods, their parameters, and the properties of the various classes, you can swap components at will as long as each component expects and honors the same interface. Because the framework provides a standard interface for all components, creating interchangeable classes is easy.
By following the program-to-interface principle, you also allow the evolution of both the framework and the components without negative effects on the other. If you enhance the behavior of the framework, but maintain the same interface to components, the existing components will get the enhanced behavior without you altering their code. The same is true for modifying the components, as long as you maintain the interface design. Programming to interface is the most important lesson in this chapter.
You took a tour of the Visual FoxPro 6.0 Application Wizard and Builder. The wizard created a framework for an application and the builder allowed you to add components to that framework.
The framework created by the wizard was used as the example for seeing all of the various non-visible manager classes. You examined the framework to see each of the manager objects described in the opening section.
To pull together all of the manager classes, see Figure 24.
Figure 24. The big picture of the Visual FoxPro 6.0 application framework.
In Figure 24, you can see all of the manager operations together. This diagram is a composite of the diagrams you saw in the individual discussions of each manager operation. You can see that the application object is at the center of all activity in the framework. Every other component in the system uses the application object to provide the services it needs. This centralized control allows the framework to handle the entire housekeeping of the system because it is aware of everything that happens.
The application object knows about every form that exists, and every menu and toolbar created. It is aware of all errors that occur and the environment in which they occur. The application object knows what data session the forms are using, it can determine the status of any data session, and the tables open there. Essentially, the application object is the 'grand poohbah' of everything that happens inside the system.
In the last section of this chapter, you saw a number of methods for making modifications to the framework. In the examples, you saw new behaviors introduced, and existing behaviors changed.
You also saw that some methods of modifying the behavior of the framework are more desirable. Modifying the classes in the framework library was the least desirable of the four methods shown: direct modification of the framework classes, subclassing, decorator classes, and mediator classes.
You may choose to use the framework created by the application wizard for your work, you may choose to create your own framework, or you may use a third-party framework. Regardless, the contents of this chapter should give you a head start on understanding the nature and design of an application framework.
Politica de confidentialitate | Termeni si conditii de utilizare |
Vizualizari: 1574
Importanta:
Termeni si conditii de utilizare | Contact
© SCRIGROUP 2024 . All rights reserved