Welcome!

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

Related Topics: PowerBuilder

PowerBuilder: Article

Writting Functions For All Data Window-ish Types

Writting Functions For All Data Window-ish Types

What's the word for a set of objects: DataWindows, DataStores, or DataWindowChilds? If there is a word, I don't know it. Objects don't have any relationship in the PowerBuilder object hierarchy, so that doesn't help.Let's call them DataObjects...oops, that's taken! Lacking any other word, let's stipulate that the term DataWidgets refers to these various DataWindow-ish types.

Many native PB functions work for all kinds of DataWidgets. SetItem() applies to DataWindows, DataWindowChilds, and DataStores, for example; so do the GetItemX() functions, such as GetItemString() and GetItemNumber(). It's nice that these functions operate identically for all DataWidget types. If I know how to call SelectRow() for one, I can call it for the others.

Which brings up the problem addressed in this article.

One of my PowerBuilder pet peeves is the fact that a user-written function can't handle different DataWidget types. Instead, separate functions have to be written for each kind of DataWidget. You can't pass a DataStore to a function that takes a DataWindow argument, for example, so you have to re-create that function to take a DataStore argument. Later you want to apply that function to a DataWindowChild, and you have to create yet another variant.

This article presents a simple technique for overcoming this weakness in the PowerBuilder architecture. It allows me to maintain source for just one kind of DataWidget and automatically generate the source for the other kinds. By using this technique you can reduce code maintenance and make your functions easier to call.

The Evolution of f_get_item()
This section describes an example of how my pet peeve was manifested when our development team started using a new function, f_get_item():

if f_get_item( dw_list, 1, "ssn", ls_ssn ) then return
which is the equivalent to:
ls_ssn = dw_list.GetItemString( 1, "ssn" )
if ls_ssn = "" then return // Failed, maybe?!
f_get_item() has certain advantages over the native functions, such as GetItemString().
  • It provides much better error messages than the GetItemX()'s barely informative "Invalid row/column specified." It's much more useful to get something like "Column 'ssn' does not appear on DataObject 'dw_cust_info'."
  • It's "type safe" in the sense that you can apply it to a column of any type, and f_get_item() will figure out which of the GetItemX() functions is applicable. The value is always returned as a string, which the programmer can cast to the required type as necessary.
The original f_get_item() handled the most common case where you get the value at the specified row and column from the Primary! buffer. The other developers also thought the function was useful, and soon many f_get_item() calls appeared in our application.

Then f_get_item() evolved.

First f_get_item() accrued new capabilities. Someone needed to get information from the Delete! buffer, so we created f_get_item2(), which allows you to specify the buffer. At that time we also added a Boolean, which (if true) would give you the DataWindow row-column's original value. Then another requirement came up - to get the display value - so f_get_item3() was created.

The new variants' prototypes were confusing and cumbersome. For example:

Boolean f_get_item2( datawindow, long, string, dwBuffer, Boolean, ref string )
Next, f_get_item() evolved in a direction directly related to this article: it was adapted to apply to different types of DataWidgets, with "ds" versions for DataStores and "dwc" versions for DataWindowChilds. We had to SaveAs the original functions under new names, then change the arguments to accept the different DataWidget types.

There are now nine variants on the original f_get_item(), three for each of the DataWidget types (see Table 1).

This is ugly for several reasons:

  1. Too many different names.
  2. The programmer has to figure out which function applies to a DataWidget.
  3. The variants with buffers and Booleans have too many arguments and are difficult to call.
  4. There's lots of code duplication.
  5. It's a maintenance nightmare; fix each bug in at least three places, maybe more.
The following is a description of a cleaner solution.

Resolving Names and Code Duplication with an NVO
This section shows an improved design for functions applicable to just one DataWidget type. Let's select DataStores. I was willing to contemplate this redesign because I intended to automatically replicate the code for the other DataWidget types, minimizing the recoding effort.

Since PB5, it's been conventional for similar functions to be grouped together on a nonvisual userObject, or NVO. Moving the functionality of the f_get_item() functions onto an NVO offered an opportunity to improve the function prototypes, especially the names and the too many function arguments. I came up with the following:

get_item() // Primary buffer
get_primary() // Same as get_item()
get_delete() // Delete buffer
get_filter() // Filter buffer
get_original() // Original value
get_display() // Display value
These functions all have the same prototype as the original f_get_item() function, which makes them easy to call. Instead of the programmer having to remember where the Delete! buffer argument goes, he or she simply calls get_delete(). The get_primary() function is provided as an alternative to get_item(), in case the programmer prefers the consistency of calling get_primary() in the midst of get_delete() or get_filter() calls.

