OpenEdge Development: Progress 4GL Handbook


Table of ContentsPreviousNextIndex
Defining Graphical Objects

This chapter returns to the user interface of your application. In "Introducing the OpenEdge AppBuilder," you saw how to lay out fill-in fields and a browse control for data display. Now you’ll learn about other kinds of visual objects and their object definitions, attributes, methods, and events.

This chapter includes the following sections:

Types of objects

The fill-in field is the simplest representation of a single data field. Other ways to represent data fields include:

Among objects that don’t display data values, you have already worked with buttons. In addition, Progress supports rectangles for highlighting and grouping other objects on the screen, and images to display pictures and diagrams. Menus and menu items for a window are other types of visual objects. Windows and frames are objects that serve as containers for other objects within a user interface.

The browse is a major visual object with many capabilities. Because of its special use as a display device for an entire query, a detailed discussion of how to use and customize the browse is postponed until "Using the Browse Object," after you’ve learned more about how to define and use queries.

Various terms describe all these objects in general. The Progress language syntax often uses the term widget. In other places you see the word control. But there is such variety to the display devices Progress supports that this book refers to them all as objects. Sometimes this book refers to them as basic objects or simple objects, to differentiate them from SmartObjects™. SmartObjects are procedure-based and have a great deal of additional standard behavior built into them via the 4GL procedures that support them. Progress Dynamics uses them to construct rich interfaces with very little developer coding. The basic objects Progress supports run the gamut from very simple objects (such as rectangles, which are purely decorational) to complex data controls (such as the browse).

For all their variety, all basic objects have the following in common:

In the next sections you’ll learn about object definitions, attributes, and methods.

Defining static objects

You’ve already seen several uses of the DEFINE statement. You have defined program variables for your procedures with the DEFINE VARIABLE statement. You have also seen the AppBuilder-generated DEFINE BUTTON statements for the four buttons in the Customers and Orders window you built in "Introducing the OpenEdge AppBuilder."

These two forms are representative of two basic ways you can use the DEFINE statement, one to define a variable for a data value that is viewed in a particular way and the other to define an object that doesn’t represent a data value. In this section, you’ll look at the two forms in a general way. Then in the next chapter, you’ll look at specific types of objects you define in each way.

Using the VIEW-AS phrase for data representation objects

Many visual objects represent a single data value. This value can be a field from a database table or it can be a program variable. In both these cases, the default visual representation of the field is normally a fill-in field.

Why specify normally? You can define a visualization for a field when you create it as part of a database table definition in the Data Dictionary.

To see an example of how you define a field’s visualization:
  1. From the AppBuilder menu, select Tools Data Dictionary.
  2. From the Sports2000 database, select the Item table. This table holds information about the different sports-related items that customers can order from your business.
  3. Choose the Fields icon and select the CatDescription field from the Fields list. This field contains the full description of an Item in the catalog.
  4. Choose the Field Properties button.
  5. In the Field Properties dialog box, choose the View-As button.
  6. A dialog box appears where you can define a default visualization for a field if you want it to be an object other than a fill-in. In this case, there is a definition for the field:

    The CatDescription field is defined to be viewed as an editor object, with a size of 41 characters by 5 lines and a vertical scrollbar. Whenever you select the CatDescription field and drop it onto a frame in the AppBuilder, Progress automatically visualizes it as an editor of this description. If you define a variable to be LIKE the CatDescription field, it inherits these visual attributes along with the rest of the field description. You can change these attributes in your programs just as you can change the display for any other field, but the default is always to view it as an editor.
  7. Cancel your way out of the dialog boxes and the Data Dictionary to return to the AppBuilder main window.

For all fields where there is no specific VIEW-AS definition in the Data Dictionary, the default visualization is a fill-in field. If you want another visualization of the field, you use a VIEW-AS phrase as part of the object definition. There are several variations on this.

In a DEFINE VARIABLE statement, you can append the VIEW-AS phrase to the definition:

DEFINE VARIABLE name AS datatype VIEW-AS display-type [options ].

The options are attributes for that visual type that you can choose, such as the SIZE of the editor and the SCROLLBAR-VERTICAL keyword.

If you’re not defining a variable but simply placing a database field or other field into a frame, then you append the VIEW-AS phrase to the name of the field in the DEFINE FRAME statement, along with whatever options apply. Here’s an example from the frame definition the AppBuilder generates if you drop the Customer Comments field onto a window and define it as an editor, as you’ll do later in the next chapter:

DEFINE FRAME CustQuery
     .
     .
     .
