While the topic of this paper is Working with Libraries in Paradox for Windows, it is really more general than that. It is a paper on how to call ObjectPAL methods which are external to the form, library or script that is calling them. The main trick to working with ObjectPAL libraries is calling the methods that are stored in them. The solution for calling methods in a library is the same as the solution for calling methods that reside in another form. The most common scenario when calling custom methods found outside of a form, library or script is to call them in a library. For the sake of simplicity this paper will assume that a form is calling methods in a library.
Libraries are repositories for code. The reason for using them is to allow for sharing code between one or more forms, rather than replicating the code across several forms.
Here are the basic steps to calling a method in a library. They will not mean much until you have finished the explanation and examples, but once the fundamentals are understood, this serves as a good check list.
Simplistically put, a library is an invisible form whose purpose is to serve as a repository for custom methods. It is a place to put code that is required by more than one form. This means that if the code that is used by more than one form is stored in a library, changing a custom method means only changing it in the library. The alternative is to copy that code across all forms needing it. If that technique is used, changes would have to be made to all copies of the code in many different forms. With the code in one place it is far easier to maintain. Libraries are also used as a mechanism that allows values to be shared between Paradox forms.
There are only three event methods in a library: open, close and error. They are the various windows found on the Methods tab of the Object Explorer: Uses, Var, Type, Const and Proc. As with forms, all custom methods in the library are also listed on this page of the Object Explorer. Although a procedure in a library may not be called from another form, the procedure window exists in a library so that multiple custom methods in the library may call any custom procedure placed in this window.
Creating a custom method in a library is the same as in a form. Press enter when <New method> is highlighted and enter the name of the method in the New Method dialog box. The text in the method window that follows, will consist of:
method customMethodName([var parameter1 smallInt, parameter2 string...])
endMethod
Figure 1 - Syntax for creating a custom method.
If any parameters are necessary, include them in parenthesis of the method. They can be passed in one of three ways: by value, by reference or as a constant. All parameter declarations must be followed with a valid ObjectPAL variable type. If the method is to return a value, the ObjectPAL type that it will return must be declared immediately following the closing parenthesis of the method statement. Once the methods have been written, save the library.
In the example of the custom method, passingExample, the first parameter, siChangeOneChangeAll, is a small integer passed by reference. The small word "var" that precedes siChangeOneChangeAll designates it as being passed by reference.
In the example of calling passingExample, assume the variable, siValWillChange, is passed by reference to passingExample as the first parameter. If this happens, siChangeOneChangeAll is passed a pointer to the siValWillChange variable. Since it is a pointer to the original variable, changes to siChangeOneChangeAll will also change siValWillChange.
siValWillChange = 32767
strWillNotChange = "This variable will retain its value"
dDontTryToChange = date("1/1/01")
if lbVar.customMethodName(siValWillChange, strWillNotChange, dDontTryToChange) then
message("Hallelujah!")
else
message("You have failed me!")
endif
; Values after
;siValWillChange = 6
;strWillNotChange = "This variable will retain its value"
;dDontTryToChange = 1/1/01
Figure 2 - Call to the custom method passingExample.
Library Method - passingExample
method passingExample(var siChangeOneChangeAll smallInt,
strCanBeALiteral string, const dCannotChange date) logical
strLiteral = "NewValue"
siVar = 6
tc.insertRecord()
; dConst cannot be changed because it is a constant
tc."Date Field" = dConst
if tc.unlockRecord() then
return true
else
tc.deleteRecord()
return
false
endif
endMethod
Figure 3 - The custom method PassingExample.
A limitation of passing by reference is that since the parameter is a pointer to a previously existing variable, only variables can be passed. In the passingExample method, siChangeOneChangeAll cannot be passed the small integer 32767, instead the value must be assigned to a small integer variable and then passed.
In passingExample the second parameter, strCanBeALiteral, is a string passed by value. A parameter can only be proceeded with "var" or "const" or nothing at all. If it is not proceeded with anything, the parameter will be passed by value.
Assume that a variable, strWillNotChange, is passed by value to passingExample as the second parameter, strCanBeALiteral. strCanBeALiteral does not point to strWillNotChange, instead it is a copy of strWillNotChange. Since it is a copy of the original, changes to strCanBeALiteral will have no effect on strWillNotChange.
Since passing a parameter by value means creating a copy of the value passed, literals can be passed. It is not necessary to assign "This variable will retain its value" to a variable first. The value can be directly passed.
lbVar.passingExample(siValWillChange,
"This variable will retain its value", dDontTryToChange)
Figure 4 - A call to passingExample, the method in figure 3.
The downside is that since a copy of the method is made there is the overhead of creating a new variable whenever passing by value. This overhead will typically be insignificant in the case of small variable types such as string and numbers. When passing memos that are megabytes in size, the overhead involved becomes too burdensome for most systems.
To pass value as a constant, proceed the parameter with "const". In the call to passingExample, if dDontTryToChange is passed as a constant to the parameter, dCannotChange, there is no need to even be concerned about what effect changes to dCannotChange will have on dDontTryToChange. This is because dCannotChange is a constant and as such cannot be modified in any way.
Calling a custom method from within a form, library or script that does not have that method registered results in a compiler error. The compiler searches methods in the Paradox libraries for a run time library (RTL) method by the name used in the call. If it is not found there, the compiler searches for the method as a custom method or custom procedure in the form. If the method is not a custom method in the form either, the compiler halts with the error "Unknown method name." As far as the compiler is concerned, if the method is neither in the form nor the Paradox libraries, it is an invalid call. At least this is the case unless the form is "told" otherwise.
Telling the form about custom methods outside of the form is the purpose of the Uses window. In the Uses window it is possible to create a list of methods that are external to the form. In essence it is a way to tell the form that although the methods listed in the Uses window are not in the Paradox libraries or in the form itself, they do reside in a library(ies) external to the form.
Before listing the methods and their syntax, the type of library must be specified. This is done rights after the word "Uses". For accessing methods in ObjectPAL libraries and forms, the key word "ObjectPAL" must follow Uses. When calling methods in DLL's the name of the DLL (without the extension) follows the Uses word.
This list of methods in the Uses window must contain the complete syntax of each method. The easiest way to do this is to copy the syntax straight from the method statement of the custom method itself. Just leave out the word "method " at the beginning of the statement. Copy everything else which will include the name of the method, its parameters and the type returned (if any).
Uses ObjectPAL
passingExample(var siChangeOneChangeAll smallInt,
strCanBeALiteral string, const dCannotChange date) logical
endUses
Figure 5 - The declaration of passingExample in the Uses block. This lets the form know that calls to this method are valid even though it is neither a custom method in the form, nor a method in the Paradox Run Time Libraries.
Since the methods exist outside of the form, it is necessary for the form to have some way of knowing where to find the library containing the methods. This is the reason for using library variables.
Declaring a library is just like declaring any other variable, it is done within a var..endVar block.
var
lbUtil library
endVar
Figure 6 - Declaring a library variable.
A decision must be made as to where the library variable should be declared. This determines the accessibility of the library to an object(s) that requires it. The same is true for the Uses declaration. Where it is declared depends on the functionality of the form but here are some general guidelines:
Opening a library variable is similar to opening a tCursor or a form. It is done by calling the open method while referencing the library variable. As with any version of the open method, the open method for library variables returns a logical value. This allows for easy testing as to the success of the opening of the library. Since code in the form relies on the methods in the library, it is important to test for the eventuality that the library cannot open.
If not lbUtil.open(":SomeAlias:Util") then
errorShow()
close() ; Close the form
endVar
Figure 7 - Opening a library variable.
method pushButton(var eventInfo Event)
lbUtil.customMethodName(siValWillChange, strWillNotChange, DontTryToChange)
endMethod
Figure 8 - Calling a custom method in a library.
The library variable will serve as a pointer to the library. Calls to methods using the library variable will force Paradox to search the library for the method referenced through the dot notation path.
As of Paradox 7 it is no longer necessary to list each individual method for a library in the Uses window. The compiler will search through the symbol table of a library or form and "discover" the methods that exist in the library. In order to do this the compiler has to know what library or form to look in. To use this technique, the path and name of the library, as a string, replaces the list of methods in the Uses window. It is absolutely necessary that the extension of the library or form be included in this string. It is through the extension that the compiler knows to search either a library or a form.
Uses ObjectPAL
":SomeAlias:Util.LSL"
endUses
Figure 9 - The Uses block using the enhanced library features available as of Paradox 7. When the form compiles, the library is accessed and all methods, constants, types and uses blocks in the library are registed in the form.
Not only is the compiler capable of searching through the library's methods, it also searches through the Types, Const and Uses windows of any library or form whose path and name is listed in the form's Uses block. What this does is to cause the compiler to find all types, uses, and constants registered libraries. As the form is compiled, all of the constants, types and uses from the library are registered in the form itself. Since this is done during the compilation, this information is now stored in the form.
This new capability allows for creating a reference library. A reference library is one with no custom methods, only Uses declarations for DLL's, types declarations and constants declarations. Any form that lists the reference library in its Uses statement will have this information stored in the form during the compilation process. Since the information is stored while compiling, it is not necessary that the reference library be available during run-time. This is a very easy way to share uses, types and constants among many forms. The library will only need to be available during run-time if it is not strictly a reference library and custom methods have been added which are accessed during run-time.
Note that since the information is stored in the form during the compilation, it is critical that when changes are made to a library accessed using this new capability (as of Paradox 7), the forms that call the library must be recompiled.
The benefits of these new library enhancements in Paradox for Windows are obvious. It is possible to call any method in a library by simply adding the calls and recompiling the form. The down side to using this capability is that since it requires the extension of the library or form, alternating between delivered libraries (LDL extension) and forms (FDL extension) and the non-delivered libraries (LSL extension) and forms (FSL extension) means changing the uses block each time this change is made.
It would appear that since the library or form reference is placed in the Uses block as a string, that it would be possible to add the extension as a constant in an appended string. The code would look something like that shown below. Unfortunately expressions will not compile in the Uses block so this is not possible.
const
CstrFormState = "D" ; References delivered libraries right now
endConst
Figure 10 - A constant declaration to be used in a uses block.
Uses ObjectPAL
:SomeAlias:Util.L" + CstrFormState + "L"
endUses
Figure 11 - An attempt to reference a constant in a uses block. Note that it will not compile.
In order to understand how to share values between forms using a library, it is best to understand that libraries can have two different types of scope: global and private.
By default a library that is opened on the Paradox desktop is opened only once. That is, the first form to open that library establishes one instance of the library in memory. Subsequent forms that open the library are really just establishing a handle to the library that is now in memory.
Assume that a library has a variable declared in the Var window called strShared. If one form (Form A) calls a method that assigns the value "Form A was here first" to strShared, that value will be set for any other form referencing the same instance of the library. Should a second form (Form B) run a method that uses strShared, the value of the variable will still be "Form A was here first".
This sharing of a single instance of a library could spell trouble under certain situations. Imagine that an array in a library is used to track all forms opened by a given form (Form A). As Form A closes, it uses this array to close any detail forms that it may have opened. Since this is a technique is used by many forms, it is prudent to put it in a library. This scenario could be disastrous if the array had been set by the Form A but was later changed by Form B. Were this to happen, then the closing of Form A would close all of the detail forms opened by Form B. It would also leave open the detail forms of Form A.
Libraries can be opened so that variables are not accessible by all forms. If the developer requires that variables in a library cannot be modified by another form, an optional parameter must be used when using the open method of type library. Using this syntax, any other form opening the library would not have access to the instance of the library that was already open. Therefore another instance of the library would be opened. Making the instance of library inaccessible to other forms requires using an optional parameter when using the library open method.
open(const libraryName String [ , const libScope SmallInt ] ) Logical
Figure 12 - The syntax for the library open method. Note the optional library scope constant.
This optional parameter, libScope, determines the accessibility, or scope, of the instance of the library to other forms. Two constants may be used in this parameter: globalToDesktop and privateToForm. As previously mentioned, an instance of a library opened in memory is global to the Paradox desktop by default. Other forms opening that library will not open unique instances of it unless they use the following syntax:
if not lbUtility.open(":UtilAlias:Utils", privateToForm) then
errorShow()
return ; Maybe
close() instead
endif
Figure 13 - Opening a library privateToForm. When this instance of the "Utils" library is loaded into memory, it will not be accessed by any other form. Normally a single instance of the library is loaded into memory and accessed by all forms needing to reference library.
Why is globalToDesktop the default? There are several reasons, not the least of which is that opening only one instance of a library conserves memory. Another reason is that it allows for passing values between forms. While passing a value between two forms is possible without a library if both forms are open, a library allows for one form to store a value in a library, while at a later time another form opens and retrieves the same value from the library. Since sharing variables between forms is not possible in Paradox, this is a common reason for starting to use libraries in an application. How is this done?
Imagine that two sales representatives from different offices have never seen each other but are told to meet for lunch by the regional manager. The regional manager arranges the meeting and tells each of them, "If you don't find the other rep, call me. I'll take a message as to where you are. When the other rep calls, I'll relay the message as to your whereabouts."
In this situation, neither representative can speak directly with the other. They can only communicate by leaving a message with the regional manager. Passing variables between Paradox forms is similar in that forms cannot share a variable. They can, however, pass a value as a parameter to a method in a library. The library acts as the regional manager. A method in the library records the value passed by storing it to a variable. The "message" is assigned to the variable. The variable will carry that value until it is changed or the library is closed. A second method simply returns the variable.
Since the variable must be assigned in one method and returned in another, the variable must be declared in the Var window so that it is accessible by more than one method. The variable declaration below shows the anyType variable, anyHolder, being declared. AnyType is used in this example so that a variety of data types may be passed such as dates, numbers, and strings.
var
anyHolder anyType
endVar
Figure 14 - A variable declaration in a library to store a value passed from one form that will later be passed to another form.
The form that has the value to be passed must call the method that assigns the value to the variable. The StorVal method, shown below, is an example of a method that makes the variable assignment.
method StorVal(anyToStore anyType)
anyHolder = anyToStore
endMethod
Figure 15 - The method in the library that is passed the value from the first form. This value is assigned to the variable declared in the library.
The call to storVal would look like this:
lbUtil.storVal(strVal)
Figure 16 - The call that stores the value in the library.
The form that requires the value must call a method in a library that returns the value stored in the variable. ReturnVal, shown below, returns the value stored in the anyHolder variable.
method ReturnVal() anyType
if anyHolder.isAssigned() then
return anyHolder
else
return blank()
endif
endMethod
Figure 17 - A method that returns a value stored in a variable that was declared in a library.
The call to returnVal would look like this:
strGetValFromLib = lbUtil.returnVal()
Figure 18 - The call to the method that returns a value stored in a library.
For a library to remain open, some form, library or script must have it opened. If library is first opened but later, all forms, libraries or scripts that had it opened are closed, the library is closed. If a library is closed, so are its variables. A form that opens at that time and attemps to open the library and access the value of a variable, will find that the variable is unassigned. This is one reason the returnVal method shown above returns a blank value if the variable is not assigned.
When declaring the parameters for a custom method, it is necessary to declare the type of variable being passed after the name of the parameter. In the StorVal method above, the type is declared as anyType. Passing arrays has a minor twist. Below is an example of declaring a six element array of type string in a var block.
var
aryEG array[6] string
endVar
Figure 19 - Declaring an array in a var block.
It would be natural to conclude that declaring a parameter as an array would involve syntax similar that shown below: In fact, the parameter declaration will fail during the compile process.
method storAnArray(var aryToStore array[6] string)
aryHolder = aryToStore
endMethod
Figure 20 - An invalid array declaration as a parameter.
After the name of a parameter, the ObjectPAL compiler expects a type. Since arrays are not a predefined Paradox type, a type must be declared for the array in the type window of the library.
Type
TaryToPass = array[6] string
endType
Figure 21 - Declaring an array as a user defined type.
After the type has been defined in the type window, the type can then be used in the parameter list for the method.
method
storAnArray(var aryToStore TaryToPass)
aryHolder = aryToStore
endMethod
Figure 22 - Referencing the array type to declare the parameter's type as an array. The array is declared as a type in type block as shown in figure 20.
Note: If Paradox 7 or a later version is used and the form's Uses statement references the library's path and file name as a string rather than listing each individual method, the types in the library will be compiled with the form. In this case the type only needs to be declared in the library. If an earlier version of Paradox is used, or the path and file name are not listed in the Uses block, the type must be declared in both the form and the library.
The event statements disableDefault, doDefault, enableDefault, and passEvent can only be used in a event method. They cannot be used in a custom method or procedure. This includes custom methods in a library. The way to work around this restriction is to use two methods. One is called before the event statement and the other is called after.
In this example, the user is asked to verify the deletion of a record. If the user confirms the deletion, doDefault is executed triggering the default behavior of deleting the record. If referential integrity is defined on this table and it has detail records that depend on it, the deletion will fail and an error code will be generated. Rather than relying on the error message generated by Paradox "Master has detail records. Cannot delete or modify.", this code lets the user know what action to take in order to be able to delete it.
method action(var eventInfo actionEvent)
if eventInfo.id = dataDeleteRecord then
msgQuestion("Delete?", "Are you really, " +
"absolutely sure you want to delete this " +
"record?") = "Yes" then
doDefault
if eventInfo.errorCode() = peKeyViol then
msgStop("Details, details...;", "This " +
"record has detail records. In order " +
"to be able to delete this record, " +
"they must be deleted first.")
endif
else
eventInfo.setErrorcode(userError)
endif
endif
endMethod
Figure 23 - Code to check for the deletion of a record. The doDefault command cannot be run in a custom method or procedure, only in an event method.
Moving this code to a library requires splitting the code into two methods and making calls to these methods from the action method. One call is made before the event statement (doDefault in this example) and one is made after.
method action(var eventInfo actionEvent)
if lbUtils.genDelete1(eventInfo) then
doDefault
lbUtils.genDelete2(eventInfo)
endif
endMethod
Figure 24 - Calling the custom methods from the event method, action. If the first method returns true then DoDefault must be run and the second method called.
GenDelete1 is called before the event statement. It must return a value signifying whether the event method (in this case action) should be followed with the event statement or a call to the second method, genDelete2.
GenDelete1 returns a value of true if the doDefault must be executed followed by a call to genAction2.
method genDelete1(var ev entInfo actionEvent) logical
if eventInfo.id = dataDeleteRecord then
if msgQuestion("Delete?", "Are you really, absolutely sure you " +
"want to delete this record?") = "Yes" then
return true
else
eventInfo.setErrorcode(userError)
endif
endif
endMethod
Figure 25 - The first of the pair of generic delete methods.
GenDelete2 contains the code that responds to the results of the default behavior that was triggered by the doDefault statement.
method genDelete2(var ev entInfo actionEvent)
if eventInfo.errorCode() = peKeyViol then
msgStop("Details, details...", "This record has detail " +
"records. In order to be able to delete this record, " +
"they must be deleted first.")
endif
endMethod
Figure 26 - The second of the pair of generic delete methods. This method requires that the doDefault has been run beforehand.
It is not uncommon for more than one library to be used in a Paradox application. There are times when a library of generic utilities is used as well a library of custom methods that is specific to the application. Sometimes a library has so many methods and variables that it runs out of symbol space and some of the code must be moved to a second library. As long as the methods in one library (AppLib1) only call methods in a second library (AppLib2), everything is fine.
In order to have one library call methods in another library, the library is usually opened from within the event open method of the calling library. In this case, the following code could be placed in open method of AppLib1:
method open(var eventInfo event)
if not lbUtil.open(":SomeAlias:AppLib2") then
errorShow()
close() ; Close the form
endif
endMethod
Figure 27 - Opening a second library from the open method of library.
Assume that there is a method that handles menu choices for the application called menuHandler that resides in AppLib1 . Calls are made from menuHandler to various custom methods that produce reports. Also assume that a generic module called outPutDest exists in AppLib1. This method asks the user for the destination of the printed output (printer, file, screen). Once the developer decides that it is time to create a new library to hold other methods, problems can begin. The menuHandler method will have to call new report modules that reside in the new library, AppLib2. These new report modules will have to make calls to outPutDest which resides in AppLib1. The calls by methods in AppLib1 to methods in AppLib2 will be able to rely on the library having been opened with the code shown above. If similar code, shown below, is placed in the open method of AppLib2, there will be a problem.
method open(var eventInfo event)
if not lbUtil.open(":SomeAlias:AppLib1") then
errorShow()
close() ; Close the form
endif
endMethod
Figure 28 - Opening the first library from the open method of the second library. This results in infinite recursion until Paradox runs out of stack space. This is not good.
The event open method of a library is called every time a form, library or script opens that library even if a global instance of the library is already in memory and the form is only creating a handle to the instance. This means that if the open method of Applib1 opens AppLib2, and the open method of AppLib2 in turn opens AppLib1, AppLib1's open method will execute a second time and open AppLib2 again. This will cause infinite calling of one open method after the other (recursion). In Paradox 5 and earlier versions, this results in ObjectPAL running out of stack space and the system comes to a halt. In Paradox 7 the form and the libraries eventually open after a long wait due to much recursion. Not only does it take a long time for the library to open, the closing of the form also takes a very long time since all of the library openings must close.
The workaround is to leave the code that opens AppLib2 in the open method of AppLib1. The code in the open method of AppLib2 that opens AppLib1 must be removed. Instead the following code should be put into any method in AppLib2 that calls methods in AppLib1:
if not libvar.isAssigned() then
if not libVar.open(":AliasName:Gen1") then
errorShow()
return
endif
endif
Figure 29 - In any method in the second library that needs to access methods in the first library, the library variable should be tested to see if it has been opened. Only if it has not been opened, then open it.
Libraries are the best way to share code across multiple forms. With the new enhancements as of Paradox 7 it is far easier to call methods in libraries. Furthermore, the functionality of libraries has been greatly extended so that they can be used to share uses, constants and types across multiple forms as well. Even with reuseable objects, where an object is copied from form to form, it can be prudent to store the code in the library. If enough copies of the reuseable objects are made and then a change must be made to the code, the use of a library will save a tremendous amount of time. If the code is going to be used in more than one form, take the easy way and use libraries.