Welcome!

PowerBuilder Authors: Chris Pollach, Yeshim Deniz, Jayaram Krishnaswamy, Kevin Benedict, Avi Rosenthal

Related Topics: PowerBuilder

PowerBuilder: Article

Using Overloaded Functions to Determine Object Types

Using Overloaded Functions to Determine Object Types

Overloading allows two or more functions to have the same name and return type, if their signature is different. The function signature is defined by its number and types of arguments. When you call an overloaded function, PowerBuilder decides which version of the function to call based on the function signature. This can be exploited to determine an object's type at runtime. This article presents a couple of practical applications of this technique, and describes a third situation where a different technique is required.

Technique 1:
Validating Arguments
I first used this method of overloaded functions when I was trying to create a little utility for generating the outline of a new function. The utility writes out the function prototype, the various components of the function header and the outline of the code itself.

The problem I had was in testing the validity of each function argument. I make it a standard practice to check all arguments at the outset of each function. As discussed in the "Checking Validity" sidebar, there's no single method for validating PowerBuilder constructs. Sometimes IsValid() is appropriate, sometimes IsNull() applies and for some variable types you need to test for specific values. How could my utility handle all possible function argument types? It had to be a very intelligent utility...or I had to have a better way to validate arguments.

The solution to my argument validation problem is my g.bad() function. The key to g.bad() is that it has variants for each significant type. I can pass nearly any argument type to g.bad() and PB passes it to the appropriate function. That way my outline-the-function utility doesn't need to figure out what validity test is appropriate; PB does it instead. The essential pair of g.bad() functions with short-hand implementations are:

// Boolean g.bad( PowerObject parm )
return not IsValid( parm )// TRUE ->
bad!

// Boolean g.bad( any the_parameter )
return IsNull( parm ) // TRUE -> bad!

PowerObjects get passed to the first and simple types go to the second. When I came up with this solution I went about creating a lot of variants, anticipating several circumstances and writing g.bad() functions to catch them. The ones that get regular use are:

g.bad( string )
The string is valid if it has at least one nonblank character.
g.bad( string, Boolean )
The string is not null. The Boolean is always true, which means, "The_empty string("") and spaces are okay."
g.bad( datawindow, string )
The DataWindow is valid and "string" is a valid column on the DataWindow object (DWO).
g.bad( date )
g.bad( datetime )
g.bad( time )
Values of 1900-01-01 and 00:00:00 are not valid.

The arguments that aren't handled by any of the type-specific g.bad() calls are handled by the catch-all versions g.bad( PowerObject ) and g.bad( any ). If you pass in a more specialized type, PB calls the specialized function, such as g.bad( datetime ) or g.bad( string ). Otherwise PB calls the more general case, like g.bad( any ). Polymorphism, late-binding, all that much-ballyhooed OO stuff - it's genuinely useful!

The actual implementations account for some of the "gotchas" mentioned in the sidebar. IsNull() is applied to PowerObjects, for example. The g.bad() functions all set a global string, g.s_bad_details, in which a description of the kind of invalidity is written. For example for strings, s_bad_details may be <NULL>, <EMPTY> or <SPACES>; the angle brackets are a hint that the string is a description created by bad(), not the string's literal value. This g.bad() feature lets me write blocks like:

if g.bad( the_column ) then
g.bug( "Passed argument
'the_column' is invalid -
it is " + g.s_bad_details )
exit // Failed
end if
I don't have to know whether the_column is a string or a window. Since every argument sets g.s_bad_details, my nonintelligent utility can generate the g.bug() call, which displays the nature of the failure in my debug log window. Whatever information I feel would be useful to the programmer goes into g.s_bad_details. I code it once in the g.bad() call, and I don't have to hand-code anything thereafter.

There are occasions not handled by the g.bad() functions. For example, if a specific DataObject is required, then I test for that explicitly, right after the code block where I establish that the passed DataWindow is valid. I might have coded g.bad( datawindow, DataObject ) if the g.bad( datawindow, string ) signature wasn't already taken by the version that checks for a specific DWO column. The presence of the column is more often the requirement anyway; if the column is there, the DataObject is okay.

The g.bad() functions make it simple for the outline-the-function utility to write the code that checks the function arguments. I also use g.bad() throughout my code whenever I need to know that the item is really there.

Technique 2:
Passing Arguments to Response Windows
PowerBuilder provides the OpenWithParm() function for passing a parameter to the window being opened. If you need to pass only a single argument such as a string or DataWindow, then you can pass it directly and access it in the window via message.StringParm or message.PowerObjectParm. It's slightly more complicated when you need to pass more than one parameter. The traditional technique is to use a structure with a number of arrays of various types. If you need to pass three strings, you put them in the first three elements of the string array on the structure and pass the structure via the PowerObjectParm.

Str_parm - Generic Parameter Struc ture definition:
String string_arg[]
long long_arg[]
powerobject po_arg[]
I've long been hostile to the str_parm kind of parameter passing because it's not self-documenting and leads to errors, especially when windows are opened from more than one place. The typical code in the opened window is:
string ls_project_key, ls_policy_key,
ls_realtor
str_parm lstr_parm

lstr_parm = message.PowerObjectParm
ls_project_key =lstr_parm.string_arg[
1 ]
ls_policy_key = lstr_parm.string_arg[
2 ]
ls_realtor = lstr_parm.string_arg[ 3
]

Looking at the code we have to accept, as an article of faith, that the values coming in represent what we think they're representing. The array could in fact be the names of some hilarious coder's three favorite stooges; the code wouldn't know any better until later when some mysterious and spectacular error occurs. Even worse, the code can be unreadable when (lazy, inconsiderate, newbie) programmers directly use string_arg[ 1 ] instead of first assigning it to a variable with a meaningful name:

// Leave next programmer clueless!
dw_1.retrieve( lstr_parm.string_arg[ 1 ] )
The potential for confusion may seem like a silly concern, but it became serious when I was working on an application where a heavily used window was opened from literally dozens of places. It was a very flexible window called under many different circumstances. Over the years the number of parameters in the OpenWithParm() structure had increased to about thirty elements. It was a maintenance nightmare. Someone trying to open the window from yet another place had to decipher the convention concerning which array element was what. Despite exceptionally good documentation, it was such a perpetual headache that one developer was dedicated almost full time to supporting that sole window. For reasons you might imagine that person quit!

The solution was a simple application of the overloaded function approach. I added a couple arrays of "container" structures to the client's str_parm-equivalent:

global type str_key_value__any from
structure
string key
any value
end type

global type str_key_value__powerob
ject from structure
string key
powerobject value
end type

These are arrays so there can be any number of strings-any pairs or string-PowerObject pairs. You can anticipate how it works. If you want to pass a PowerObject, it goes into the string-PowerObject array. A simple type goes into the string-any array. The "key" element is a string used to identify each array element. Populating the parameter to pass looks like this:
string ls_project_key, ls_poli
cy_key, ls_realtor
str_parm lstr_parm
nv_pass lnv_pass

(... set the strings to the values you
want to pass ...)

if lnv_pass.of_set( lstr_parm, "pro
ject key", ls_project_key ) then exit // Failed
if lnv_pass.of_set( lstr_parm, "policy key",
ls_policy_key ) then exit
if lnv_pass.of_set( lstr_parm, "real
tor", ls_realtor ) then exit
OpenWithParm( w_test, lstr_parm )

The code on the receiving end is very similar:

string ls_project_key, ls_policy_key,
ls_realtor
str_parm lstr_parm
nv_pass lnv_pass

if lnv_pass.of_get_parm( lstr_parm )
then exit // Failed
if lnv_pass.of_get( lstr_parm, "pro
ject key", ls_project_key ) then exit
if lnv_pass.of_get( lstr_parm, "poli
cy key", ls_policy_key ) then exit
if lnv_pass.of_get( lstr_parm, "real
tor", ls_realtor ) then exit

To turn the "sending" code into the "receiving" code, add the of_get_parm() call and change each instance of "of_set()" into "of_get()".

This alternative is self-documenting: if you want to pass a project key, use the name "project key". The receiver will have to use project key as well, so there are no mysterious generically named parameters. Since the method makes no assumptions about offsets into arrays, another programmer can add additional parameters without worrying about whether the ith element has already been used. The of_get_parm() call does error-checking, which many programmers omit:

if not IsValid( message.PowerObject
Parm ) then exit // Not there!
if ClassName( message.PowerObject
Parm ) <> "str_parm" then exit // Wrong type of object!
the_str_parm = message.PowerObject Parm

With nv_pass, it took one nasty search-and-destroy mission to retrofit the old code, then that thirty-parameter maintenance nightmare was vanquished.

The implementation of of_set() is very simple, as suggested above. There's one version for PowerObjects and another for simple (any) types. Each adds another element to the array of the relevant type, then assigns the value and the key.

An essential feature is it first checks if the key already exists in the array before adding it at the end of the array. Otherwise, the subsequent of_get() would find the earlier instance of the key in the array because the of_get() searches for the key sequentially from the first element. The of_get() would then mistakenly return the originally set value! (Yes, this is a former bug.)

Listing 1 shows the code for the PowerObject version of of_set(); "oc" is the Object Container, an array of the second of the two structures shown above.

The get-the-value-back process is similar. It searches through the array looking for the key. If the key is found it returns the value, otherwise it displays a g.bug() debug message. You'd be amazed how useful that is:

Failed to find the key 'realtor'

Oh, I need to pass the realtor? OK.

There's one wrinkle with the of_get(). The of_set() functions pass their variable arguments by value, e.g., ls_realtor is passed in by value. No problem, of_set( any ) handles that fine. The of_get() has to pass that third argument by reference, however, so the variable can return the value previously set via of_set(). The by-reference aspect means the third of_get() argument can't be an any type. If the formal parameter of a function is a passed-by-reference any, then the actual argument must be an any, i.e., it can't be a string, long, or other more specific type. PowerBuilder won't do the string-to-any cast as you make the function call. Instead, PB gives you a "Cannot assign variable of type string to type any" compile-time error. Therefore there's an of_get() for each of the simple types.

We want only one function to traverse the array looking for the key-value pair. That function is nontrivial and we don't want to maintain it in 15 places. All the simple type of_get() functions call one of_get_ac() function (the "ac" stands for "Any Container"). The of_get_ac() function finds the value using the key and places the value into an instance variable on the nv_pass NVO. The of_get() can then assign that reference variable to the string or long or whatever reference variable was passed in. The instance variable is iany_value in the example of_get() shown in Listing 2.

It's a way to work around the restriction on any variables passed by reference. The proliferation of of_get() functions isn't so bad: you simply change the third argument to a new type and do a SaveAs to create the new function. If you ever try to of_get() an unsupported type, the PB compiler will remind you that it needs to be added.

The nv_pass key-value-pairs method is as easy to use as the conventional structure-of-arrays approach, and it's more robust and extensible. Again, the crucial device was using overloaded functions to differentiate the PowerObjects from the simple types.

Technique 3:
Determining Whether an Object Is a GraphicObject
This is an instance where the overloaded functions technique didn't work. I was attempting to solve the problem with my f_context() function: it accepts a PowerObject and repeatedly calls GetParent() until the enclosing window is found, building a "context" string as it goes. It's helpful when you're trying to address a control several object levels removed, such as a DataWindow on a tab page on a tab on a window. The context string it generates looks like:

w_sales.tab_projections.
tabpage_for_sale.dw_for_sale
Since the GetParent() function is available for GraphicObjects but not for the PowerObject passed into the function, f_context() casts the PowerObject to a GraphicObject to get access to GetParent(). Listing 3 shows the relevant code.

The problem occurs when f_context() is called in something other than a GraphicObject. It accepts a PowerObject instead of a GraphicObject because I want it to work with NVOs as well. With NVOs, GetParent() makes no sense (and isn't available), so f_context() simply returns the NVO's ClassName() concatenated with the_location argument. However, f_context() has to detect that the passed PowerObject is not a GraphicObject, so it knows better than to attempt the cast to the GraphicObject.

So f_context() needs g.is_graphic _object(), which returns true if its argument is a GraphicObject.

I first wrote g.is_graphic_object() using the overloaded functions technique. I created two versions of g.is_graphic_object() - one taking a PowerObject, the other having a GraphicObject as its argument. I expected PowerBuilder to call the more specialized GraphicObject version when a GraphicObject was passed in, and to call the more general PowerObject version when an NVO was passed. It's analogous to calling g.bad( string ) when a string is passed, and g.bad( any ) when there isn't a more specialized variant of g.bad().

Wrong! To my dismay and confusion PB always called the PowerObject version even when a GraphicObject was passed. Why? If you look at the PB object hierarchy (see Figure 1), you'll see that GraphicObject is a kind of PowerObject. They're not at the same level in the object hierarchy. I'm guessing that PB's "pick the most specific overloaded variant" rule applies within a given level. A PowerObject and GraphicObject are at different levels, so it doesn't work.

What was the workaround? It's another trick: since assigning a GraphicObject generates a system error, we can detect a non-GraphicObject by attempting the assignment and seeing whether a system error occurs. In English:

  1. A flag is set that means "ignore the next system error."
  2. The assignment is attempted.
  3. In the system error handler a global flag is set indicating that the system error occurred.
  4. Back in g.is_graphic_object(), the "system error occurred" flag shows that the NVO is not a graphic object, so the function returns false.

With GraphicObjects no system error occurs, so we turn off the "ignore the next system error" flag and return true. As I've said, it's a trick. The details are available in the g.is_graphic_object() source.

The best solution occurs to me only as I'm completing this article: I should have two versions of g.context(), one for NVOs and another for GraphicObjects, then the overloaded functions would handle f_context()'s problem directly. Time to dust off the global search-and-replace tool!

Summary
If you find yourself in a situation where you need to identify an object type, overloaded functions can be a powerful technique. Since PB automatically calls the appropriate overloaded function, the code is straightforward and the execution path is simple. It's especially convenient because PB will call a specialized variant, if applicable, otherwise it will invoke the more general function. If overloading doesn't suffice you can always test for a specific type by attempting an assignment to that type and seeing if a system error results.

The ClassDefinition.DataTypeOf at-tribute is another way to identify object types, but that tends to resolve at a greater level of detail than you need. To differentiate PowerObjects from simple types, for example, you would need to classify the approximately 135 possible DataTypeOf (Object!) values into three groups: PowerObjects, simple types and others. It would be a massive choose-case requiring updates with each new version of PB.

The examples are useful (I hope). With g.bad() you can pretty much forget about what function to call when. Just apply g.bad() when you want to test whether something is really there. With nv_pass, your OpenWithParm()s will be easier to understand, extend and especially debug. f_context() is a great way to figure out and use those deeply nested object paths. All are available from the SYS-CON Web site.

More Stories By Hoyt Nelson

Hoyt Nelson is an independent contractor, a 10-year PowerBuilder veteran, and father to the amazing Nelson, Kent, Emma, and Molly (Hi, kids!).

Comments (0)

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.