Customer.Comments AT ROW 5.29 COL 76 NO-LABEL
VIEW-AS EDITOR SCROLLBAR-VERTICAL
SIZE 36 BY 3.14
     .
     .
     .

Defining objects that don’t represent data values

Objects that don’t represent single data values use a form of the DEFINE statement that names the object type directly. You have seen the DEFINE BUTTON, DEFINE BROWSE, and DEFINE FRAME statements already in the AppBuilder code for CustOrders.w. These are all examples of this form. Each statement type accepts the same kinds of options that the VIEW-AS phrase of the DEFINE VARIABLE statement does, with the options list specialized for each object type. You’ll look at some of these object types in the next chapter.

Using and setting object attributes

Each different type of object has its own attributes, which represent some aspect or capability of the object that you can set and query. Attributes fall into several basic categories:

You can specify initial values for many object attributes when you define the object. The 4GL supports a large number of keywords that represent these various attributes. In some cases, a single keyword represents the attribute, such as SCROLLBAR-VERTICAL for an editor. In other cases, the attribute keyword takes one or more arguments, in the form of other values that follow the keyword in the DEFINE statement, such as ROW 5.

You can also query most attribute values from within the procedure that defines the object. To retrieve the value of an attribute, you use the form:

object-name:attribute-name [IN { FRAME | MENU | SUB-MENU } name ]

Do not use spaces on either side of the colon between the object-name and the attribute-name. Each attribute has an appropriate data type, such as DECIMAL for the ROW attribute or LOGICAL for the HIDDEN attribute. You can use an attribute value anywhere in an expression or assignment where you would use any other value. If the attribute reference is not unambiguous, you can provide context for it in the reference by qualifying it with the name of the frame, menu, or submenu it appears in. The default is the most recently defined container whose description includes the object.

For example, this attribute reference checks whether the First button is hidden:

IF BtnFirst:HIDDEN THEN . . .

Changing attribute values

You can also change many attribute values at run time, even those for static objects. You simply place the attribute reference on the left side of an assignment. The documentation of the individual attributes in the final volume of OpenEdge Development: Progress 4GL Reference, as well as the online help, tells you whether you can set them. Generally, at run time you cannot change attributes that are part of the definition of an object, such as its initial value or its display type. But many attributes can change during program execution, such as attributes that define whether an object is hidden or visible and whether it is enabled or disabled. Even some basic display attributes such as a field’s font or a button’s label can change at run time to give greater flexibility to your application’s interface. If you try to set an attribute for an object that is not settable, you get a compile-time error telling you so.

To get a full description of any attribute from online help, including what object types it applies to and whether it is readable, writable, or both:
  1. Highlight the attribute keyword in any Progress editor window:
  2. Press F1. The online help for the selected entry appears:
  3. When you choose the DISPLAY button, the help text appears:

Common attribute values for visual objects

There are literally hundreds of different attributes, many of which apply only to a single object type. But several basic ones that you will likely use most often apply to most or all visual objects. This section summarizes a few of these.

Geometry attributes

These attributes affect the size and location of the object in the frame or window:

Appearance attributes

These attributes affect the appearance of the object:

Data management attributes

These attributes affect how to manage data associated with the object:

Relationship attributes

These attributes affect how the object interacts with other objects:

Identifying attributes

These attributes identify characteristics of the object:

You can see most of the object attributes that you can set for an object type in the AppBuilder property sheet for the object, such as the one shown in Figure 8–1 for the Customer.Name fill-in field in the CustOrders window.

Figure 8–1: Property sheet for a fill-in field

The Advanced button takes you to the dialog box shown in Figure 8–2. These are attributes that you might use less often, some of which are mentioned in this chapter.

Figure 8–2: Advanced Properties dialog box

Certain property sheet settings, such as Display and Enable, affect AppBuilder-generated executable statements and preprocessor values rather than the DEFINE statement for the object itself. You’ve seen the effect of the Display and Enable settings already in the code for the AppBuilder-generated enable_UI procedure:

IF AVAILABLE Customer THEN
     DISPLAY Customer.CustNum Customer.Name Customer.Address Customer.City
          Customer.State
       WITH FRAME CustQuery IN WINDOW CustWin.
   ENABLE BtnFirst BtnNext BtnPrev BtnLast Customer.CustNum Customer.Name
        Customer.Address Customer.City Customer.State OrderBrowse dTotalPrice
        dTotalExt dAvgDisc cWorstWH cBestWH
       WITH FRAME CustQuery IN WINDOW CustWin.

The AppBuilder also keeps the list of displayed and enabled objects in preprocessor values:

&Scoped-define FIELDS-IN-QUERY-CustQuery Customer.CustNum Customer.Name ~
Customer.Address Customer.City Customer.State
&Scoped-define ENABLED-FIELDS-IN-QUERY-CustQuery Customer.CustNum ~
Customer.Name Customer.Address Customer.City Customer.State

Other property sheet settings for attributes that you cannot set as part of the object definition are set in a series of ASSIGN statements the AppBuilder generates, such as these for the READ-ONLY, PRIVATE-DATA, SELECTABLE, MOVABLE, and RESIZABLE attributes in this example:

ASSIGN
     BtnFirst:PRIVATE-DATA IN FRAME CustQuery     =
               "Private data for the first button.".
ASSIGN
     cBestWH:READ-ONLY IN FRAME CustQuery         = TRUE.
ASSIGN
     cWorstWH:SELECTABLE IN FRAME CustQuery       = TRUE
     cWorstWH:MOVABLE IN FRAME CustQuery          = TRUE
     cWorstWH:READ-ONLY IN FRAME CustQuery        = TRUE
     cWorstWH:RESIZABLE IN FRAME CustQuery        = TRUE.

As you develop your own applications, you need to keep in mind which attributes you can define in a DEFINE statement, which in a frame definition, and which only in executable statements at run time.

Invoking object methods

In addition to attributes, some objects support methods. A method is an operation that performs a specific action related to an object. You can think of methods as being very much like the built-in functions the language supports. The methods are also identified by keywords that you use in 4GL syntax following an object reference, which can be the object name or handle followed by a colon, just as for attributes:

object-reference:method ( optional-arguments ) [ IN FRAME frame-name ]

Methods typically take one or more arguments, defined in a comma-separated list in parentheses following the method name.

For example, in the next chapter you’ll learn how to view a text field as an editor, with multiple lines and scrollbars and so forth. There’s an editor method, called READ-FILE, that you can use to open and read an operating system file into an editor, just as the Progress Procedure Editor does. READ-FILE takes a single argument, the name of the file to read. So this sample syntax reads a file into an editor called cEditor:

cEditor:READ-FILE(‘:myTextFile.txt’)

Methods always return a value, just as built-in functions do. Generally, that value is a LOGICAL indicating whether the operation succeeded or not (with a TRUE value indicating success). You can assign the return value to a variable or field in an assignment statement:

lSuccess = cEditor:READ-FILE(‘:myTextFile.txt’).

The initial letter l indicates that this is a logical variable.

You can also ignore the return value (as you can with any function) and simply treat the method reference as a statement of its own:

cEditor:READ-FILE(‘:myTextFile.txt’).

Some methods return more meaningful values that you would normally not ignore. Indeed, some methods exist solely to return a meaningful value. You can think of these methods as being similar to attributes. However, because an input parameter is required and attribute references cannot take parameters, you use a method instead to retrieve the return value. For example, the following code sample uses a Progress system-wide object called FONT-TABLE to calculate the width of a button label in the current font. It then uses this value to calculate the required width of a frame that has five buttons. Because the button label must be passed in to the operation, the syntax must be defined as a method (in this case called GET-TEXT-WIDTH-CHARS) rather than an attribute (which might have been called TEXT-WIDTH-CHARS):

DEFINE VARIABLE dWidth AS DECIMAL     NO-UNDO.
/* You can specify the LABEL attribute for the button in its definition. */
DEFINE BUTTON bChoose LABEL "Choose me".
/* You must put the button in a frame because otherwise Progress does not compile the method reference. */
DEFINE FRAME aFrame bChoose.
/* Here you use the method return value in a large expression. */
dWidth = 5 * (FONT-TABLE:GET-TEXT-WIDTH-CHARS(bChoose:LABEL) + 1) + 2.

As you can see from this example, you can reference the method within an expression anywhere another value could appear.

Instantiating and realizing objects

There are a couple of important principles that this chapter has been taking for granted in the discussion of objects. In this section, you look at exactly how objects are instantiated (or created) at run time, how they are realized (or viewed), and what their scope is.

Instantiating objects in a container

When you define a frame you give it a name. Progress creates only one instance of a named frame within a procedure, so that frame name identifies a unique object. Progress instantiates the frame when you first use it by executing an I/O-oriented statement involving the frame, such as a DISPLAY or VIEW statement. The same is true for dialog boxes (which are a type of frame), and for menus and their submenus.

