Selected PowerScript Topics

About this chapter

This chapter describes how to use elements of the PowerScript language in an application.

Dot notation

Dot notation lets you qualify the item you are referring to instance variable, property, event, or function with the object that owns it.

Dot notation is for objects. You do not use dot notation for global variables and functions, because they are independent of any object. You do not use dot notation for shared variables either, because they belong to an object class, not an object instance.

Qualifying a reference

Dot notation names an object variable as a qualifier to the item you want to access:

objectvariable.item

The object variable name is a qualifier that identifies the owner of the property or other item.

Adding a parent qualifier

To fully identify an object, you can use additional dot qualifiers to name the parent of an object, and its parent, and so on:

parent.objectvariable.item

A parent object contains the child object. It is not an ancestor-descendant relationship. For example, a window is a control's parent. A Tab control is the parent of the tab pages it contains. A Menu object is the parent of the Menu objects that are the items on that menu.

Many parent levels

You can use parent qualifiers up to the level of the application. You typically need qualifiers only up to the window level.

For example, if you want to call the Retrieve function for a DataWindow control on a tab page, you might qualify the name like this:

w_choice.tab_alpha.tabpage_a.dw_names.Retrieve()

Menu objects often need several qualifiers. Suppose a window w_main has a menu object m_mymenu, and m_mymenu has a File menu with an Open item. You can trigger the Open item's Selected event like this:

w_main.m_mymenu.m_file.m_open.EVENT Selected()

As you can see, qualifying a name gets complex, particularly for menus and tab pages in a Tab control.

How many qualifiers?

You need to specify as many qualifiers as are required to identify the object, function, event, or property.

A parent object knows about the objects it contains. In a window script, you do not need to qualify the names of the window's controls. In scripts for the controls, you can also refer to other controls in the window without a qualifier.

For example, if the window w_main contains a DataWindow control dw_data and a CommandButton cb_close, a script for the CommandButton can refer to the DataWindow control without a qualifier:

dw_data.AcceptText()
dw_data.Update()

If a script in another window or a user object refers to the DataWindow control, the DataWindow control needs to be qualified with the window name:

w_main.dw_data.AcceptText()

Referencing objects

There are three ways to qualify an element of an object in the object's own scripts:

  • Unqualified:

    li_index = SelectItem(5)

    An unqualified name is unclear and might result in ambiguities if there are local or global variables and functions with the same name.

  • Qualified with the object's name:

    li_index = lb_choices.SelectItem(5)

    Using the object name in the object's own script is unnecessarily specific.

  • Qualified with a generic reference to the object:

    li_index = This.SelectItem(5)

    The pronoun This shows that the item belongs to the owning object.

This pronoun

In a script for the object, you can use the pronoun This as a generic reference to the owning object:

This.property
This.function

Although the property or function could stand alone in a script without a qualifier, someone looking at the script might not recognize the property or function as belonging to an object. A script that uses This is still valid if you rename the object. The script can be reused with less editing.

You can also use This by itself as a reference to the current object. For example, suppose you want to pass a DataWindow control to a function in another user object:

uo_data.uf_retrieve(This)

This example in a script for a DataWindow control sets an instance variable of type DataWindow so that other functions know the most recently used DataWindow control:

idw_currentdw = This

Parent pronoun

The pronoun Parent refers to the parent of an object. When you use Parent and you rename the parent object or reuse the script in other contexts, it is still valid.

For example, in a DataWindow control script, suppose you want to call the Resize function for the window. The DataWindow control also has a Resize function, so you must qualify it:

// Two ways to call the window function
w_main.Resize(400, 400)
Parent.Resize(400, 400)

// Three ways to call the control's function
Resize(400, 400)
dw_data.Resize(400, 400)
This.Resize(400, 400)

GetParent function

The Parent pronoun works only within dot notation. If you want to get a reference to the parent of an object, use the GetParent function. You might want to get a reference to the parent of an object other than the one that owns the script, or you might want to save the reference in a variable:

window w_save
w_save = dw_data.GetParent()

For example, in another CommandButton's Clicked event script, suppose you wanted to pass a reference to the control's parent window to a function defined in a user object. Use GetParent in the function call:

uo_winmgmt.uf_resize(This.GetParent(), 400, 600)

ParentWindow property and function

Other tools for getting the parent of an object include:

  • ParentWindow property -- used in a menu script to refer to the window that is the parent of the menu

  • ParentWindow function -- used in any script to get a reference to the window that is the parent of a particular window

For more about these pronouns and functions, see the section called “Pronouns” in PowerScript Reference and the section called “ParentWindow” in PowerScript Reference.

Objects in a container object

Dot notation also allows you to reach inside an object to the objects it contains. To refer to an object inside a container, use the Object property in the dot notation. The structure of the object in the container determines how many levels are accessible:

control.Object.objectname.property
control.Object.objectname.Object.qualifier.qualifier.property

Objects that you can access using the Object property are:

  • DataWindow objects in DataWindow controls

  • External OLE objects in OLE controls

