[ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7] [ 8 ] [ 9 ]

This sample project hook subclass takes the following actions when you bring forms into the project:

  • It adds an app mediator object to forms you bring into the project (this mediator object may be the base mediator object from _FRAMEWK.VCX or any subclass of this object that you nominate).
  • It adds code to the form QueryUnload event, clearly marked so you can distinguish it as generated, that accesses framework behavior through the mediator object. It will not touch the code you may already have written in the QueryUnload event, but you will need to review its changes for integration. It gets this code from a template program called TFRM_QUL.PRG in the PHOOK folder of your source code, which of course you can edit as needed.
  • It adds code to the mediator's LoadApp and DoSessionSets methods, ready for you to edit, which serve to show you the general method the mediator uses to "talk to" the app for extension purposes. It uses the template programs TMED_LA.PRG and TMED_SS.PRG to derive this sample code.
  • It adds information about all its actions (and those of its superclass, the shipping project hook) to a text log, which it places (excluded) in your project. Although you can't see it in the figure above, this project hook log is additive, so you should eventually have a log of all the "automatic" actions taken.

In your project hook object, you might want to add code or template instructions like the ones I've written in these TMED_*.PRGs, to additional mediator methods, such as SetDocumentToNew(), PickRecordToWorkOn(). You might want to adjust the Output… group of properties and methods, or add context menu and toolbar information. (See above for what these items do.)

You might also add these values and behavior in your form mediator subclass, rather than writing it into the actual form level mediator object. However, much of this behavior is data-specific, and really belongs on the document level. All of it is team-data-handling-practices specific, which is why it is left up to you to figure out how you might want to make use of it. To see why, here's an example of what you might want to do in PickRecordToWorkOn():

 * sample mediator.PickRecordToWorkOn() code
   * -- you may be changing this
   * directly in the form or in the
   * subclass of _formmediator that
   * you choose to use in your form classes
   * or you could SELECT here, but
   * this is up to you
   * the following setting, lAdding, is
   * known from the app and that's all!
   IF THIS.lAdding
   * or other test as appropriate for you
   * or other action as appropriate for you
   BROWSE && ?? or check a current ID held by the app object? ENDIF

… As you can see, you have some decisions to make before you implement this connection of the mediator to the application in the way most advantageous to your development style. The application framework pieces don't even call this method, because the app has no way of knowing how and when you load your data. When you write the code above, you also decide when in your initialization procedures you want to call it.

The sample project hook class I've written has a little extra logic to handle formsets. In essence, if it finds a formset completely "un-framework-enabled", it adds mediators to each form in the set. However, if there is already a mediator in any of the forms, it will assume that any un-framework-enabled forms are meant to be without mediators. This is similar to the logic that the application object uses to when dynamically adding mediators to formsets at runtime.

This sample does not touch form classes you add in VCXs, but it definitely could. As you'll see when you investigate this project hook subclass, the relevant code is so easy it's almost embarrassing to think about how we struggled to achieve this sort of intervention in the past !

Runtime-generated components

As we've just seen, when the application object needs something at runtime, such as the ability to "framework-enable" a form with a mediator, it can be pretty resourceful. The other components it creates at runtime are an error logging table and a user preferences table.

The error log and error handling in the framework is actually the responsibility of one of the application object's FFC components (_error, from _APP.VCX). The application object tells this component where it would prefer to place the log, and what the log table's name should be (using its cErrorLogTableName property). It adds the error log viewing dialog described at the end of the last section, as a more appropriate display for a full-fledged application than the _error's default display mechanism. It augments the error member's handling to set the app object's own iLastError flag. Other than that, the application object mostly just stays out of its cusError member's way until cusError has handled the error. Afterwards, the app object queries cusError's results to find out what, if any, choices the user may have made and how severe the error was.

By contrast, the application takes care of a user preferences table that is a completely framework-specific component. If you have disabled user log ins for this application, the table still exists to store an application-wide set of preferences, shared by all users (similar to the behavior you get when you turn off user profiles in Windows 95).

This table stores:

  • user passwords and user levels for each record. The user levels are not directly used, but are intended to allow you to assign permissions for users with some external logic. There is an abstract method, SetUserPermissions(), called at an appropriate moment, to allow you to determine the level of the current user and decide what to do about it, according to any system you like.
  • user options (described below)
  • user favorites (for use in the Favorites menu popup). If you look at the information in this field of the user table, you'll find that the application stores the information in a straight text format. IDs for favorites are stored as the fully-pathed name, where the user has indicated a Favorite by picking a file from disk, or as a record number in the metatable, where the Favorite was chosen from metatable entries. The later identification technique may seem simplistic, and some provision has been made for you to switch to a different form of identification if you prefer. However, this method prevents the App Builder, project hook, and other mechanisms that might add records to the metatable from having to generate unique IDs of any sort (the metatable is not in a DBC).
    To avoid potential problems when the metatable is edited, the user favorites information stores LUPDATE() information for the metatable. You'll see this information in the first line of the memofield. The application object warns the user if the metatable has been edited since s/he last determined his or her Favorites list, and gives the user an opportunity to clear the Favorites list at that point.
  • user macro sets. These are allowed only in READ EVENTS applications, so that the environment can be saved and restored properly.
  • a "notes" field you can use for user feedback during acceptance testing period, for the users to store draft help information, or any other purpose you like.