Other objects, however, might not be uniquely identified by their names. When Progress encounters, for example, a DEFINE BUTTON statement, or a DEFINE VARIABLE statement with a VIEW-AS phrase that defines a particular type of visual object, it registers the description of the object but it does not actually create it. Progress can create the object only when you associate it with a container frame or window that has itself been instantiated. Then Progress can identify a unique instance of the object in that container, and create it as part of the container.

This means that you could create multiple instances of an object in different containers. For example, this code fragment describes a button and creates two instances of it in two different frames:

DEFINE BUTTON bBothFrames.
ENABLE bBothFrames WITH FRAME F1.
ENABLE bBothFrames WITH FRAME F2.

Qualifying object references to specify a unique identity

The two ENABLE statements for different frames are two distinct instances of a single button definition. Each one is said to have a unique identity, which is manifested in its object handle. Using either the handle or a frame qualifier that identifies which instance of the object you’re referring to, you can set and retrieve attribute values for the object instances independently of one another.

Recall that Progress associates an object reference by default with the most recently defined container for that object. It’s important to keep this in mind when you’re writing your applications. As with all defaults, you are much better off being explicit about object references than taking your chances with how the defaults work in your case.

Here are a few examples to illustrate this point. If you run the code fragment above that defines and enables button bBothFrames, it terminates immediately because there is no statement to make it wait for user input.

To test the effects of how Progress defines a unique identity for an object:
  1. Add a statement to make the procedure and its frame stay active while you observe the behavior of the two buttons:
  2. DEFINE BUTTON bBothFrames.
    ENABLE bBothFrames WITH FRAME F1.
    ENABLE bBothFrames WITH FRAME F2.
    WAIT-FOR CLOSE OF THIS-PROCEDURE.

  3. Run this code to see the two instances of the same button, each in its own frame:
  4. You can’t really see the frames F1 and F2 because Progress has made them just big enough to hold the buttons. You can see that both buttons have the same default label (the button name), and they are both enabled.
  5. Add an attribute reference to disable the button:
  6. DEFINE BUTTON bBothFrames.
    ENABLE bBothFrames WITH FRAME F1.
    ENABLE bBothFrames WITH FRAME F2.
    bBothFrames:SENSITIVE = NO.
    WAIT-FOR CLOSE OF THIS-PROCEDURE.

    The question is, just which instance of the button are you disabling?
  7. Run the procedure again to see which button is disabled:
  8. It’s the second one, because that’s the most recent reference to the button in a frame. To change that behavior (or to make it explicit without relying on the default), you can use the frame qualifier.
  9. Add this frame phrase to the statement to identify the first frame:
  10. DEFINE BUTTON bBothFrames.
    ENABLE bBothFrames WITH FRAME F1.
    ENABLE bBothFrames WITH FRAME F2.
    bBothFrames:SENSITIVE IN FRAME F1 = NO.
    WAIT-FOR CLOSE OF THIS-PROCEDURE.

  11. Run the procedure again to see the result:
  12. Now it is the first instance of the button that is disabled.
To make sure it’s clear which of these frames is which, you can put the frame name into the button. Make these changes to the procedure:

DEFINE BUTTON bBothFrames.
ENABLE bBothFrames WITH FRAME F1.
ENABLE bBothFrames WITH FRAME F2.
bBothFrames:SENSITIVE IN FRAME F1 = NO.
bBothFrames:LABEL IN FRAME F1 =
     "Frame " + bBothFrames:FRAME-NAME IN FRAME F1 .
bBothFrames:LABEL = "Frame " + bBothFrames:FRAME-NAME.
WAIT-FOR CLOSE OF THIS-PROCEDURE.

Take a look at the new code you added. The first new statement identifies the button instance in frame F1 explicitly: bBothFrames:LABEL IN FRAME F1. It sets the button label attribute to the text Frame plus the value of the FRAME-NAME attribute for the button. The second reference to the button in that statement likewise has to be qualified by the IN FRAME phrase to get the right one: bBothFrames:FRAME-NAME IN FRAME F1.

By contrast, the second new statement just takes the defaults: bBothFrames:LABEL = "Frame " + bBothFrames:FRAME-NAME. Because frame F2 is the most recently referenced frame for the button, the defaults use that frame. Figure 8–3 shows the result when you rerun the procedure.

Figure 8–3: Frames for buttons

In Figure 8–3 you can see the frames Progress created for the buttons. At first they were the same size as the buttons. Your new statements changed the label of each button, and Progress automatically reduced the button size accordingly. The remaining portion of the frame appears as a white space after each button.