These expressions refer to properties of the DataWindow object inside a DataWindow control:

dw_data.Object.emp_lname.Border
dw_data.Object.nestedrpt[1].Object.salary.Border

No compiler checking

For objects inside the container, the compiler cannot be sure that the dot notation is valid. For example, the DataWindow object is not bound to the control and can be changed at any time. Therefore, the names and properties after the Object property are checked for validity during execution only. Incorrect references cause an execution error.

For more information

For more information about runtime checking, see Optimizing expressions for DataWindow and external objects.

For more information about dot notation for properties and data of DataWindow objects and handling errors, see DataWindow Reference.

For more information about OLE objects and dot notation for OLE automation, see Using OLE in an Application.

Constant declarations

To declare a constant, add the keyword CONSTANT to a standard variable declaration:

CONSTANT { access } datatype constname = value

Only a datatype that accepts an assignment in its declaration can be a constant. For this reason, blobs cannot be constants.

Even though identifiers in PowerScript are not case sensitive, the declarations shown here use uppercase as a convention for constant names:

CONSTANT integer GI_CENTURY_YEARS = 100
CONSTANT string IS_ASCENDING = "a"

Advantages of constants

If you try to assign a value to the constant anywhere other than in the declaration, you get a compiler error. A constant is a way of assuring that the declaration is used the way you intend.

Constants are also efficient. Because the value is established during compilation, the compiled code uses the value itself, rather than referring to a variable that holds the value.

Controlling access for instance variables

Instance variables have access settings that provide control over how other objects' scripts access them.

You can specify that a variable is:

  • Public

    Accessible to any other object

  • Protected

    Accessible only in scripts for the object and its descendants

  • Private

    Accessible in scripts for the object only

For example:

public integer ii_currentvalue
CONSTANT public integer WARPFACTOR = 1.2
protected string is_starship

// Private values used in internal calculations
private integer ii_maxrpm
private integer ii_minrpm

You can further qualify access to public and protected variables with the modifiers PRIVATEREAD, PRIVATEWRITE, PROTECTEDREAD, or PROTECTEDWRITE:

public privatewrite ii_averagerpm

Private variables for encapsulation

One use of access settings is to keep other scripts from changing a variable when they should not. You can use PRIVATE or PUBLIC PRIVATEWRITE to keep the variable from being changed directly. You might write public functions to provide validation before changing the variable.

Private variables allow you to encapsulate an object's functionality. This technique means that an object's data and code are part of the object itself and the object determines the interface it presents to other objects.

If you generate a component from a custom class user object, you can choose to expose its instance variables in the component's interface, but private and protected instance variables are never exposed.

For more information

For more about access settings, see the section called “Declarations” in PowerScript Reference.

For more about encapsulation, see Selected Object-Oriented Programming Topics.

Resolving naming conflicts

There are two areas in which name conflicts occur:

  • Variables that are defined within different scopes can have the same name. For example, a global variable can have the same name as a local or instance variable. The compiler warns you of these conflicts, but you do not have to change the names.

  • A descendant object has functions and events that are inherited from the ancestor and have the same names.

If you need to refer to a hidden variable or an ancestor's event or function, you can use dot notation qualifiers or the scope operator.

Hidden instance variables

If an instance variable has the same name as a local, shared, or global variable, qualify the instance variable with its object's name:

objectname.instancevariable

If a local variable and an instance variable of a window are both named birthdate, then qualify the instance variable:

w_main.birthdate

If a window script defines a local variable x, the name conflicts with the X property of the window. Use a qualifier for the X property. This statement compares the two:

IF x > w_main.X THEN ...

Hidden global variables

If a global variable has the same name as a local or shared variable, you can access the global variable with the scope operator (::) as follows:

::globalvariable

This expression compares a local variable with a global variable, both named total:

IF total < ::total THEN ...

Use prefixes to avoid naming conflicts

If your naming conventions include prefixes that identify the scope of the variable, then variables of different scopes always have different names and there are no conflicts.

For more information about the search order that determines how name conflicts are resolved, see the section called “Declarations” in PowerScript Reference and the section called “Calling Functions and Events” in PowerScript Reference.

Overridden functions and events

When you change the script for a function that is inherited, you override the ancestor version of the function. For events, you can choose to override or extend the ancestor event script in the painter.

You can use the scope operator to call the ancestor version of an overridden function or event. The ancestor class name, not a variable, precedes the colons:

result = w_ancestor:: FUNCTION of_func(arg1, arg2)

You can use the Super pronoun instead of naming an ancestor class. Super refers to the object's immediate ancestor:

result = Super:: EVENT ue_process()

In good object-oriented design, you would not call ancestor scripts for other objects. It is best to restrict this type of call to the current object's immediate ancestor using Super.

For how to capture the return value of an ancestor script, see Return values from ancestor scripts.

Overloaded functions

When you have several functions of the same name for the same object, the function name is considered to be overloaded. PowerBuilder determines which version of the function to call by comparing the signatures of the function definitions with the signature of the function call. The signature includes the function name, argument list, and return value.