In addition, you may add fields to this table. See the application object's CreateUserTable( ) method for more precise information about the table's required structure and tags.

Invoking New App-Specific Pieces

You'll find that it's easy to add functionality to the application object by subclassing it to suit your needs. For example, I've mentioned that the framework is somewhat biased against collaboration, because it can't require forms to have knowledge of the framework. For this reason, the framework DoForm() method doesn't pass parameters to forms.

However, in your development work, you may know exactly what forms and form classes are invoked in any situation, and decide that parameter passing is okay.

I've written an example NewApplication subclass, which you'll find in the APPSUB\NEWAPP.VCX. It has one new array property and one new method (DoFormWithParams) to show you how I recommend you add this ability. There are also some comments about alternatives in the method.

You could create this code and add this property to any generated app_application subclass. Then, as indicated earlier, you could use a slightly different version of the Application Wizard to migrate this code and property to the standard superclass for your future generated app_application classes.

You can easily give your new application subclass new features by giving it new members, similar to the FFC-descended members it already possesses. (My other session on application testing techniques will show you a framework-generated example application with a new "runner" member object, to facilitate automated demo and testing runs.)

For most of you, however, the most important part of adding application-specific features is adding knowledge of new programs, forms, and reports into the application. You've already seen that you can put items into the application's metatable to give the framework some knowledge of them. However, all you have to do to make the application aware of any new source code elements, to take care of them at runtime, is to pass them through one of the provided application Do… methods. I've mentioned the DoForm() method a few times.

The application has a Do.. method, like DoForm(), specific to each type of VFP source code. If you're completely unaware of the type of process you are trying to invoke, simply use DoFile() with the source code file name, and let DoFile() channel the file appropriately. If you do know what type of process you're executing, use the source-code type specific Do.. method for your type of source code directly, because you can then add appropriate information into its optional parameters, specific to this source code type.

Where would you issue the Do… method? From a user's perspective, menus are the glue that hold most VFP applications together. You can invoke any process from the menu with an APP_GLOBAL.Do… method call. (Here, as explained earlier, APP_GLOBAL is a #DEFINE that will resolve to the application object's reference variable.) These calls will also go into toolbar buttons and other techniques for starting processes that you devise in your app.

Using the Do.. methods allows the framework to validate your calls, and to maintain information about any modaless components, such as forms and toolbars, you want to manage in your application.

When you're working with apprentice programmers, you'll find it's a lot cleaner to tell them that they instantiate a toolbar by saying APP_GLOBAL.DoToolbar(tcClassLib, tcClass), and they instantiate a form by saying APP_GLOBAL.DoForm(tcFileName [,tcClass] [,tlNoMultipleInstances] [,tlNoShow] [,tlGoMenu] [,tlNavToolbar]), and that they even "instantiate" a menu by saying APP_GLOBAL.DoMenu(tcFileName [,tlUniquePopupNames]), rather than worrying about the various sorts of VFP syntax. You'll also find that writing only these application method calls into your menus, with no procedures hidden in menu cleanup, makes debugging somewhat easier and also scopes the code properly. The latter may not be a concern until you start working with modular apps, where the running APP or EXE doesn't necessarily contain all the code for the full application. But anybody who has ever heard a beginner say "I try to fire a menu option and it asked me where my procedure was" will appreciate the value of good habits in this respect.
Suppose some day you decide your team should use object-oriented menus, with Colin Nicholls' custom VFP classses or some other system. Or, suppose VFP someday gets OO menus internally. They'd all have different syntax requirements. However, from the point of view of your novice programmers, the process will not have changed. They will still use APP_GLOBAL.DoMenu(…)to create each menu -- even though you will have written code into this method to provide a different sort of menu.

More Framework Customization: the User Option System

As you've seen, you can configure a framework application through the normal OOP-paths of subclassing and changing property values. You can use Builder- and Project Hook- assisted methods to incorporate new application-specific elements. The framework has another special way to allow customization of values that you'll set from values saved and set at runtime. This is the "Options" system.

If you have chosen to allow user logins for an application, options are saved on a per-user basis. If you haven't, they're saved for the application as a whole.

Since the framework maintains these values, it has a defined and fairly strict storage system defined for them. The aCurrentUserOpts[ ] array property of the application object, which has four columns, holds the current values. The User_Opts field of the user preferences table holds the currently-saved values for this user, as a LOCAL array with the same format. The framework takes care of copying these items back and forth as necessary.

This array's format and its use are strictly defined by the framework, but its contents are completely up to you. The _options dialog delivered in _FRAMEWK.VCX is the nearest thing to a "sample" rather than a real-life-application component that the framework has. Although this dialog is usable as-is, you should not assume that the options you see in this dialog are meaningful to the framework.

The application object applies options to various application elements without actually knowing what they are, as you'll see in this section. It only knows how you've classified each option you want, according to what you've put in the array.

[ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7] [ 8 ] [ 9 ]