All these functions call a seventh function, get_item_main(), that does all the work. Thus, the six functions above are wrapper functions that "remember" how to call get_item_main(), and only get_item_main() has the long complicated list of arguments. There's no code duplication: the only function with more than a few lines is get_item_main().

How does this relate to the larger problem? I was disconcerted when f_get_item() proliferated into nine functions. Now there are seven functions for just DataStores; isn't it getting worse?

Not in my opinion. The goal in creating wrapper functions was ease of use for the programmer. All the functions take the same minimal set of arguments

get_xxx( datastore, long row, string column, ref string value )
so the programmer knows how to call all the functions. Also, the new function names are a lot easier to remember than the long list of arguments needed for f_get_item2() and f_get_item3(). Finally, get_item_main() can't be called with incompatible arguments, such as specifying the Filter! buffer and also the Boolean that says, "give me the original value." That's because get_item_main() is private, can be called only via the wrapper functions, and uses the correct arguments.

More to the point: we've created seven functions where before we had three, but the re-creation of the DataStore functions for the other DataWidget types is automatic so there's actually fewer functions to maintain. I'd have hesitated to create a lot of DataStore functions if I was faced with the prospect of manually creating them for DataWindows and DataWindowChilds as well. Being relieved of that responsibility means I can implement the design I think best for one DataWidget, knowing that it will be painless to propagate the design to the other DataWidgets.

The reorganization of the f_get_item() into seven functions on an NVO has the following advantages:

  • Easier calls: All the public functions have the same prototype with the minimum number of arguments so the calls are as concise as possible.
  • Better function names: The names of the public functions "contain" the specialized purpose of each function, which makes them self- documenting and easy to remember. If I want to get a value from the filter buffer, for example, "get_filter()" is obviously the applicable function.
  • No code duplication: Only get_item_main() contains a significant amount of PowerScript.
  • Invalid calls are impossible: The only function with possibly incompatible arguments is private, and the public wrappers to that function "know" how to call it.
  • NVO's functions are automated: We don't need to worry about propagating the NVO's functions for the other DataWidget types because that's automated.

If you don't like using function names such as get_delete() and get_original() to identify what you're after, an alternative would be to have just two versions, one of which takes an additional argument that handles all the less-used alternatives:

get_item( datastore, row, column, ref result )
get_item( datastore, row, column, type, result )
About 95% of the time you'd call the first function, which does the conventional GetItemX() from the Primary! buffer. The second version allows the programmer to specify various magic strings as the "type," for example, "primary," "filter," "delete," "original," or "display." (When passing magic strings as parameters, it's nice to allow the programmer to specify the minimum number of characters that determine the string [e.g., one ["f"] or two ["de"] characters], and make sure that case is immaterial.) That way you get the economy of just four arguments most of the time, and one more function handles the alternatives with just one additional argument. One disadvantage is the use of magic strings, which have to be remembered, but the parameter "delete" (or "de") isn't much harder to remember than get_delete().

Listings 1 and 2 show the source for get_delete() and get_ item_main(). Note that my usual error checking has been removed from all code listings, so the core code isn't obscured. The g.bug() calls simply display their argument to expose the error to the programmer. The other functions should be self-explanatory.

Generating the NVOs for the Other DataWidget Types
Okay, we've improved our f_get_item() design for DataStores. Now what do we do for the other DataWidget types? We can't SaveAs the NVOs, right? Well, yes we can, sort of. The function f_create_overloaded_dw_and_dwc_objects() adapts the DataStore NVO for DataWindows and DataWindowChilds (see Listing 3). Here's an outline of its activities:

  1. Export the DataStore NVO source.
  2. Modify the source for DataWindows.
  3. Write out the DataWindow source.
  4. Modify the source for DataWindowChilds.
  5. Write out the DataWindowChild source.

The modifications are about what you'd expect: change "DataStore" to "DataWindow" (for example), and change the name of the argument to match the revised type. Not so obviously, the function also changes the "parent" object from which the NVO inherits, as discussed in Note 2 below.

The source is formatted so it can be imported into a PBL using the Library Painter's import facility. It'd be nice to use the open repository CASE API (ORCA) to import the modified source directly, but I couldn't get that to work in a couple of tries, and the manual import takes only a minute.