Overloading

Events and global functions cannot be overloaded.

Return values from ancestor scripts

If you want to perform some processing in an event in a descendant object, but that processing depends on the return value of the ancestor event script, you can use a local variable called AncestorReturnValue that is automatically declared and assigned the return value of the ancestor event.

The first time the compiler encounters a CALL statement that calls the ancestor event of a script, the compiler implicitly generates code that declares the AncestorReturnValue variable and assigns to it the return value of the ancestor event.

The datatype of the AncestorReturnValue variable is always the same as the datatype defined for the return value of the event. The arguments passed to the call come from the arguments that are passed to the event in the descendant object.

Extending event scripts

The AncestorReturnValue variable is always available in extended event scripts. When you extend an event script, PowerBuilder generates the following syntax and inserts it at the beginning of the event script:

CALL SUPER::event_name

You see the statement only if you export the syntax of the object.

Overriding event scripts

The AncestorReturnValue variable is available only when you override an event script after you call the ancestor event using the CALL syntax explicitly:

CALL SUPER::event_name

or

CALL ancestor_name::event_name

The compiler does not differentiate between the keyword SUPER and the name of the ancestor. The keyword is replaced with the name of the ancestor before the script is compiled.

The AncestorReturnValue variable is declared and a value assigned only when you use the CALL event syntax. It is not declared if you use the new event syntax:

ancestor_name::EVENT event_name ( )

Example

You can put code like the following in an extended event script:

IF AncestorReturnValue = 1 THEN
  // execute some code
ELSE
  // execute some other code
END IF

You can use the same code in a script that overrides its ancestor event script, but you must insert a CALL statement before you use the AncestorReturnValue variable:

// execute code that does some preliminary processing
CALL SUPER::ue_myevent
IF AncestorReturnValue = 1 THEN
…

Types of arguments for functions and events

When you define a function or user event, you specify its arguments, their datatypes, and how they are passed.

There are three ways to pass an argument:

  • By value

    Is the default

    PowerBuilder passes a copy of a by-value argument. Any changes affect the copy, and the original value is unaffected.

  • By reference

    Tells PowerBuilder to pass a pointer to the passed variable

    The function script can change the value of the variable because the argument points back to the original variable. An argument passed by reference must be a variable, not a literal or constant, so that it can be changed.

  • Read-only

    Passes the argument by value without making a copy of the data

    Read-only provides a performance advantage for some datatypes because it does not create a copy of the data, as with by value. Datatypes for which read-only provides a performance advantage are String, Blob, Date, Time, and DateTime.

    For other datatypes, read-only provides documentation for other developers by indicating something about the purpose of the argument.

Matching argument types when overriding functions

If you define a function in a descendant that overrides an ancestor function, the function signatures must match in every way: the function name, return value, argument datatypes, and argument passing methods must be the same.

For example, this function declaration has two long arguments passed by value and one passed by reference:

uf_calc(long a_1, long a_2, ref long a_3) &
   returns integer

If the overriding function does not match, then when you call the function, PowerBuilder calculates which function matches more closely and calls that one, which might give unexpected results.

Ancestor and descendant variables

All objects in PowerBuilder are descendants of PowerBuilder system objects the objects you see listed on the System page in the Browser.

Therefore, whenever you declare an object instance, you are declaring a descendant. You decide how specific you want your declarations to be.

As specific as possible

If you define a user object class named uo_empdata, you can declare a variable whose type is uo_empdata to hold the user object reference:

uo_empdata uo_emp1
uo_emp1 = CREATE uo_empdata

You can refer to the variables and functions that are part of the definition of uo_empdata because the type of uo_emp1 is uo_empdata.

When the application requires flexibility

Suppose the user object you want to create depends on the user's choices. You can declare a user object variable whose type is UserObject or an ancestor class for the user object. Then you can specify the object class you want to instantiate in a string variable and use it with CREATE:

uo_empdata uo_emp1
string ls_objname
ls_objname = ... // Establish the user object to open
uo_emp1 = CREATE USING ls_objname

This more general approach limits your access to the object's variables and functions. The compiler knows only the properties and functions of the ancestor class uo_empdata (or the system class UserObject if that is what you declared). It does not know which object you will actually create and cannot allow references to properties defined on that unknown object.

Abstract ancestor object

In order to address properties and functions of the descendants you plan to instantiate, you can define the ancestor object class to include the properties and functions that you will implement in the descendants. In the ancestor, the functions do not need code other than a return value they exist so that the compiler can recognize the function names. When you declare a variable of the ancestor class, you can reference the functions. During execution, you can instantiate the variable with a descendant, where that descendant implements the functions as appropriate:

uuo_empdata uo_emp1
string ls_objname
// Establish which descendant of uo_empdata to open
ls_objname = ...
uo_emp1 = CREATE USING ls_objname

// Function is declared in the ancestor class
result = uo_emp1.uf_special()

This technique is described in more detail in Dynamic versus static lookup.

Dynamic function calls