The lesson of this little exercise is that you must always be aware that every use of an object in a different container is a distinct instance of the object, and you should always make your object references as explicit as needed to be sure that you do not simply get a default behavior that isn’t what you want.

Realizing and derealizing objects

When you define an object, Progress creates an internal data structure associated with that object. Before the object can be displayed, the window system must also create a data structure for the object. When this second data structure has been created, then the object is realized.

You can modify some object attributes at any time. Others are initially modifiable, but become fixed once the object is realized. In addition, some attributes must be set before realization takes place. For this reason, it is important to have an understanding of when objects are realized.

In general, an object is realized when the application needs to make it visible on the screen. Therefore, field-level objects, buttons, and the like are typically realized when their containers are realized or made visible, and conversely, a container object is realized when a statement forces any of its contained objects to be realized or made visible.

In addition, an object is realized if a statement references any method of the object because methods operate on the realized instance object. Also, some attribute values, such as those that reference an object’s size, can only be determined if the object has been realized, so a reference to any of those attributes forces the object to be realized if it is not already. Examples of this are the MODIFIED attribute of an editor, which is true if the text in the editor has been changed since it was initialized, and the MAX-HEIGHT or MAX-WIDTH attributes of a frame which cannot be calculated without realizing the frame.

A static object is derealized, or destroyed, when it goes out of scope. Generally, the scope of a defined object is the procedure in which it’s defined. Field-level objects are derealized when their frame is derealized. In turn, a frame is derealized when its containing window is deleted.

Using object events

Graphical applications are often referred to as event-driven applications. Unlike the hierarchical, menu-driven applications typical in a character terminal environment, graphical applications put the user more in control of the sequence of events. Using the mouse, menus, and active controls (like buttons on the screen that can respond to user actions), the user can navigate through the application with much more flexibility than in most older applications.

But this flexibility does not happen automatically. You, the developer, must build it into the application by programming procedures or blocks of code that respond to user actions. These are called triggers. The construction of the user interface of an application around blocks of trigger code is the single most fundamental difference in the architecture of event-driven applications.

You’ve already seen some examples of the language constructs the Progress 4GL uses to establish triggers and respond to events when you looked through and extended the AppBuilder-generated code for the CustOrders window in "Using Basic 4GL Constructs." In this section, you’ll look at those constructs in a little more detail.

User interface events

Each visual object type supports a set of user interface events. You can see a list of all these events in the AppBuilder if you go into the triggers section of the Section Editor for an object and then choose the New button. Each object type first supports a list of common events, such as the events for a button, shown in Figure 8–4.

Figure 8–4: Common button events

They are called common events simply because they are the events most commonly associated with an object. Each object has its own set of these events but there is a lot of overlap. For example, any object that can be part of the tab order of a frame has the ENTRY event, which fires when the user tabs into the object, and the LEAVE event, which fires when the user tabs out of that object. Any object that can be the target of keystrokes (even an object like a button that doesn’t use those keystrokes to set a data value) can use the ANY-KEY or ANY-PRINTABLE events to respond to them.

In some cases, the most common event is one that is distinctive for that object. For example, the whole purpose of a button is for someone to choose it and to have an action result. Therefore, the button supports the CHOOSE event, which is supported only by buttons and menu items. This is the default event that comes up in the Section Editor when you go into the triggers section for it. Data-representation objects, which can have actual values, support the VALUE-CHANGED event, which fires when the user enters a new value for the object.

To see a complete description of all the common events, see the high-level widget events topic in the online Help.

Objects support direct manipulation events, such as CHOOSE shown in Figure 8–5, which fire when the user performs an action (typically using the mouse) that involves selecting an object and possibly moving or resizing it. Some of these events are the result of making the object SELECTABLE, RESIZABLE, or MOVABLE.

Figure 8–5: Direct manipulation events

There is a whole host of events associated with all of the possible ways the user can make a selection with either a standard two-button mouse or a three-button mouse, such as the portable mouse events shown in Figure 8–6, that can map to either type of mouse.

Figure 8–6: Portable mouse events

Progress also supports a set of ten miscellaneous events that have no standard meaning or action, but which are intended to let you associate a trigger with some event that only happens programmatically using the APPLY statement, and which has nothing to do with a particular user action. These are called the developer events and are numbered U1 through U10, as shown in Figure 8–7.

Figure 8–7: Developer events

These developer events have no built-in significance, but allow you to define a block of code to execute. For example, you can specify ON U1 OF an object and then programmatically APPLY “U1” to the object to execute the code.

Finally, you can associate a trigger with virtually any keyboard keystroke combination, by choosing the Keyboard Event button and then typing the keystroke combination, such as CTRL-X shown in Figure 8–8.

