Reference Variables and the PFC - PBulletin, May 1998
Powersoft do not give a great deal of attention to explaining PowerBuilder reference variables in PowerBuilder documentation. This is unfortunate, as an understanding of the way in which the product deals with these variables is crucial. This article deals with:
- PowerBuilder reference variables, the class pool and the instance pool
- Some common pitfalls when using reference variables
- How the PFC exploits reference variables.
The code examples in the Example and subsequent sections relate to a SyPower demonstration applet designed to familiarise new SyPower PowerBuilder developers with a service based architecture.
PowerBuilder allows developers to declare variables of two types:
- Normal data types as found in all programming languages (string, integer, decimal, etc.)
- Class variables (window, menu, button, etc.).
For the handling of class variables, PowerBuilder uses two areas of memory:
- The class pool holds currently-instantiated class definitions
- The instance pool holds all current instances.
When a developer issues the command Open(w_window), PowerBuilder:
- Scans the class pool to see if the w_window definition is already in memory
- loads the definition into the class pool if it is not already present
- creates an instance of w_window in the instance pool
- populates the global reference variable w_window with a pointer to the class definition
- creates a manifestation of the object on the screen.
The Open() command shields the developer from the need to declare a reference variable explicitly and populate it with an instance. This would normally be achieved through something like
However, w_window is both a global reference variable pointing to an instance, and the name of the class definition. In a 4GL such as PowerBuilder, the distinction between a class and an object gets blurred. Although we refer to the items in PBLs as objects, they are in fact classes. In an environment such as C++ the distinction is more obvious. The class is the ream of script that defines a window, whilst the object is what appears on screen at run time.
As the term suggests, a global reference variable has application-wide scope. As long as there is an instance of w_window in the instance pool, the developer could code something like
w_window.toolbarvisible = FALSE
from any script anywhere in the application.
Data Typing and Class Variables
PowerBuilder uses very strong data typing. This can lead to problems when using reference variables, even amongst experienced developers. A common mistake is to declare a window function and then attempt to access it from a menu through the PARENTWINDOW pronoun
This will result in a compiler error because PowerBuilder treats PARENTWINDOW as a reference to a system class window. If we look in the object browser at functions defined for the system class window, obviously there is no wf_dosomething available. If on the other hand we declare an event ue_dosomething and then code
the compiler is happy. Look at the system class window in the object browser to see why. There is a function TriggerEvent declared. The beauty of this approach is that TriggerEvent does not cause a runtime error if ue_dosomething is not present on the target window. Instead, it silently returns -1. This is a very powerful aid to writing generic code. We can trigger different code in different windows and controls without the need to 'hard wire' any references in our menu. For example, we might code an application-wide of_sendmessage function and, in this function, get the active sheet and trigger a ue_dosomething event. By defining an ancestor sheet with a placeholder ue_dosomething event and then implementing different code in descendants, we have created a very powerful framework for sending messages. Examine the code in the PFC messagerouter to see how it exploits TriggerEvent in order to deliver a message. Remember the old chestnut that OO is all about messages, not data flows?
Figure 1 - System class window
Local Reference Variables
The Open(w_window) approach described above would not work if an application needed to open multiple instances of a window. Imagine a master-detail situation in which master records are displayed on one sheet and clicking on a master record opens another sheet with detail records displayed. (The user must have the option to open more than one detail sheet at once, as one of the main attractions of a GUI environment is the freedom from performing tasks in a strictly linear fashion.) However, if the developer tried to use a global reference variable w_detail to accomplish this, the user would never be able to open more than one detail sheet. Once one detail sheet were opened, PowerBuilder would see that the definition w_detail already exists in the class pool, and that it is already pointing at an instance. Each subsequent call to Open(w_detail) would produce no result, no matter how many different master records the user doubleclicked.
Instead, the developer could code
Here the developer has used a local reference variable of type w_detail (a class variable). When the script has finished executing , lw_detail goes out of scope and is destroyed, but the instance remains in the instance pool. If the script is executed again, PowerBuilder creates a new instance of w_detail and once again populates lw_detail with a pointer to it, resulting in a new sheet being opened.
Reference Variables as Pointers
By coding li_integer1 = li_integer2, the developer has caused PowerBuilder to copy the value stored in the first variable into the second variable. As the foregoing implies, reference variables do not behave in this way. Coding lw_window2 = lw_window copies a pointer to the instance of w_window from lw_window to lw_window2. This is similar to passing a parameter by reference.
Pointers and the PFC
The concept of a reference variable as a pointer is fundamental to the PFC. The PFC is built around a service-based architecture. Most of its objects can be classified as either client or service objects. Clients request processing from service objects by redirecting processing from their own 'native' events to custom events in PFC objects. (These events begin with the pfc_ prefix, e.g. pfc_open.) This is possible because of reference variables.
Imagine that a generic ancestor datawindow required two types of processing.
- Row selection
It requests this functionality from two client objects, n_cst_sort and n_cst_select. These objects are derived from the parent service object n_cst_dwsrv, which contains variables and processing generic to all datawindow services.
Figure 2 - Service-based architecture
Connecting Client and Service Objects
Before service objects can perform work for clients, they must be able to 'see' the client. Equally, the client must be able to 'see' the service provider. This is where reference variables are involved. To 'switch on' a service, the developer codes
where xxx is the name of the service. In the above example, the code would be
This code switches on the select and sort services. To see how this is accomplished, examine the ancestor object u_dw. First, note that it declares two instance variables
There is one instance variable for every service that the object might call upon. (Equally, note that u_dw contains an of_Setxxx (FALSE) function for every one of these services in its destructor event in order to prevent memory leaks.) When u_dw is instantiated, these variables do not point to an instance. Any attempt to use them would result in the notorious 'null object reference' error.
Now examine the functions of_Setxxx (remember that ab_switch is the name of the Boolean argument passed to the function). The code for of_SetSelect() is
if ab_switch then
If the passed value is TRUE, the code checks whether the instance variable already points to a valid instance (line 2). If it does not, it creates an instance of n_cst_sort and populates inv_sort with a pointer to it (line 3). At this point, u_dw can 'see' the service object n_cst_sort through inv_sort, but the service object cannot see the client. Line 4 accomplishes this by passing a variable of type u_dw (using the pronoun THIS) to the service object. This code is in fact held in the ancestor object n_cst_dwsrv, as it is the same for all datawindow service objects.
The code for n_cst_dwsrv.of_SetRequestor() is simply
idw_requestor = a_dw
where idw_requestor is an instance variable of type u_dw and a_dw is the argument passed to the function. This code copies the pointer to the instance of u_dw to the local reference variable idw_requestor. Now the service object can see u_dw through idw_requestor.
In the PFC, this pattern is repeated for all client and service objects. Each client contains:
- an instance reference variable for each service
- an of_Setxxx() function for each service
and each service contains
- an instance reference variable for the client
- an of_SetRequestor() function.
Example of the Select Service Select functionality highlights a row in the datawindow when the user selects it. The event which fires when the user makes a selection is the Clicked! event. Clicked! code in our client object is
if IsValid (inv_select) then
This code checks whether the instance variable inv_sort points to an instance of n_cst_sort (in other words, whether the service has been switched on). If it does, it redirects or delegates processing to the srv_clicked event of the service object. The code for srv_clicked is
idw_requestor.SetRow (0, FALSE)
This is exactly the same code as would have appeared in u_dw.Clicked!, except that it is qualified by the instance variable idw_requestor. Code written within an object does not need to be qualified. This is similar to virtual referencing in a directory structure. Referencing from the virtual's directory requires the virtual name only. Referencing from another directory requires that the virtual name be qualified with the path.
The above example is trivial. Why code 'native' datawindow functions from 'outside' the datawindow? The benefit of a service-based architecture really becomes apparent when we add significant amounts of custom processing to base PFC objects in corporate layer descendants, and we will only use this processing some of the time. In the SyPower corporate layer, for example, we have an additional datawindow service, syp_dwsrv_sql, which is used to build SQL 'where' clauses and manipulate the SQL of a datawindow programmatically. However, we do not want this code to be present in every datawindow in an application. Typically it will only appear in datawindows in search-response type windows. Consequently , the functionality needs to be instantiated in these windows only. As a result, our corporate datawindow syp_u_dw carries a small amount of overhead in the reference variable inv_ sql. This is far more efficient than storing all our corporate SQL processing in the corporate datawindow itself and using this proc essing only on a few occasions. Furthermore, as we added more and more corporate functionality we would end up with a 'fat ancestor' - a huge datawindow loaded with processing, only some of which is used in any one application. The service-based architecture, on the other hand, provides us with a very lean model - we can make functionality available only when it is necessary by 'switching on' services; in other words, by populating reference variables only when the services they point to are required.
An understanding of reference variables is fundamental for PowerBuilder developers. Developers must become familiar with their underlying concepts in order to avoid common pitfalls, the reasons for which are often far from immediately obvious. Furthermore, the whole architecture of the PFC is based around reference variables. It exploits reference variables and service delegation to provide developers with a very memory-efficient architecture that allows functionality to be loaded into memory only when it is needed.