Another way to handle functions that are not defined for the declared class is to use dynamic function calls.

When you use the DYNAMIC keyword in a function call, the compiler does not check whether the function call is valid. The checking happens during execution when the variable has been instantiated with the appropriate object:

// Function not declared in the ancestor class
result = uo_emp1.DYNAMIC uf_special()

Performance and errors

You should avoid using the dynamic capabilities of PowerBuilder when your application design does not require them. Runtime evaluation means that work the compiler usually does must be done at runtime, making the application slower when dynamic calls are used often or used within a large loop. Skipping compiler checking also means that errors that might be caught by the compiler are not found until the user is executing the program.

Dynamic object selection for windows and visual user objects

A window or visual user object is opened with a function call instead of the CREATE statement. With the Open and OpenUserObject functions, you can specify the class of the window or object to be opened, making it possible to open a descendant different from the declaration's object type.

This example displays a user object of the type specified in the string s_u_name and stores the reference to the user object in the variable u_to_open. Variable u_to_open is of type DragObject, which is the ancestor of all user objects. It can hold a reference to any user object:

DragObject u_to_open
string s_u_name
s_u_name = sle_user.Text
w_info.OpenUserObject(u_to_open, s_u_name, 100, 200)

For a window, comparable code looks like this. The actual window opened could be the class w_data_entry or any of its descendants:

w_data_entry w_data
string s_window_name
s_window_name = sle_win.Text
Open(w_data, s_window_name)

Optimizing expressions for DataWindow and external objects

No compiler validation for container objects

When you use dot notation to refer to a DataWindow object in a DataWindow control or DataStore, the compiler does not check the validity of the expression:

dw_data.Object.column.property

Everything you specify after the Object property passes the compiler and is checked during execution.

The same applies to external OLE objects. No checking occurs until execution:

ole_1.Object.qualifier.qualifier.property.Value

Establishing partial references

Because of the runtime syntax checking, using many expressions like these can impact performance. To improve efficiency when you refer repeatedly to the same DataWindow component object or external object, you can define a variable of the appropriate type and assign a partial reference to the variable. The script evaluates most of the reference only once and reuses it.

The datatype of a DataWindow component object is DWObject:

DWObject dwo_column
dwo_column = dw_data.Object.column
dwo_column.SlideLeft = ...
dwo_column.SlideUp = ...

The datatype of a partially resolved automation expression is OLEObject:

OLEObject ole_wordbasic
ole_wordbasic = ole_1.Object.application.wordbasic
ole_wordbasic.propertyname1 = value
ole_wordbasic.propertyname2 = value

Handling errors

The Error and (for automation) ExternalException events are triggered when errors occur in evaluating the DataWindow and OLE expressions. If you write a script for these events, you can catch an error before it triggers the SystemError event. These events allow you to ignore an error or substitute an appropriate value. However, you must be careful to avoid setting up conditions that cause another error. You can also use try-catch blocks to handle exceptions as described in Exception handling in PowerBuilder.

For information

For information about DataWindow data expressions and property expressions and DWObject variables, see the section called “About DataWindow data expressions” in DataWindow Reference, the section called “PowerBuilder: DataWindow property expressions” in DataWindow Reference, and the section called “Using the DWObject variable in PowerBuilder” in DataWindow Reference. For information about using OLEObject variables in automation, see Using OLE in an Application.

Exception handling in PowerBuilder

When a runtime error occurs in a PowerBuilder application, unless that error is trapped, a single application event (SystemError) fires to handle the error no matter where in the application the error happened. Although some errors can be handled in the system error event, catching the error closer to its source increases the likelihood of recovery from the error condition.

You can use exception-handling classes and syntax to handle context-sensitive errors in PowerBuilder applications. This means that you can deal with errors close to their source by embedding error-handling code anywhere in your application. Well-designed exception-handling code can give application users a better chance to recover from error conditions and run the application without interruption.

Exception handling allows you to design an application that can recover from exceptional conditions and continue execution. Any exceptions that you do not catch are handled by the runtime system and can result in the termination of the application.

Exception handling can be found in such object-oriented languages as Java and C++. The implementation for PowerBuilder is similar to the implementation of exception handling in Java. In PowerBuilder, the TRY, CATCH, FINALLY, THROW, and THROWS reserved words are used for exception handling. There are also several PowerBuilder objects that descend from the Throwable object.

Basics of exception handling

Exceptions are objects that are thrown in the event of some exceptional (or unexpected) condition or error and are used to describe the condition or error encountered. Standard errors, such as null object references and division by zero, are typically thrown by the runtime system. These types of errors could occur anywhere in an application and you can include catch clauses in any executable script to try to recover from these errors.

User-defined exceptions

There are also exceptional conditions that do not immediately result in runtime errors. These exceptions typically occur during execution of a function or a user-event script. To signal these exceptions, you create user objects that inherit from the PowerScript Exception class. You can associate a user-defined exception with a function or user event in the prototype for the method.