Figure 8–8: Keyboard Event dialog box

Defining triggers

The most basic way to define a trigger is to put the trigger definition directly into the object definition:

DEFINE BUTTON BtnQuit LABEL "Quit"
     TRIGGERS:
       ON CHOOSE
         QUIT.
     END TRIGGERS.

The TRIGGERS block in a DEFINE statement can contain one or more individual trigger definitions, each starting with an ON phrase naming the event followed by a single statement or a DO-END block of statements. This form is called a definitional trigger.

If you use the AppBuilder to create your application procedures, it always creates separate blocks of executable code that attach triggers to objects at run time. This form is called a run-time trigger. There is no inherent advantage to one form over the other. Either way, the code in the trigger block is compiled and turned into r-code along with the rest of the procedure. You should use the AppBuilder to organize your trigger blocks to provide a more readable structure to your procedures.

The form of the trigger block for a run-time trigger names both the event and the object it applies to:

ON event-name [ , . . .] OF object-name [ , . . .] statements

Either the event-name or the object-name (or both) can be a comma-separated list. The statements can be either a single statement or a DO-END block of statements.

Setting up triggers to be executed

A trigger is executed if the object it applies to is enabled (sensitive), and if the trigger is currently active and in scope.

The trigger is active if the statement that defines it has been executed. As discussed earlier in the book, Progress processes statements in a procedure in the order it encounters them in. Thus, the definition of an object must come before the block of code that defines a trigger for the object. Otherwise, Progress won’t recognize the object reference. And the trigger definition must come before the user receives control and can perform the action that would fire the event and run the trigger.

The scope of a definitional trigger is the scope of the object it’s defined for. The scope of a run-time trigger is the nearest containing procedure or trigger block where it is defined. So if a trigger definition is inside an internal procedure, then its scope is limited to that internal procedure. If it’s outside of any internal procedure, its scope is the entire external procedure.

The AppBuilder places all trigger definitions toward the top of the procedure, following the Definitions section of the procedure but before the main block and all internal procedures. In this way the trigger blocks are scoped to the entire procedure and they are made active before any user actions that invoke them can occur.

To build a very simple example procedure to demonstrate some of the rules of run-time trigger processing in Progress:
  1. Define a button and give it an initial label, then define an INTEGER variable as a counter:
  2. DEFINE BUTTON bButton LABEL "Initial Label".
    DEFINE VARIABLE iCount AS INTEGER     NO-UNDO.

  3. Add a statement to enable the button in a frame. As you learned earlier, this causes both the button and its frame to be instantiated and realized:
  4. ENABLE bButton WITH FRAME fFrame.

  5. Define a run-time trigger for the button that changes its label so that you can see that the trigger fired:
  6. ON CHOOSE OF bButton IN FRAME fFrame
    DO:
         iCount = iCount + 1.
          bButton:LABEL IN FRAME fFrame = "External " + STRING(iCount).
    END.

  7. Create a WAIT-FOR statement that blocks the termination of the procedure and allows the user to choose the button:
  8. WAIT-FOR CLOSE OF THIS-PROCEDURE.

  9. Run the procedure. As you would expect, the button’s label is changed when you choose the button:
  10. The ON CHOOSE trigger you defined is scoped to the entire procedure and executes each time the button is pressed.
To define another trigger for the button in an internal procedure, make these changes to the procedure:

DEFINE BUTTON bButton LABEL "Initial Label".
DEFINE VARIABLE iCount AS INTEGER     NO-UNDO.
ENABLE bButton WITH FRAME fFrame.
ON CHOOSE OF bButton IN FRAME fFrame
DO:
     iCount = iCount + 1.
     bButton:LABEL IN FRAME fFrame = "External " + STRING(iCount).
END.
RUN ChooseProc.
WAIT-FOR CLOSE OF THIS-PROCEDURE.
PROCEDURE ChooseProc.
     ON CHOOSE OF bButton IN FRAME fFrame
     DO:
          iCount = iCount + 1.
          bButton:LABEL In FRAME fFrame = "Internal " + STRING(iCount).
     END.
END.

Before the WAIT-FOR statement the code runs the internal procedure ChooseProc. This procedure defines a different trigger for the same button, which identifies the trigger with the label Internal. The second trigger definition replaces the first one within the ChooseProc procedure.

Note that the scope of the button is the entire external procedure because it is defined at that level. The scope of the variable iCount is also the external procedure for the same reason. But what is the scope of the second trigger definition you just added?

