This sample project hook subclass takes the following actions when you bring forms into the project:
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 IF EMPTY(ALIAS()) RETURN * or you could SELECT here, but * this is up to you ENDIF * the following setting, lAdding, is * known from the app and that's all! IF THIS.lAdding IF GETFLDSTATE(1) #3 * or other test as appropriate for you APPEND BLANK * or other action as appropriate for you ENDIF ELSE BROWSE && ?? or check a current ID held by the app object? ENDIF THISFORM.Refresh()
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 !
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:
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.
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.