For example, a user-defined exception might be created to indicate that a file cannot be found. You could declare this exception in the prototype for a function that is supposed to open the file. To catch this condition, you must instantiate the user-defined exception object and then throw the exception instance in the method script.

Objects for exception handling support

Several system objects support exception handling within PowerBuilder.

Throwable object type

The object type Throwable is the root datatype for all user-defined exception and system error types. Two other system object types, RuntimeError and Exception, derive from Throwable.

RuntimeError and its descendants

PowerBuilder runtime errors are represented in the RuntimeError object type. For more robust error-handling capabilities, the RuntimeError type has its own system-defined descendants; but the RuntimeError type contains all information required for dealing with PowerBuilder runtime errors.

One of the descendants of RuntimeError is the NullObjectError type that is thrown by the system whenever a null object reference is encountered. This allows you to handle null-object-reference errors explicitly without having to differentiate them from other runtime errors that might occur.

Error types that derive from RuntimeError are typically used by the system to indicate runtime errors. RuntimeErrors can be caught in a try-catch block, but it is not necessary to declare where such an error condition might occur. (PowerBuilder does that for you, since a system error can happen anywhere anytime the application is running.) It is also not a requirement to catch these types of errors.

Exception object type

The system object Exception also derives from Throwable and is typically used as an ancestor object for user-defined exception types. It is the root class for all checked exceptions. Checked exceptions are user-defined exceptions that must be caught in a try-catch block when thrown, or that must be declared in the prototype of a method when thrown outside of a try-catch block.

The PowerScript compiler checks the local syntax where you throw checked exceptions to make sure you either declare or catch these exception types. Descendants of RuntimeError are not checked by the compiler, even if they are user defined or if they are thrown in a script rather than by the runtime system.

Handling exceptions

Whether an exception is thrown by the runtime system or by a THROW statement in an application script, you handle the exception by catching it. This is done by surrounding the set of application logic that throws the exception with code that indicates how the exception is to be dealt with.

TRY-CATCH-FINALLY block

To handle an exception in PowerScript, you must include some set of your application logic inside a try-catch block. A try-catch block begins with a TRY clause and ends with the END TRY statement. It must also contain either a CATCH clause or a FINALLY clause. A try-catch block normally contains a FINALLY clause for error condition cleanup. In between the TRY and FINALLY clauses you can add any number of CATCH clauses.

CATCH clauses are not obligatory, but if you do include them you must follow each CATCH statement with a variable declaration. In addition to following all of the usual rules for local variable declarations inside a script, the variable being defined must derive from the Throwable system type.

You can add a TRY-CATCH-FINALLY, TRY-CATCH, or TRY-FINALLY block using the Script view Paste Special feature for PowerScript statements. If you select the Statement Templates check box on the AutoScript tab of the Design Options dialog box, you can also use the AutoScript feature to insert these block structures.

Example

Example catching a system error

This is an example of a TRY-CATCH-FINALLY block that catches a system error when an arccosine argument, entered by the application user (in a SingleLineEdit) is not in the required range. If you do not catch this error, the application goes to the system error event, and eventually terminates:

Double ld_num
ld_num = Double (sle_1.text)
TRY
   sle_2.text = string (acos (ld_num))
CATCH (runtimeerror er)   
   MessageBox("Runtime Error", er.GetMessage())
FINALLY   
   // Add cleanup code here   
   of_cleanup()   
   Return
END TRY   
MessageBox("After", "We are finished.")

The system runtime error message might be confusing to the end user, so for production purposes, it would be better to catch a user-defined exception -- see the example in Creating user-defined exception types -- and set the message to something more understandable.

The TRY reserved word signals the start of a block of statements to be executed and can include more than one CATCH clause. If the execution of code in the TRY block causes an exception to be thrown, then the exception is handled by the first CATCH clause whose variable can be assigned the value of the exception thrown. The variable declaration after a CATCH statement indicates the type of exception being handled (a system runtime error, in this case).

CATCH order

It is important to order your CATCH clauses in such a way that one clause does not hide another. This would occur if the first CATCH clause catches an exception of type Exception and a subsequent CATCH clause catches a descendant of Exception. Since they are processed in order, any exception thrown that is a descendant of Exception would be handled by the first CATCH clause and never by the second. The PowerScript compiler can detect this condition and signals an error if found.

If an exception is not dealt with in any of the CATCH clauses, it is thrown up the call stack for handling by other exception handlers (nested try-catch blocks) or by the system error event. But before the exception is thrown up the stack, the FINALLY clause is executed.

FINALLY clause

The FINALLY clause is generally used to clean up after execution of a TRY or CATCH clause. The code in the FINALLY clause is guaranteed to execute if any portion of the try-catch block is executed, regardless of how the code in the try-catch block completes.

If no exceptions occur, the TRY clause completes, followed by the execution of the statements contained in the FINALLY clause. Then execution continues on the line following the END TRY statement.

In cases where there are no CATCH clauses but only a FINALLY clause, the code in the FINALLY clause is executed even if a return is encountered or an exception is thrown in the TRY clause.