The benefit of this generate-two-objects-from-one-source method was demonstrated during my testing. It turned out I had forgotten to set the get_filter() function's "result" argument to pass by reference. I corrected that defect, reran f_create_overloaded_dw_and_dwc_objects(), and imported the new DW and DWC source files. It probably took two minutes. There was no manual coding required for the two generated objects. The following are some notes about the generation process.

1. Running f_create_overloaded_dw_and_dwc_objects()
An easy way to run f_create_overloaded_dw_and_dwc_objects() is to add it to your application. With our application we have a Tools menu that offers several items to our users. The Tools menu also has a collection of developer-only menu items, visible only during development. The "Create DW and DWC functions" menu item's visibility is set in the Tools menu item's Selected event:

// Visible in development only m_create_dw_and_dwc.visible = not f_running_exe()
where f_running_exe() is something like:
// Returns TRUE if running an executable return handle( GetApplication() ) <> 0
Building it into the application makes it readily available when you have changes to propagate.

2. The three DataWidget NVOs form one inheritance hierarchy, so calls are easy
We have three NVOs, one for each kind of DataWidget. How do we apply them? How do we call the various functions? The application of the DataWidgets is simplified by the fact that they're all related through inheritance.

In my framework all NVOs ultimately inherit from u_base_nvo. The DataStore object inherits from u_base_nvo; f_create_overloaded_dw_and_dwc_objects() modifies the DataStore source so the DataWindow NVO inherits from the DataStore NVO, and the DataWindowChild NVO inherits from the DataWindow NVO:

// Fix the source for U_DW_FUNCTIONS
ls_source = f_substr( ls_source, "from u_base_nuo", & "from u_ds_functions" )
...
// Fix the source for U_DWC_FUNCTIONS
ls_source = f_substr( ls_source, "from u_ds_functions", & "from u_dw_functions" )
Yielding the object hierarchy:
u_base_nvo
|
u_ds_functions
|
u_dw_functions
|
u_dwc_functions
This hierarchy makes the application of the NVOs straightforward. The u_dwc_function's NVO is instantiated as a global object f, so the function calls look very similar to the original:
// Global declarations
u_dwc_functions f

// Start-up code
f = create u_dwc_functions

// Typical call
if f.get_item( dw_list, 1, "ssn", ls_ssn ) then return // Failed

Because of their inheritance relationship the functionality of all three DataWidget NVOs is instantiated in the f-object, and PowerBuilder figures out which function to call depending on the type of DataWidget passed. If a DataWindow is passed in, for example, the get_item() on u_dw_functions will be called.

This was the original goal: to allow user-written functions to have a single function that applies to all DataWidget types. My pet peeve is resolved. It's not really a single function, of course, but I have to maintain only one function.

By the way, all the data types except DateTime can be directly converted from the string that's returned by reference by the f.get_item() functions:

date ldt_birthday
string ls_result

// Get a date variable
if f.get_item( dw_list, 1, "birthdate", ls_result ) then return
ldt_birthday = date( ls_result )

In the example above, the date string returned in ls_result is in the YYYY-MM-DD format that's immune to the short date problem (see "The Windows Short Date Revisited" article [PBDJ, Vol. 6, issue 6]).

Since PB doesn't provide a native DateTime( string ) function, you should roll your own. Declare a global function with the name DateTime() that takes a string argument and returns a DateTime value. (See Listing 4 for the DateTime() script.) Then you can convert DateTime variables just like the example above:

DateTime ldtm_birthday
string ls_result

// Get a DateTime variable
if f.get_item( dw_list, 1, "birthdate", ls_result ) then return
ldtm_birthday = DateTime( ls_result )

3. f_get_library_object_from_stash()
This function appears at the top of f_create_overloaded_ dw_and_dwc_objects(). Given an object name, the function returns the name of the PBL that contains the object along with the object's comment and object type. For purposes of f_create_overloaded_dw_and_dwc_objects(), only the containing PBL is required; we need to know which PBL contains the u_ds_functions NVO so we can get the NVO's source.

If you want to re-create the functionality of f_get_library_object_from_stash(), here's a description of how it works. First you need to stash the library list. Since you don't have f_stash_library_object_list(), here's that function's outline:

  1. Find the PB.INI file.
  2. Extract from PB.INI the list of PBLs in the current application.
  3. For each PBL, do a LibraryDirectory() to get the PBL's objects, and import the object list into a DataWindow.
  4. LibraryImport() the (populated) DataWindow into a PBL in your application.
