Mark is a PowerBuilder consultant with Toronto-based Data Management Consultants (DMC), specializing in the delivery of custom Oracle solutions. Mark can be contacted on CompuServe at 75462,422.
With so much emphasis placed on the visual aspects of PowerBuilder's native objects, little attention has been given to Non-Visual User Objects (NVOs). Although they may be PowerBuilder's most useful tool for creating truly object-oriented applications, NVOs are rarely used effectively and seem poorly understood. PowerBuilder itself is not inherently object oriented, but it allows you to develop applications using procedural, object-oriented, or cross-combined methodologies.
Increasingly in client-server computing, companies are creating database-level, entity-oriented data objects to provide consistent, uniform interaction with data sets from within applications. NVOs can be used to segment applications along functional or task-oriented boundaries as well as along data boundaries. If properly implemented, data-oriented NVOs also provide a logical interface on the application-development side. In this article, I'll examine the effective use of NVOs and their role in application development.
NVOs are composed of instance variables, functions, structures, and events. PowerBuilder provides only two predefined events--the Constructor Event and Destructor Event--but you can create additional user-defined events. However, the calling arguments that can be supplied when an event is triggered are limited in number and complexity, so the preferred method of communication with NVOs is via function calls.
The Constructor Event is triggered as the object is being instantiated and can be used to initialize attributes or any other necessary operation at startup. Make sure that the application has initialized everything the user object requires before the object is created. Similarly, the Destructor Event is triggered as the object is being destroyed, allowing the user object to clean up after itself if necessary. You should always explicitly destroy any objects that you create at run time, if only to exert greater control over the behavior of the instantiated objects.
Typically, the instance variables contained within the NVO will be declared as private or protected variables. This means that their scope is limited to the object that contains them and they cannot be accessed by scripts that are external to the NVO. This method can be frustrating, but it is extremely important to maintain the NVO's self-reliance. If the value of an instance variable is required outside the object, then its contents should be available through a function call. Generally, the more functions and variables available to external objects and scripts, the harder the object and application are to maintain. The internal activities of the NVO will likely change over time, so the less the outside world knows about them, the better.
Access to functions and data within an NVO can be organized in two ways. The first method is to directly access the NVO's functions and data stores; see Listing One. However, this makes the NVO's internal workings available to external scripts, thus defeating the NVO's long-term purpose.
The second method obscures the NVO's inner activity by providing a single-entry dispatch service within the NVO, as demonstrated by Listing Two. The NVO receives instructions through a single entry point, from which it calls the functions necessary to perform the operation. This allows the NVO to evolve without requiring that scripts be rewritten in every application that uses it. The drawback of this approach is that many NVOs deal with several different and complex data types, and it is difficult to move many different types of data through a single pipeline-style interface. To deal with this, you can either convert data into a standard data type (such as a string or PowerObject) and parse the variable from within the NVO, or provide multiple dispatch functions to handle the different data types. You must decide which approach is most logical on a case-by-case basis.
Listing Two uses the function f_Method() to send instructions to the customer-manager object, CustMgr, based on a predefined method identifier called find and the qualifying string sCustKey. The number of qualifying arguments varies according to the individual need of the NVO; one qualifying argument satisfies a significant percent of the requirements.
In Listing Two, the script that calls the CustMgr NVO does not know how CustMgr will resolve the request, how the data is stored internally, or where the data is stored. The f_Query() function works on the same premise as f_Method(). The calling script relies on blind faith, and assumes that its request will be processed. Listing One forces the internal workings of the CustMgr NVO to remain static, while Listing Two only requires that the dispatch services remain static. The way in which the requests and queries are satisfied is hidden from the calling script. Table entities and attributes could be renamed or restructured, and the only maintenance required would be to update the CustMgr NVO and regenerate the application.
In developing a truly object-oriented class library, NVOs play an important role by encapsulating functions and data into discrete, reusable objects. Typical functional NVOs would be a menu security manager, message manager, window manager, and perhaps an external-function API manager. While these NVOs would be generic for any company, data-aware NVOs are company specific. Typical data-aware NVOs might be a customer-profile manager or an inventory manager.
A class library comprises several layers of objects, starting with the most generic and progressively becoming more specialized; see Figure 1.
The NVO is not used as a base object from which application-specific objects are created, but rather to extend the library's functionality. The NVO is generally used in its original form, but its operation can be customized within an application through inheritance.
A Functional Task Manager (FTM) is a discrete unit of processing logic that contains all the knowledge necessary to perform a task. Typically, FTMs are business-independent units that provide applications with value-added capabilities such as access security, drag-and-drop, and serial communications. The complexities of the particular task are hidden from you--they are taken care of by the packaged routines of the FTM. A side benefit of this application architecture is that you don't need to master everything. With a little knowledge about serial communications, for example, you can embed a serial-communications FTM into an application and quickly work on the application-specific scripts, as opposed to delving into the mysteries of serial communications under Windows.
Typically, FTMs are layered. The lowest-level functions perform small and/or implementation-specific tasks. Each new level has progressively more-generic tasks, finally bundling the lower-level tasks into a form that is usable by application developers. It is possible to accomplish this layered effect in one of two ways: by coding the layering within a single NVO or by using inheritance to progressively build more-specific FTMs from FTMs that are more generic. But using inheritance for its own sake adds unnecessary complexity and overhead. If several FTMs are based on a similar low-level function set, then inheritance is the perfect choice to create different FTM classes. If the FTM stands alone as a single class, however, multiple levels of inheritance merely create excess baggage.
A Data Object Manager (DOM) is to Data Objects what FTMs are to tasks. In its simplest form, a Data Object is a business-dependent entity or group of entities--such as Customer, Supplier, or Component--with an attached set of methods for manipulating it. These methods fully define all operations that may be performed on the given data set. All applications must access the Data Object via the methods associated with it and must fully comply with its rigorous rules. These rules are designed to protect the integrity of the underlying data and provide a consistent interface from any application.
Generally, most attention given to Data Objects applies only to the back-end DBMS, where the methods are implemented as stored procedures and triggers. NVO DOMs extend this concept to the front-end development tool. The back-end DBMS enforcement is still required; NVO DOMs make the front end consistent with the back end. The concepts illustrated in Listing Two apply equally to Data Objects.
The uo_ObjManager NVO is an FTM that allows you to browse library lists and directories. It provides code for choosing objects from a PowerBuilder library; their subsequent manipulation is up to you. Example 1 presents the naming standards and conventions I've used.
The uo_ObjManager object contains several embedded functions; see Figure 2. It reads the PB.INI file to determine the current application and library. It then locates the current library list and produces a list of object names stored in a given library.
Built around this NVO is a window that manages the user interface to the library and object lists. (The code and events are not included in this article.) Figure 3 is a simple UI that could be used to select a PowerBuilder object.
Both FTMs and DOMs must be instantiated at run time, generally during the Application Open event or the Open Event of an MDI Frame window (in the case of an MDI application). They are instantiated using the CREATE statement and usually assigned to a global variable of that type. The DESTROY statement eliminates the object and invalidates any references to it. Example 2 details the declaration, instantiation, and destruction of NVOs.
The uo_ObjManager NVO is instantiated into the global ObjMgr and initialized during the Application Open Event; see Listing Three. In the Open Event of the window, w_main, the library list DataWindow is populated by instructing ObjMgr to create a library list and then requesting its value; see Listing Four. As the user clicks on a particular library, the program reads the directory of all objects contained in that library and displays them in the dw_ObjList data window; see Listing Five.
The ObjMgr NVO reduces the UI to a generic list-of-values handler--no intrinsic knowledge about PowerBuilder libraries or objects is embedded within the window. Thus, this sample application is segregated along the functional boundaries of producing the lists and managing the UI.
As Example 3 shows, ObjMgr has several private instance variables for storing results generated by the creation of library lists and object lists. ObjMgr contains six functions: two public and four protected. Table 1 describes the available function-access levels and their impact on design.
ObjMgr can process four methods:
PowerBuilder stores the library lists for each defined application in its PB.INI file. The default application is identified by two profile strings: APPNAME, which identifies the application; and APPLIB, which identifies the library that holds the application. The library lists are stored in PB.INI as $library1(application)=library1;..;libraryn. Once the application name and library name are determined, they are concatenated and the library list profile string is read from the PB.INI file. The f_IdentifyApplication function, which is associated to the Initialize method, appears in Listing Eight.
Once the application is identified, the library list can be retrieved from the PB.INI file by reading a profile string. (You can then format it into a PowerBuilder list that can be imported into a data-window object.) The libraries in the list are separated by semicolons that must be replaced with a Tab and Line Feed combination to be compatible with ImportString; see Listing Nine.
Once created, the library list can be displayed to the user. To retrieve a list of library objects that match a specific type, the user simply selects a library. The f_ReadLibrary function reads a specific library and creates a list of objects. PowerBuilder's LibraryDirectory function returns each directory entry as object name-{tab}datetime{tab}comments{linefeed}. The f_ReadLibrary function truncates the date, time, and comments from each entry and stores the resulting string in isObjList; see Listing Ten.
A complication of the single-entry pipe-line is dealing with many different data types. One solution is to convert them all to a single data type. This is not always possible, but it will solve most conflicts. Listing Eleven illustrates the conversion between an enumerated data type and a string. PowerBuilder's LibraryDirectory function requires an enumerated LibDirType variable indicating the type of object to be returned. The f_SetLibDir-Type function in Listing Eleven translates string identifiers into enumerated LibDir-Type values.
With the introduction of PowerBuilder 4.0, PowerSoft has added more predefined user-object classes. NVOs can be either Custom or Standard class objects. The Standard class objects are based on predefined PowerBuilder object classes such as Message, Pipeline, Error, and Transaction. The techniques I've discussed here should be implemented using the Custom object class.
PowerBuilder 4.0
Powersoft Corp.
561 Virginia Rd.
Concord, MA 01742-2732
508-287-1500
Figure 1: Components of a layered class library.
Figure 2: Embedded-function list for uo_ObjManager.
Figure 3: Object-browser UI.
Example 1: Naming conventions and standards: (a) scope; (b) type; (c) VariableName.
Copyright © 1995, Dr. Dobb's Journal
(a)
g=Global variables
s=Shared variables
i=Instance variable
Local-variable scope indicator is left blank.
(b)
s=String
l=Long
i=Integer
e=Enumerated
(c)
sLibraryList is an instance string variable.
eLibDir is a local enumerated variable.
Example 2: Declaring, instantiating, and destroying an NVO. (a) Declaration of ObjMgr in the global declarations window; (b) instantiation of ObjMgr in Application Open event; (c) destruction of ObjMgr in Application Close event.
(a)
// declare place holder for instance of uo_objmanager
uo_objmanager ObjMgr
(b)
// create instance of uo_objmanager
ObjMgr = CREATE uo_objmanager
(c)
// remove instance of uo_objmanager
DESTROY ObjMgr
Example 3: ObjMgr private instance variables.
Private string isAppName // currently defined application
Private string isAppLib // currently defined library containing application Private string isAppDir // directory path to isAppLib
Private string isPBiniFile // full path & name to PB.INI
Private string isLibraryList // current library list
Private string isObjectList // current object list
Private LibDirType ieLibDirType = DirAll! // default object type to browse
Table 1: Object functions can be declared with one of three access attributes.
Access Description
Attribute
Public Least restrictive level of function access; available
from any script within the application. If the
function is called from a script external to the
object in which it was declared, it must be referenced
in a fully qualified manner such as
ObjMgr.f_Method().
Protected Medium level of function access; called from
scripts within the object or descendant object in
which the function was declared. If the object was
inherited to create a new class, all scripts within
the object can still reference the function in the
ancestor.
Private Most restrictive level of function access; same as
protected access except that the function cannot be
called from descendant scripts. The function is
hidden from all scripts outside the exact class in
which it was declared. Generally, only one or two
functions in an NVO should be publicly accessible.
Use of protected versus private access depends on the
intent of the object. Private access limits all new
object classes to using ancestor functionality as
originally intended. Protected access allows new
object classes to completely re-invent the behavior
of the object and should be used with caution.Listing One
// Find customer profile identified by sCustKey
IF CustMgr.f_FindCustomer(sCustKey) THEN
// cannot find sCustKey
RETURN
END IF
// Get customer occupation
sCustJob = CustMgr.CustProfile.sOccupation
Listing Two
// Find customer profile identified by sCustKey
IF CustMgr.f_Method("find", sCustKey) THEN
// cannot find sCustKey
RETURN
END IF
// Get customer occupation
sCustJob = CustMgr.f_Query("profile", "occupation")
Listing Three
// create an instance of uo_ObjManager
ObjMgr = CREATE uo_objmanager
// instruct it to initialize itself
ObjMgr.f_Method('Initialize', '')
// open the browsing window
Open(w_main)
Listing Four
// instruct ObjMgr to create an internal library list
ObjMgr.f_Method('BuildLibraryList', '')
// request the library list and import it into dw_LibList
dw_LibList.ImportString(ObjMgr.f_Query('LibraryList', ''))
Listing Five
long lRow
// check for valid row selection
lRow = This.GetClickedRow()
IF lRow > 0 THEN
// update library selection
This.SelectRow(0, FALSE)
This.SelectRow(lRow, TRUE)
// create an internal list of objects contained in selected library
ObjMgr.f_Method('ReadLibrary', This.GetItemString(lRow, 'selection'))
// refresh destination datawindow, dw_ObjList
dw_ObjList.Reset()
// request the object list and import it into dw_ObjList
dw_ObjList.ImportString(ObjMgr.f_Query('ObjectList', ''))
END IF
Listing Six
// Boolean f_Method(string sMethod, string sQualifier)
// Dispatch methods identified by sMethod. sQualifier provides
// additional information for individual methods.
CHOOSE CASE Lower(sMethod)
CASE 'initialize'
RETURN f_IdentifyApplication()
CASE 'buildlibrarylist'
RETURN f_BuildLibraryList()
CASE 'readlibrary'
RETURN f_ReadLibrary(sQualifier)
CASE 'setlibdirtype'
RETURN f_SetLibDirType(sQualifier)
CASE ELSE
RETURN False
END CHOOSE
RETURN True
Listing Seven
// String f_Query(string sQuery, string sQualifier)
// Provides feedback to requests for internal information.
CHOOSE CASE Lower(sQuery)
CASE 'librarylist'
RETURN isLibraryList
CASE 'objectlist'
RETURN isObjectList
CASE ELSE
RETURN ""
END CHOOSE
Listing Eight
// Boolean f_IdentifyApplication()
// Reads current application from PB.INI file.
long lPos1, lPos2
string sPBPath
// Get path for PB.INI
sPBPath = ProfileString ("WIN.INI", "POWERBUILDER", "INITPATH","")
// Build full filename for PB.INI
isPBiniFile = "PB.INI"
IF sPBPath <> "" THEN
IF Right(sPBPath, 1) <> "\" THEN
sPBPath = sPBPath + "\"
END IF
isPBiniFile = sPBPath + isPBiniFile
END IF
// Get Application name and main library from INI file
isAppName = ProfileString(isPBiniFile, "APPLICATION", "APPNAME", "")
isAppLib = ProfileString(isPBiniFile, "APPLICATION", "APPLIB","")
// separate path prefixed to application library
isAppDir = ""
lpos1 = 0
lpos2 = Pos(isAppLib, "\")
DO WHILE lpos2 > 0
lpos1 = lpos2
lpos2 = Pos(isAppLib, "\", lpos1 + 1)
LOOP
IF lpos1 > 0 THEN isAppDir = Left(isAppLib, lpos1)
RETURN True
Listing Nine
// Boolean f_BuildLibraryList(). Reads current library list and formats it
// to be compatible with the ImportString() function.
long lPos1
// Get the library path from PB.INI for the specified application
isLibraryList = ProfileString(isPBiniFile, "Application","$" +&
isAppLib + "(" + isAppName + ")", "")
lPos1 = Pos(isLibraryList, ";")
DO WHILE lPos1 > 0
isLibraryList = Replace(isLibraryList, lPos1, 1, "~t~n")
lPos1 = Pos(isLibraryList, ";", lPos1 + 2)
LOOP
RETURN True
Listing Ten
// Boolean f_ReadLibrary(sQualifier). Reads current library based on
// preset object type and creates list of objects found.
string sObjList
Long lPos1, lPos2, lDirLen
isObjectList = ""
sObjList = LibraryDirectory (sQualifier, ieLibDirType)
// For each entry in a LibraryDirectory listing,
lDirLen = Len(sObjList)
lPos1 = 1
DO WHILE lPos1 < lDirLen
// Locate first tab separator
lPos2 = Pos (sObjList, "~t", lPos1)
// Peel object name & append to object list
isObjectList = isObjectList +&
Mid(sObjList, lPos1, lPos2 - lPos1) + "~t~n"
// Advance to start of next directory item
lPos1 = Pos (sObjList, "~n", lPos2) + 1
LOOP
RETURN True
Listing Eleven
// Boolean f_SetLibDirType(string sQualifier).
// Identifies library directory type.
CHOOSE CASE Lower(sQualifier)
CASE 'dirall'
ieLibDirType = DirAll!
CASE 'dirapplication'
ieLibDirType = DirApplication!
CASE 'dirdatawindow'
ieLibDirType = DirDataWindow!
CASE 'dirfunction'
ieLibDirType = DirFunction!
CASE 'dirmenu'
ieLibDirType = DirMenu!
CASE 'dirstructure'
ieLibDirType = DirStructure!
CASE 'diruserobject'
ieLibDirType = DirUserObject!
CASE 'dirwindow'
ieLibDirType = DirWindow!
END CHOOSE
RETURN True
DDJ