If an exception occurs within the context of the TRY clause and an applicable CATCH clause exists, the CATCH clause is executed, followed by the FINALLY clause. But even if no CATCH clause is applicable to the exception thrown, the FINALLY clause still executes before the exception is thrown up the call stack.

If an exception or a return is encountered within a CATCH clause, the FINALLY clause is executed before execution is transferred to the new location.

FINALLY clause restriction

Do not use RETURN statements in the FINALLY clause of a TRY-CATCH block. This can prevent the exception from being caught by its invoker.

Creating user-defined exception types

You can create your own user-defined exception types from standard class user objects that inherit from Exception or RuntimeError or that inherit from an existing user object deriving from Exception or RuntimeError.

Inherit from Exception object type

Normally, user-defined exception types should inherit from the Exception type or a descendant, since the RuntimeError type is used to indicate system errors. These user-defined objects are no different from any other nonvisual user object in the system. They can contain events, functions, and instance variables.

This is useful, for example, in cases where a specific condition, such as the failure of a business rule, might cause application logic to fail. If you create a user-defined exception type to describe such a condition and then catch and handle the exception appropriately, you can prevent a runtime error.

Throwing exceptions

Exceptions can be thrown by the runtime engine to indicate an error condition. If you want to signal a potential exception condition manually, you must use the THROW statement.

Typically, the THROW statement is used in conjunction with some user-defined exception type. Here is a simple example of the use of the THROW statement:

Exception    le_ex
le_ex = create Exception
Throw le_ex
MessageBox ("Hmm", "We would never get here if" &   
   + "the exception variable was not instantiated")

In this example, the code throws the instance of the exception le_ex. The variable following the THROW reserved word must point to a valid instance of the exception object that derives from Throwable. If you attempt to throw an uninstantiated Exception variable, a NullObjectError is thrown instead, indicating a null object reference in this routine. That could only complicate the error handling for your application.

Declaring exceptions thrown from functions

If you signal an exception with the THROW statement inside a method script -- and do not surround the statement with a try-catch block that can deal with that type of exception -- you must also declare the exception as an exception type (or as a descendant of an exception type) thrown by that method. However, you do not need to declare that a method can throw runtime errors, since PowerBuilder does that for you.

The prototype window in the Script view of most PowerBuilder painters allows you to declare what user-defined exceptions, if any, can be thrown by a function or a user-defined event. You can drag and drop exception types from the System Tree or a Library painter view to the Throws box in the prototype window, or you can type in a comma-separated list of the exception types that the method can throw.

Example

Example catching a user-defined exception

This code displays a user-defined error when an arccosine argument, entered by the application user, is not in the required range. The try-catch block calls a method, wf_acos, that catches the system error and sets and throws the user-defined error:

TRY   
   wf_acos()

CATCH (uo_exception u_ex)   
   MessageBox("Out of Range", u_ex.GetMessage())
END TRY

This code in the wf_acos method catches the system error and sets and throws the user-defined error:

uo_exception lu_error
Double ld_num
ld_num = Double (sle_1.text)
TRY
   sle_2.text = string (acos (ld_num))
CATCH (runtimeerror er)   
   lu_error = Create uo_exception
   lu_error.SetMessage("Value must be between -1" &
      + "and 1")
   Throw lu_error
END TRY

Adding flexibility and facilitating object reuse

You can use exception handling to add flexibility to your PowerBuilder applications, and to help in the separation of business rules from presentation logic. For example, business rules can be stored in a non-visual object (nvo) that has:

  • An instance variable to hold a reference to the presentation object:

    powerobject my_presenter
  • A function that registers the presentation object

    The registration function could use the following syntax:

    SetObject (string my_purpose, powerobject myobject)
  • Code to call a dynamic function implemented by the presentation object, with minimal assumptions about how the data is displayed

    The dynamic function call should be enclosed in a try-catch block, such as:

    TRY      
             my_presenter.Dynamic nf_displayScreen(" ")
          CATCH (Throwable lth_exception)      
             Throw lth_exception
    END TRY   

    This try-catch block catches all system and user-defined errors from the presentation object and throws them back up the calling chain (to the object that called the nvo). In the above example, the thrown object in the CATCH statement is an object of type Throwable, but you could also instantiate and throw a user exception object:

    uo_exception luo_exception
    
    TRY      
             my_presenter.Dynamic nf_displayScreen(" ")
    CATCH (Throwable lth_exception)      
             luo_exception = Create uo_exception
             luo_exception.SetMessage & +
             (lth_exception.GetMessage())
             Throw luo_exception
    END TRY   

Code for data processing could be added to the presentation object, to the business rules nvo, or to processing objects called by the nvo. The exact design depends on your business objectives, but this code should also be surrounded by try-catch blocks. The actions to take and the error messages to report (in case of code processing failure) should be as specific as possible in the try-catch blocks that surround the processing code.

There are significant advantages to this type of approach, since the business nvo can be reused more easily, and it can be accessed by objects that display the same business data in many different ways. The addition of exception handling makes this approach much more robust, giving the application user a chance to recover from an error condition.