This provides a place where you can look up the PBL location of any object.

Then f_get_library_object_from_stash() does the lookup using the application object's imported DataWindow. It does a find() to locate the object, then does GetItemString()s to identify the containing PBL and the object's comment and type.

If you don't want to implement all this, simply hard-code the location of the base NVO, that is, the PBL that contains your equivalent to u_ds_functions.

4. get_column_type()
This function is defined on u_ds_functions() so it's automatically propagated for the other DataWidget types. It's called in get_ item_main() to determine which of the various GetItemX() functions apply.

Here are the essential parts of get_column_type():

// Get the "raw" column type
ls_column_type = the_ds.describe( the_column + ".coltype" )
if pos( "?!", ls_column_type ) <> 0 then exit // Failed

// Truncate the (10) part for decimals
ll_pos = pos( ls_column_type, "(" )
if ll_pos > 0 then ls_column_type = mid( ls_column_type, 1, ll_pos - 1 )

// Treat character data as string data
if left( ls_column_type, 4) = "char" then ls_column_type = "string"

// Treat numeric types OTHER than decimal as number
if ls_column_type <> "decimal" and f_dw_col_type_is_numeric(
ls_column_type ) then ls_column_type = "number"

where f_dw_col_type_is_numeric()'s key lines are:

string ls_numeric_types = &
"|number|dec|decimal|double|float|int|integer|long|real|unsignedint|unsignedinteger|unsignedlong|uint|ulong|"

lb_is_numeric = pos( ls_numeric_types, "|" + the_column_type + "|" ) <> 0

get_column_type() returns "string" for any char type, "number" for any nondecimal numeric type, and "decimal" for any variation on decimal(x), for example, decimal(10). These are the type distinctions needed to select the appropriate GetItemX() function.

5. Not all attributes apply everywhere
Some differences between the DataWidgets preclude "one size fits all" PowerScript. For example, for DataStore and DataWindows the DataObject attribute is directly available for error messages, so you can tell the programmer exactly which DataWindow Object is missing column "ssn", for example. It turns out that DataWindow Childs don't have a DataObject attribute. You have to do a describe() on the DWC's "parent" DataWindow to get the DWC's DataObject, and you're out of luck if you don't have that DataWindow in your script.

This is handled by removing that part of the error message when the DWC NVO is generated. Below, a hard-coded string is replaced with the empty string (""):

// Fix the source for DWC_FUNCTIONS (the "C" is new)
ls_source = f_substr( ls_source, ' on DataObject " + upper( the_dwc.DataObject )', '"' )
ls_source = f_substr( ls_source, '+ " on DataObject " + upper( the_dwc.DataObject )', "" )

It's usually dangerous to depend on a hard-coded string because it might change someday. Fortunately, in this case the feedback will be immediate when the DWC-NVO source fails to import.

6. Not all functions apply everywhere
I have a favorite function, f_select_and_scroll_to_row(), that combines a SelectRow(), ScrollToRow(), and SetRow(). That function can't be applied within u_ds_functions because ScrollToRow() can be applied to DataWindows and DataWindowChilds but not DataStores. If I wanted to implement f_select_and_scroll_to_row() in the f-object hierarchy, I could write:

// In u_ds_functions
the_dw.SetRow( the_row )
the_ds.SelectRow( the_row, true )
// the_ds.ScrollToRow( the_row )
The ScrollToRow() is commented out because it's not applicable to DataStores. I could then make the commented-out line "active" for the other DataWidgets by removing the double slashes in f_create_overloaded_dw_and_dwc_objects():
// Fix the source for DW_FUNCTIONS
ls_source = f_substr( ls_source, "// the_ds.ScrollToRow ", "the_ds.ScrollToRow " )

A block of code can be conditionally "commented in" by surrounding it with labeled slash-asterisk pairs:

/*dwc-on
... dwc-specific code ...
dwc-on*/
Removing "/*dwc-on" and "dwc-on*/" from the source would make the in-between code "active" for DWCs. Code can be commented out by preceding the labeled slash-asterisk pairs with double slashes:
///*dwc-off ... code that is invalid for dwc's ... //dwc-off*/
Removing the double slashes makes the slash-asterisk pairs effective, so the block of code between them becomes a comment for DWCs only.