It is scoped only to the internal procedure where it is defined. So what happens when you run this version of the procedure? Before you run it, walk through the code in your head to decide what happens.

The external procedure defines a trigger for the button. It then runs an internal procedure that defines a different trigger. That internal procedure then exits, without giving the user any chance to use the new trigger, and its trigger goes out of scope. So what happens when the user chooses the button?

Figure 8–9: Result of trigger example procedure

Figure 8–9 shows that Progress reverts to the original trigger. This example shows you that Progress effectively keeps a stack of trigger definitions. If a later definition goes out of scope, Progress reverts to the trigger definition that is now back in scope (if there is one).

So how would you see the effect of the internal procedure trigger? You can place a WAIT-FOR statement inside the internal procedure to force Progress to wait until the user chooses the button.

To try this, add this statement to the end of the ChooseProc procedure:

WAIT-FOR CHOOSE OF bButton.

Note that you can wait for any event, not just the close of the procedure or its window. This statement waits until the user chooses the button exactly once. Then the WAIT-FOR is satisfied, the internal procedure exits, and the first trigger takes over.

To see the result of each button press, run the procedure again. Figure 8–10 shows the result of the first four button presses.

Figure 8–10: Results of running the ChooseProc procedure
Making a trigger persistent

Adding the second WAIT-FOR statement is very awkward, and it is pretty close to an absolute rule in Progress that your entire application, not just a single procedure, should have only a single WAIT-FOR statement. This rule is summarized in the saying: “One world, one WAIT-FOR.” Multiple WAIT-FOR statements can easily get tangled up in each other if they are not satisfied in the exact reverse order from the order they are defined in, and this can result in a WAIT-FOR that doesn’t terminate properly.

So how else do you establish a trigger inside an internal procedure or a trigger block, or for that matter inside another external procedure that you call, and have that trigger persist beyond the scope of its declaration?

Progress provides a special statement to let you do this:

PERSISTENT RUN procedure-name [ (input-parameters ) ].

The persistent trigger definition can have only this one RUN statement (not a DO-END block with any other statements). You can pass optional INPUT parameters to the procedure you run but no OUTPUT or INPUT-OUTPUT parameters. If you pass parameters, the parameter values are evaluated once when the trigger is defined. They are not re-evaluated each time the trigger executes.

To see how you write a persistent trigger and what its effects are, change the ChooseProc procedure and add the new procedure PersistProc, as follows:

PROCEDURE ChooseProc.
    ON CHOOSE OF bButton IN FRAME fFrame
        PERSISTENT RUN PersistProc.
END PROCEDURE.
PROCEDURE PersistProc:
        iCount = iCount + 1.
        bButton:LABEL IN FRAME fFrame = "Internal " + STRING(iCount).
END PROCEDURE.

Now when you run the procedure and choose the button you get a very different result.
Figure 8–11 shows the results of the first three button presses.

Figure 8–11: Results of running the PersistProc procedure

Once you have established the persistent trigger, it remains in effect as long as the object it’s associated with exists, just as a definitional trigger would.

Using the REVERT statement to cancel a trigger

An ON statement can contain the single keyword REVERT to cancel an existing trigger definition before it goes out of scope:

ON events OF objects REVERT.

The REVERT statement cancels any run-time trigger for the event and object defined in the current procedure or trigger. Note that you cannot use REVERT to cancel a persistent trigger. Instead, you must run another persistent trigger procedure that either defines a new trigger or consists of just a RETURN statement.

Defining triggers to fire anywhere

You can use the ANYWHERE option of the ON statement to set up a trigger that applies to all objects in an application. Use the following syntax:

ON events ANYWHERE statement or code block.

Applying events in your application

You have already seen cases where a procedure causes an event to fire “artificially” by using the APPLY statement. In the CustOrders procedure, each button trigger has to APPLY the VALUE-CHANGED event to the Order browse to get it to run the internal procedure to display related data for the Order, such as this code for the Next button trigger:

DO:
  GET NEXT CustQuery.
  IF AVAILABLE Customer THEN
  DO:
    DISPLAY {&FIELDS-IN-QUERY-CustQuery}
        WITH FRAME CustQuery IN WINDOW CustWin.
    {&OPEN-BROWSERS-IN-QUERY-CustQuery}
    APPLY "VALUE-CHANGED" TO OrderBrowse.
  END.
END.

The APPLY statement causes whatever trigger is currently active for the event and object to fire. Unlike the ON statement, you must place the name of the event in quotation marks in an APPLY statement, if it is a literal value. This makes it possible to APPLY the value of a character variable instead. In this way, you can define a CHARACTER variable, assign it a value of an event name during program execution, and then use that value in an APPLY statement, adding flexibility to your programming. For example:

DEFINE VARIABLE cEvent AS CHARACTER NO-UNDO.
     .
     .
     .
cEvent = “VALUE-CHANGED”.
     .
     .
     .
APPLY cEvent TO <some object>.

By contrast, Progress must know the event for an ON statement at compile time to prepare the trigger properly. For this reason the event name can’t be a variable, so you can specify it with or without quotation marks in the ON statement.

As another example, here’s a little procedure that transfers keystrokes from one fill-in field to another:

DEFINE VARIABLE cFillFrom AS CHARACTER NO-UNDO.
DEFINE VARIABLE cFillTo AS CHARACTER NO-UNDO.
ENABLE cFillFrom cFillto WITH FRAME fFrame.
ON ANY-PRINTABLE OF cFillFrom
     APPLY LAST-KEY TO cFillto.
WAIT-FOR CLOSE OF THIS-PROCEDURE.

The LAST-KEY keyword is a built-in function that returns the value of the last keystroke the user pressed. Every time you enter a letter into the first fill-in field, cFillFrom, the ANY-PRINTABLE trigger fires which, as its name suggests, responds to any printable character typed on the keyboard, and this trigger retrieves that keystroke and applies it to the other fill-in field, cFillTo. So you can apply not just the standard events but even keyboard characters to an object like a fill-in field, if is enabled for input. Figure 8–12 shows the effect of typing Text into cFillFrom.

Figure 8–12: Result of typing into cFillFrom
Using NO-APPLY to suppress default processing for an event

When you type a letter such as A in a field, it naturally appears in the field on the screen. This is called the default processing for the event. If there were a trigger ON “A” OF cFillTo, then you could APPLY that event to the fill-in field, the trigger would fire, and the letter would appear. This is the normal result of applying an event: both the default processing and any trigger on the event occur. However, some event-object pairs do not get Progress default processing using the APPLY statement. For example, applying the CHOOSE event programmatically to a button executes the trigger on that button but does not give focus to the button in the way that choosing it would.

As another twist to this relationship between events and their actions, consider the action on the object that initiates the event, not the one that receives it and does its default processing for the event. Sometimes you want only the trigger action on the target object to occur and not the default processing for the object that initiated the event. In this case, you can use the special RETURN NO-APPLY statement at the end of the trigger definition to suppress the default processing on the object that initiated it.

To suppress the default processing for an event in your test procedure:
  1. Add the RETURN NO-APPLY statement (along with the DO-END statements to turn the trigger into a block of code):
  2. DEFINE VARIABLE cFillFrom AS CHARACTER NO-UNDO.
    DEFINE VARIABLE cFillTo AS CHARACTER NO-UNDO.
    ENABLE cFillFrom cFillto WITH FRAME fFrame.
    ON ANY-PRINTABLE OF cFillFrom
    DO:
        APPLY LAST-KEY TO cFillto.
        RETURN NO-APPLY.
    END.
    WAIT-FOR CLOSE OF THIS-PROCEDURE.

  3. Run the procedure and type some text into cFillFrom:
  4. When you type, the keystrokes are applied to cFillTo, that is its default processing. But the RETURN NO-APPLY statement suppresses the default processing the cFillFrom, so it remains blank.
  5. Add this statement to the trigger:
  6. ON ANY-PRINTABLE OF cFillFrom
    DO:
        APPLY LAST-KEY TO cFillto.
        APPLY '*' TO cFillFrom.
        RETURN NO-APPLY.
    END.

  7. Run the procedure.You get asterisks in cFillFrom for each keystroke you type:

The trigger first applies the keystroke to the second fill-in, which causes the character to be displayed. It then applies the literal ‘:*’ to the first fill-in, causing it to be displayed there. Finally, it does a RETURN NO-APPLY to suppress the display of the character you actually typed into the first fill-in. You could use this sort of code for a password field, for example.

Applying a nonstandard event

With the APPLY statement, you can actually send any event to any object that has a trigger for that event. Progress executes the trigger even if there is no default handling for the event associated with that object type. Thus, you can use this feature to extend the repertoire of developer events (predefined events with the names U1 through U10) to include any event not normally associated with an object. For example, you could apply CHOOSE to a fill-in field, which does not normally support that event.


Copyright © 2005 Progress Software Corporation
www.progress.com
Voice: (781) 280-4000
Fax: (781) 280-4095
Table of ContentsPreviousNextIndex