Using the SystemError and Error events

Error event

If a runtime error occurs, an error structure that describes the error is created. If the error occurs in the context of a connection to a remote server then the Error event on the Connection, DataWindow, or OLE control object is triggered, with the information in the error structure as arguments.

The error can be handled in this Error event by use of a special reference argument that allows the error to be ignored. If the error does not occur in the context described above, or if the error in that context is not dealt with, then the error structure information is used to populate the global error variable and the SystemError event on the Application object is triggered.

SystemError event

In the SystemError event, unexpected error conditions can be dealt with in a limited way. In general, it is not a good idea to continue running the application after the SystemError event is triggered. However, error-handling code can and should be added to this event. Typically you could use the SystemError event to save data before the application terminates and to perform last-minute cleanup (such as closing files or database connections).

Precedence of exception handlers and events

If you write code in the Error event, then that code is executed first in the event of a thrown exception.

If the exception is not thrown in any of the described contexts or the object's Error event does not handle the exception or you do not code the Error event, then the exception is handled by any active exception handlers (CATCH clauses) that are applicable to that type of exception. Information from the exception class is copied to the global error variable and the SystemError event on the Application object is fired only if there are no exception handlers to handle the exception.

Error handling for new applications

For new PowerBuilder applications, the recommended approach for handling errors is to use a try-catch block instead of coding the Error event on Connection, DataWindow, or OLE control objects. You should still have a SystemError event coded in your Application object to handle any uncaught exceptions. The SystemError event essentially becomes a global exception handler for a PowerBuilder application.

Garbage collection and memory management

The PowerBuilder garbage collection mechanism checks memory automatically for unreferenced and orphaned objects and removes any it finds, thus taking care of most memory leaks. You can use garbage collection to destroy objects instead of explicitly destroying them using the DESTROY statement. This lets you avoid runtime errors that occur when you destroy an object that was being used by another process or had been passed by reference to a posted event or function.

A reference to an object is any variable whose value is the object. When the variable goes out of scope, or when it is assigned a different value, PowerBuilder removes a reference to the object and counts the remaining references, and the garbage collection process destroys the object if no references remain.

Garbage collection occurs:

  • When the garbage collection interval has been exceeded and the PowerBuilder application becomes idle and

  • When you explicitly call the GarbageCollect function.

When PowerBuilder completes the execution of a system-triggered event, it makes a garbage collection pass if the set interval between garbage collection passes has been exceeded. The default interval is 0.5 seconds. Note that this system-triggered garbage collection pass only occurs when the PowerBuilder application is idle, therefore if a long computation or process is in progress when the interval is exceeded, garbage collection does not occur immediately.

You can force immediate garbage collection by invoking the GarbageCollect function. When you use dot notation and OLEObjects, temporary variables are created. These temporary variables are released only during the garbage collection process. You might want to invoke GarbageCollect inside a loop that appears to be causing memory leaks.

The garbage collection pass removes any objects and classes that cannot be referenced, including those containing circular references (otherwise unreferenced objects that reference each other).

Posting events and functions

When you post an event or function and pass an object reference, PowerBuilder adds an internal reference to the object to prevent its memory from being reclaimed by the garbage collector between the time of the post and the actual execution of the event or function. This reference is removed when the event or function is executed.

Exceptions to garbage collection

There are a few objects that are prevented from being collected:

  • Visual objects

    Any object that is visible on your screen is not collected because when the object is created and displayed on your screen, an internal reference is added to the object. When any visual object is closed, it is explicitly destroyed.

  • Timing objects

    Any Timing object that is currently running is not collected because the Start function for a Timing object adds an internal reference. The Stop function removes the reference.

  • Shared objects

    Registered shared objects are not collected because the SharedObjectRegister function adds an internal reference. SharedObjectUnregister removes the internal reference.

Controlling when garbage collection occurs

Garbage collection occurs automatically in PowerBuilder, but you can use functions to force immediate garbage collection or to change the interval between reference count checks. Three functions let you control when garbage collection occurs: GarbageCollect, GarbageCollectGetTimeLimit, and GarbageCollectSetTimeLimit.

For information about these functions, see PowerScript Reference. For an example illustrating their use, see the Code Examples sample application, described in Using Sample Applications.

Performance concerns

You can use tracing and profiling to examine the effect of changing the garbage collection interval on performance.

For information about tracing and profiling, see the section called “Tracing and Profiling Applications” in Users Guide.

Configuring memory management

You can set the PB_POOL_THRESHOLD environment variable to specify the threshold at which the PowerBuilder memory manager switches to a different memory allocation strategy.

When most windows, DataWindows, DataStores, or other PowerBuilder objects are destroyed or reclaimed by the garbage collector, the PowerBuilder heap manager returns the memory allocated for each object to a global memory pool and records its availability on a global free list. The freed memory is not returned to the operating system. When a new object is created, PowerBuilder allocates blocks of memory from the global memory pool (if sufficient memory is available in the global free list) or from the operating system (if it is not) to a memory pool for the object.