In other cases it may be necessary to actually comment out or remove entire functions, which would make f_create_overloaded_dw_and_ dwc_objects() considerably more complex.

Another option might be to insert DataWidget-specific objects into the hierarchy:

u_base_nvo
|
u_ds_functions
|
u_dw_functions
|
u_dw_specific_functions
|
u_dw_specific_functions_for_dwc
|
u_dwc_functions
For example, suppose that u_dw_specific_functions is a place to put functions that apply to DataWindows and DataWindowChilds but not DataStores. Then u_dw_specific_functions_for_dwc can be generated by f_create_overloaded_dw_and_dwc_objects() to apply those functions to DataWindowChilds.

I'm not certain I have the DataWidget hierarchy right; it may make more sense to have the DWC in the middle. Fortunately, it's easy enough to change later, since any of the objects is easily created from the others.

7. Generating NVOs has disadvantages
A major benefit of DataWidget generation is ease of maintenance, because only the source for DataStores has to be maintained. A necessary cost of the approach is no customization in the descendent objects. If you change u_dw_functions, for example, those changes will be lost the next time you generate that object. In effect, all customization has to be done in the base u_ds_functions object, with tricks such as removing double slashes required to change the behavior in descendants. That's always harder than doing it directly. It's advisable to put "Do not edit this object" in the comments for the generated NVOs to prevent newbies from wasting their time.

Another disadvantage is inconsistent implementation of DataWidget functions, with a few on your f-object and the rest in their current no-f format. DataWidget generation makes sense only for commonly used functions that apply to all the DataWidgets, which are only a subset of your DataWindow functions. You can use the extended hierarchy discussed above to provide a place for DataWindow functions that aren't applicable DataStores. That's only a partial solution, however, because some of those won't work with DataWindowChilds. Further, moving functions to u_ds_functions or another NVO is nontrivial, something you won't want to undertake for all your functions. Compounding the problem is the need to retrofit your code to call the new functions. (Has anyone invented a decent search-and-replace utility yet?) So you'll necessarily have some functions that you call with your f-object and some you call via your traditional methods.

Still, for those occasions when you're annoyed at the need to re-create a function for yet another DataWidget type, it's just as easy to move it to your u_ds_functions equivalent and run the autogenerate facility.

8. An alternative approach
PBDJ
editor-in-chief John Olson suggest a technique you may find preferable to the one proposed here. A function can take a PowerObject instead of a DataWidget, and the dynamic keyword would be applied so PB can apply the appropriate function at runtime:

get_item ( PowerObject the_po, long the_row, string the_column, ref string the_result )

the_result = the_po.dynamic GetItemString( the_row, the_column )

This allows a single function to accept all the DataWidget types.

A disadvantage is lack of compile-time checking: the function would accept any Power Object as the code is composed, and invalid PowerObjects (such as a window or CommandButton) would produce spectacular errors at runtime. That's a minor drawback, however, that could be remedied by verifying that the PowerObject is a DataWidget before executing the dynamic function call.

John points out another problem with the dynamic technique: it doesn't allow you to access the DataWidget's attributes. The PowerObject has to be cast to its specific DataWidget type before you can reference the DataWidget attributes. I like to include the DataWidget's DataObject in error messages, for example. My code would have to have branches for each DataWidget type to make the DataObject attribute available, defeating the economy of the dynamic approach. Then again, I could call a get_dataobject() function that does the cast and returns the DataObject (if available).

John's idea certainly merits consideration. I may end up with a mixed model that combines the dynamic technique with my DataWidget hierarchy.

Summary
Generating DataWidget objects in a single inheritance hierarchy offers several advantages:

  • Only one source object needs to be maintained, and changes are easily propagated to the other DataWidget types.
  • The implementation requires just one global object, since the final descendent subsumes all the functionality of its ancestors.
  • Calls are identical across DataWidget types; PowerBuilder figures out which function to call based on the DataWidget type, so the programmer can ignore that issue.
This is a new idea for me, so my experience is limited. My intention, however, is to cease doing a SaveAs each time a function has to be adapted to another DataWidget type. Instead, I'll move the function to u_ds_functions and fire off my "Create DW and DWC functions" menu item.

So far I've added a set of set_item() functions that are the counterparts to the get_item() functions. This gives me one vernacular for getting and setting DataWidget values with the enhanced error messages and the type-safe application. I expect that eventually a lot of my DataWidget functionality will migrate to the f-hierarchy, for ease of use and maintenance.

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.