When the memory required by an object exceeds 256KB, PowerBuilder uses a different strategy. It allocates subsequent memory requirements from the operating system in large blocks, and returns the physical memory to the operating system when the object is destroyed. It retains the virtual memory to reduce fragmentation of the virtual address space.

For most applications and components, the threshold of 256KB at which PowerBuilder switches to the "large blocks" strategy works well and reduces the memory required by an application when it is working at its peak level of activity. However, if you want to keep the overall physical memory usage of your application as low as possible, you can try setting a lower threshold.

The advantage of setting a low threshold is that the size of the global memory pool is reduced. The application does not retain a lot of memory when it is inactive. The disadvantage is that large blocks of memory are allocated for objects that require more memory than the threshold value, so that when the application is running at its peak of activity, it might use more virtual memory than it would with the default threshold.

Setting a low threshold can be beneficial for long-running client applications that use many short-lived objects, where the client application's memory usage varies from low (when idle) to high (when active). For multithreaded applications, such as servers, a higher threshold usually results in lower virtual memory utilization.

Logging heap manager output

You can record diagnostic ouput from the PowerBuilder heap manager in a file to help you troubleshoot memory allocation issues in your application. The PB_HEAP_LOGFILENAME environment variable specifies the name and location of the file.

If you specify a file name but not a directory, the file is saved in the same directory as the PowerBuilder executable.

If you specify a directory that does not exist, the file is not created.

By default, the log file is overwritten when you restart PowerBuilder. If you want diagnostic output to be appended to the file, set PB_HEAP_LOGFILE_OVERWRITE to false.

You can set the variables in a batch file that launches the application, or as system or user environment variables on the computer or server on which the application or component runs.

Efficient compiling and performance

The way you write functions and define variables affects your productivity and your application's performance.

Short scripts for faster compiling

If you plan to build machine code dynamic libraries for your deployed application, keep scripts for functions and events short. Longer scripts take longer to compile. Break the scripts up so that instead of one long script, you have a script that makes calls to several other functions. Consider defining functions in user objects so that other objects can call the same functions.

Local variables for faster performance

The scope of variables affects performance. When you have a choice, use local variables, which provide the fastest performance. Global variables have the biggest negative impact on performance.

Reading and writing text or binary files

You use PowerScript text file functions to read and write text in line mode or text mode, or to read and write binary files in stream mode:

  • In line mode, you can read a file a line at a time until either a carriage return or line feed (CR/LF) or the end-of-file (EOF) is encountered. When writing to the file after the specified string is written, PowerScript appends a CR/LF.

  • In stream mode, you can read the entire contents of the file, including any CR/LFs. When writing to the file, you must write out the specified blob (but not append a CR/LF).

  • In text mode, you can read the entire contents of the file, including any CR/LFs. When writing to the file, you must write out the specified string (but not append a CR/LF).

Reading a file into a MultiLineEdit

You can use stream mode to read an entire file into a MultiLineEdit, and then write it out after it has been modified.

Understanding the position pointer

When PowerBuilder opens a file, it assigns the file a unique integer and sets the position pointer for the file to the position you specify the beginning, after the byte-order mark, if any, or end of the file. You use the integer to identify the file when you want to read the file, write to it, or close it. The position pointer defines where the next read or write will begin. PowerBuilder advances the pointer automatically after each read or write.

You can also set the position pointer with the FileSeek or FileSeek64 function.

File functions

These are the built-in PowerScript functions that manipulate files:

Function

Datatype returned

Action

FileClose

Integer

Closes the specified file

FileDelete

Boolean

Deletes the specified file

FileEncoding

Encoding enumerated type

Returns the encoding used in the file

FileExists

Boolean

Determines whether the specified file exists

FileLength

Long

Obtains the length of a file with a file size of 2GB or less

FileLength64

LongLong

Obtains the length of a file of any size

FileOpen

Integer

Opens the specified file

FileRead

Integer

Reads from the specified file (obsolete)

FileReadEx

Long

Reads from the specified file

FileSeek

Long

Seeks to a position in a file with a file size of 2GB or less

FileSeek64

LongLong

Seeks to a position in a file of any size

FileWrite

Integer

Writes to the specified file (obsolete)

FileWriteEx

Long

Writes to the specified file


Encoding

The last argument in the FileOpen function lets you create an ANSI, UTF-8, UTF-16LE (Little Endian), or UTF16-BE (Big Endian) file.

The encoding argument, like all arguments of the FileOpen function except the file name, is optional. You need only specify it if you want to create a new text file with Unicode encoding. If the filename argument refers to a file that does not exist, the FileOpen function creates the file and sets the character encoding specified in the encoding argument.

By default, if the file does not exist and the encoding argument is not specified, PowerBuilder opens a file with ANSI encoding. This ensures compatibility with earlier versions of PowerBuilder.

The FileRead and FileWrite functions cannot read more than 32,766 bytes at a time. The FileReadEx and FileWriteEx functions can write an unlimited number of bytes at a time.