OpenEdge Development: Progress 4GL Handbook


Table of ContentsPreviousNextIndex
Defining Functions and
Building Super Procedures

This chapter covers some other important topics concerning how you construct procedures. Specifically, it describes how to define functions of your own within your 4GL procedures to complement the built-in Progress functions you’ve been introduced to. It also details how to build super procedures, which provide standard libraries of application behavior. Finally, it describes the PUBLISH and SUBSCRIBE keywords that help procedures communicate with each other.

This chapter includes the following sections:

User-defined functions

As you have already seen in the previous chapter’s discussion of INTERNAL-ENTRIES and the GET-SIGNATURE method, there is an alternative to the internal procedure as a named entry point within a procedure file. This is the user-defined function, sometimes referred to as a UDF or simply as a function. You are probably familiar with functions from other programming languages. Fundamentally, a function differs from a procedure in that it returns a value of a specific data type, and therefore can act in place of any other kind of variable or expression that would evaluate to the same data type. This means that you can place functions inside other expressions within a statement or within the WHERE clause of a result set definition. By contrast, you must invoke a procedure, whether external or internal, with a RUN statement. If it needs to return information, you have to use OUTPUT parameters that your code examines after the procedure returns. You should therefore consider defining a function within your application for a named entry point that needs to return a single value and that is convenient to use within larger expressions.

You can also compare user-defined functions with the many built-in functions that are a part of the Progress 4GL, such as NUM-ENTRIES, ENTRY, and many others that you’ve seen and used. Your functions can provide the same kinds of widely useful behavior as the built-in functions, but specific to your application.

If you use a function in a WHERE clause or other expression in the header of a FOR EACH block, Progress evaluates the function once, at the time the query with the WHERE clause is opened or the FOR EACH block is entered.

You can perhaps best think of a function as a small piece of code that performs a frequently needed calculation, or that checks a rule or does some common data transformation. In general, this is the best use for functions. However, a function can be a more complex body of code if this is appropriate.

Defining a function

This is the syntax you use to define the header for a function:

FUNCTION function-name [ RETURNS ] datatype [ ( parameters ) ] :

A function always must explicitly return a value of the data type you name. It can take one or more parameters just as a procedure can. Although this means that a function can return OUTPUT parameters or use INPUT-OUTPUT parameters just as a procedure can, you should consider that a function normally takes one or more INPUT parameters, processes their values, and returns its RETURN value as a result. If you find yourself defining a function that has OUTPUT parameters, you should reconsider and probably make it a procedure instead. One of the major features and benefits of a function is that you can embed it in a larger expression and treat its return value just as you would a variable of the same type. If there are OUTPUT parameters, this won’t be the case as you must check their values in separate statements.

The body of the function can contain the same kinds of statements as an internal procedure. Just as with internal procedures, you cannot define a temp-table or any shared object within the function. It has two important additional restrictions as well:

A function must contain at least one RETURN statement to return a value of the appropriate data type:

RETURN return-value.

The return-value can be a constant, variable, field, or expression (including possibly even another function reference) that evaluates to the data type the function was defined to return.

A function must end with an END statement. Just as an internal procedure normally ends with an END PROCEDURE statement, it is normal to end a function with an explicit END FUNCTION statement to keep the organization of your procedure file clearer. The FUNCTION keyword is optional in this case but definitely good programming practice.

Although a function must always have a return type and return a value, a reference to the function is free to ignore the return value. That is, a 4GL statement can simply consist of a function reference without a return value, much like a RUN statement without the RUN keyword. This might be appropriate if the function returns a true/false value indicating success or failure, and in a particular piece of code, you are not concerned with whether it succeeded or not.

Whether a function takes any parameters or not, you must include parentheses, even if they’re empty, on every function reference in your executable code.

Making a forward declaration for a function

In "Introducing the OpenEdge AppBuilder," you looked at the overall organization of a procedure file and used the AppBuilder-generated form as a good basis for your own procedure files. In fact, you should always use the AppBuilder to create your procedures if they have any internal entries at all, even if they have no visual content. The AppBuilder organizes the physical file for you and generates a lot of the supporting code you need.

If you follow the structural guidelines for a procedure file, you place all your internal entries—internal procedures and user-defined functions—at the end of the file. Normally, you need to reference the functions in your procedure file within the body of the code that comes before their actual implementation, either in the main block or in other internal entries in the file.

The Progress compiler needs to understand how to treat your function when it encounters it in the executable code. In particular, it needs to know the data type to return. It also does you a service by enforcing the rest of the function’s signature—its parameter list—whenever it finds a reference to it. For this reason, Progress needs to know at least the definition of the function—its return type and parameters—before it encounters a use of the function elsewhere in the procedure. To do this and still leave the actual implementation of the function toward the end of the file, you can provide a forward declaration of the function, also called a prototype, toward the top of the procedure file, before any code that uses the function.

This is the syntax for a forward declaration of a function:

FUNCTION function-name [ RETURNS ] datatype [ ( parameters )
  { FORWARD | [ MAP [ TO ] actual-name ] IN proc-handle | IN SUPER }

As you can see, the basic definition of the function name, return type, and parameters is the same as you would use in the function header itself. If you provide a forward declaration for a function, the parameter list is optional in the function header (though the RETURNS phrase is not). It’s good practice, though, to provide it in both places.

A function prototype can point to an actual function implementation in one of three kinds of places:

Making a local forward declaration

If you use the FORWARD keyword in the function prototype, then this tells Progress to expect the actual function implementation later in the same procedure file. Here’s a simple example of a function that converts Celsius temperatures to Fahrenheit:

/* h-ConvTemp1.p -- procedure to convert temperatures
   and demonstrate functions. */
FUNCTION CtoF RETURNS DECIMAL (INPUT dCelsius AS DECIMAL) FORWARD.
DEFINE VARIABLE dTemp AS DECIMAL NO-UNDO.
REPEAT dTemp = 0 TO 100:
    DISPLAY dTemp LABEL "Celsius"
            CtoF(dTemp) LABEL "Fahrenheit"
            WITH FRAME f 10 DOWN.
END.
FUNCTION CtoF RETURNS DECIMAL (INPUT dCelsius AS DECIMAL):
    RETURN (dCelsius * 1.8) + 32.
END FUNCTION.

This procedure executes as follows:

  1. The procedure makes a forward declaration of the CtoF conversion function, so that it can be used in the procedure before its implementation code is defined.
  2. The function is used inside the REPEAT loop in the DISPLAY statement. Notice that it appears where any DECIMAL expression could appear and is treated the same way.
  3. There is the actual implementation of the function, which takes the Celsius temperature as input and returns the Fahrenheit equivalent.

Figure 15–1 shows the first page of output from the h-ConvTemp1.p procedure.

Figure 15–1: Result of the ConvTemp.p procedure

You could leave the parameter list out of the function implementation itself, but it’s good form to leave it in.

Making a declaration of a function in another procedure

Because functions are so generally useful, you might want to execute a function from many procedures when it is in fact implemented in a single procedure file that your application runs persistent. In this case, you can provide a prototype that specifies the handle variable where the procedure handle of the other procedure is to be found at run time. This is the second option in the prototype syntax:

[ MAP [ TO ] actual-name ] IN proc-handle

The proc-handle is the name of the handle variable that holds the procedure handle where the function is actually implemented. If the function has a different name in that procedure than in the local procedure, you can provide the MAP TO actual-name phrase to describe this. In that case, actual-name is the function name in the procedure whose handle is proc-handle.

To see an example of the CtoF function separated out in this way:
  1. Create a procedure with just the function definition in it:
  2. /* h-FuncProc.p -- contains CtoF and possible other useful functions. */
    FUNCTION CtoF RETURNS DECIMAL (INPUT dCelsius AS DECIMAL):
         RETURN (dCelsius * 1.8) + 32.
    END FUNCTION.

    It could also have other internal entries to be used by others that have its procedure handle at run time.
  3. Change the procedure that uses the function to declare it IN hFuncProc, its procedure handle. The code has to run h-FuncProc.p persistent or else access its handle if it’s already running. In this case, it also deletes it when it’s done:
  4. /* h-ConvTemp2.p -- procedure to convert temperatures
       and demonstrate functions. */
    DEFINE VARIABLE hFuncProc AS HANDLE     NO-UNDO.
    FUNCTION CtoF RETURNS DECIMAL (INPUT dCelsius AS DECIMAL) IN hFuncProc.
    DEFINE VARIABLE dTemp AS DECIMAL      NO-UNDO.
    RUN h-FuncProc.p PERSISTENT SET hFuncProc.
    REPEAT dTemp = 0 TO 100:
        DISPLAY dTemp LABEL "Celsius"
                CtoF(dTemp) LABEL "Fahrenheit"
                WITH FRAME f 10 DOWN.
    END.
    DELETE PROCEDURE hFuncProc.

  5. Run this variant h-ConvTemp2.p, to get the same result as before. This time here’s the end of the display, just to confirm that you got the arithmetic right:

An externally defined function such as this one can also reside in an entirely different OpenEdge session, connected to the procedure that uses the function using the OpenEdge AppServer. In this case, you declare the function in the same way but use the AppServer-specific ON SERVER phrase on the RUN statement to invoke the function. As with any AppServer call, you can’t pass a buffer as a parameter to such a function. See OpenEdge Application Server: Developing AppServer Applications for more information.

Making a declaration of a function IN SUPER

The third option in the function prototype is to declare that it is found IN SUPER at run time. This means that the function is implemented in a super procedure of the procedure with the declaration. You learn about super procedures in the "Using super procedures in your application" .

Using the AppBuilder to generate function definitions

If you want to build a procedure file of any size with a number of internal entries, whether internal procedures or functions, you should definitely use the AppBuilder to create it for you. The AppBuilder generates most of the supporting statements you’ve just read about here for user-defined functions, and provides a separate code section for each to make it easy to maintain your procedures.

To see how to define a function in the AppBuilder and what it does for you:
  1. Go into the Section Editor for the new h-OrderWin.w procedure.
  2. Select Functions from the Section drop-down list. This message appears:
  3. Answer Yes. The New Function dialog box appears:
  4. Enter the function name dataColor and specify INTEGER for the Returns value, then choose OK.
  5. Enter this definition for the function:
  6. RETURNS INTEGER
      ( daFirst AS DATE, daSecond AS DATE ) :
    /*---------------------------------------------------------------------
      Purpose: Provides a standard warning color if one date
                is too far from another.
        Notes: The function returns a color code of:
                -- yellow if the dates differ at all
                -- purple if they are more than five days apart
                -- red if they are more than ten days apart
    ---------------------------------------------------------------------*/
      DEFINE VARIABLE iDifference AS INTEGER NO-UNDO.
      iDifference = daSecond - daFirst.
      RETURN IF iDifference = ? OR iDifference > 10 THEN 12 /* Red */
             ELSE IF iDifference > 5 THEN 13 /* Purple */
             ELSE IF iDifference > 0 THEN 14 /* Yellow */
             ELSE ?. /* Unknown value keeps the default background color. */
    END FUNCTION.

    The AppBuilder has generated the FUNCTION dataColor header syntax along with the RETURNS phrase, a placeholder RETURN statement and the END FUNCTION statement. You fill in the parameter definitions where the comment prompts you to do that. The function accepts two dates as INPUT parameters, calculates the difference between them (which is an integer value), and returns an integer value representing the appropriate background color for the second date. This is color code 12, 13, or 14, representing colors red, purple, and yellow, depending on how far apart the two dates are and whether either of them is undefined.
  7. Go into the main block and add a line of code to invoke the function and assign the BGCOLOR attribute for background color to the date field when the row is displayed:
  8. MAIN-BLOCK:
    DO ON ERROR   UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK
       ON END-KEY UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK:
      RUN enable_UI.
      shipDate:BGCOLOR = dateColor(PromiseDate, ShipDate).
      IF NOT THIS-PROCEDURE:PERSISTENT THEN
        WAIT-FOR CLOSE OF THIS-PROCEDURE.
    END.

  9. Save h-OrderWin.w, then Run h-CustOrderWin6.w.
  10. There are only a few Orders where the ShipDate is later than the PromiseDate.
  11. To find a few, enter a New State of GA for Georgia.
  12. Choose the Next button to get to Customer 3.
  13. Select Order number 160 in the browse and choose the Order Detail button to run h-OrderWin.w. You see the field is displayed in red, as it should be:
  14. Select Order number 189, which has no ShipDate at all, and choose Order Detail:
  15. The ShipDate shows up in red here also because it has the unknown value.
    Think about the code you just wrote. You defined an implementation for a function called dateColor, which goes to the bottom of the h-OrderWin.w procedure file, along with any internal procedures. Then you added a line of code to the main block that references the function.
    The main block is above the function in the procedure, so why didn’t Progress complain that there was no declaration for the function? The reason is that the AppBuilder generated it for you.
  16. Go into the Compile Code Preview window for h-OrderWin.w and search for dateColor (use the CTRL-F sequence for Find).

You’ll see the function prototype in one of the header sections that comes before the main block or any other executable code:

/* ************************ Function Prototypes ********************** */
FUNCTION dateColor RETURNS INTEGER
   ( daFirst AS DATE, daSecond AS DATE )   FORWARD.

Thus, the AppBuilder not only helps you generate the header for the function code itself, but it also generates a prototype for you and organizes all the code into sections where you can easily inspect and maintain individual entries.

The AppBuilder copies the parameter definitions from your function header just as you entered it in the Section Editor so you cannot remove them from the function header, even though in a hand-coded procedure file you could define them in the prototype and leave them out of the function header itself.

Making run-time references with DYNAMIC-FUNCTION

Progress lets you construct a reference to a function at run time, using a built-in function called DYNAMIC-FUNCTION. Here’s the syntax:

DYNAMIC-FUNCTION ( function [ IN handle ] [ , parameters ] )

Like a function reference itself, the DYNAMIC-FUNCTION function can appear anywhere in your procedure code where an expression of that data type could appear.

The first parameter to DYNAMIC-FUNCTION is the name of the function to invoke. This procedure can be a quoted literal or an expression, such as a variable that evaluates to a valid function name.

Following this, you can optionally include an IN handle phrase to direct Progress to execute the function in a persistent procedure handle. In this case, the handle must be an actual field or variable name of HANDLE data type, not an expression.

If the function itself takes any parameters, you pass those as additional parameters to DYNAMIC-FUNCTION, in the same form that you would pass them to the function itself.

DYNAMIC-FUNCTION gives you the flexibility to have code that can execute different function names depending on the situation, since the function parameter can be a variable. You might have, for example, several different functions that do parallel work for different categories of data in a loop you’re iterating through. However, that is of perhaps limited value because all the functions you invoke must have the same signature.

The most common use of DYNAMIC-FUNCTION is for one of two other reasons:

Using super procedures in your application

Super procedures were introduced to the Progress language in Version 9 as a way to provide standard libraries of application behavior that can be inherited by other procedures and, where necessary, overridden or specialized by individual application components. They give an object-oriented flavor to Progress programming that was not available before.

A super procedure is a separately compiled Progress procedure file. It’s entry points can effectively be added to those of another procedure so that a RUN statement or function reference in the other procedure causes the Progress interpreter to search both procedures for an internal procedure or function to run. There is also a RUN SUPER statement that lets you implement multiple versions of a single entry point, each in its own procedure file, and have them all execute at run time to provide the application with complex behavior defined at a number of different levels.

In this section, you learn how to build super procedures and how to define application behavior in them that many other procedures can use in a consistent way.

Super procedure language syntax

On the face of it, there is nothing special about a super procedure. That is, there is nothing specific in the 4GL syntax of a Progress procedure file to identify it as a super procedure. Rather, it is how the procedure is referenced by other procedures that makes it a super procedure. Having said this, there are definite guidelines to follow when you build a procedure file that you want to use as a super procedure. These guidelines are discussed in the next section. First, you’ll examine the syntax used for super procedures.

A Progress procedure file must be running as a persistent procedure before it can be made a super procedure of another procedure file. So the first step is that the code somewhere in the application must run the procedure PERSISTENT:

RUN superproc PERSISTENT SET superproc-hdl.

ADD-SUPER-PROCEDURE method

Any application procedure with access to the procedure handle of the super procedure can then add it as a super procedure to itself:

THIS-PROCEDURE:ADD-SUPER-PROCEDURE( superproc-hdl
    [ , search-directive ] ).

Or, in the more general case, it is possible to add a super procedure to any known procedure handle:

proc-hdl:ADD-SUPER-PROCEDURE( superproc-hdl [, search-directive ] ).

In addition, you can add a super procedure at the Session level, using the special SESSION handle, in which case its contents are available to every procedure running in the OpenEdge session:

SESSION:ADD-SUPER-PROCEDURE( superproc-hdl [, search-directive ] ).

The optional search-directive can be either SEARCH-SELF (the default) or SEARCH-TARGET. The significance of this option is discussed in the "Super procedure guidelines" .

REMOVE-SUPER-PROCEDURE method

Once you add a super procedure, you can also remove it from its association with the other procedure, whether you reference it as THIS-PROCEDURE, SESSION, or some other handle:

proc-hdl:REMOVE-SUPER-PROCEDURE( superproc-hdl ).

You can execute multiple ADD-SUPER-PROCEDURE statements for any given procedure handle (including SESSION). The super procedure handles form a stack, which is searched in Last In First Out (LIFO) order when Progress encounters a RUN statement or a function reference at run time. That is, the super procedure added last is searched first to locate the entry point to run. At any time, you can retrieve the list of super procedure handles associated with a procedure using the SUPER-PROCEDURES attribute of a procedure handle:

proc-hdl:SUPER-PROCEDURES

This attribute evaluates to a character string holding the list of super procedure handles (starting with the last one added, therefore indicating the order in which they are searched) as a comma-separated character string.

Changing the order of super procedures

You can rearrange the order of super procedures in two ways:

  1. If the ADD-SUPER-PROCEDURE method is executed for a procedure handle already in the stack, Progress moves it to the head of the list (that is, to the position of being searched first). If the procedure was already first in the stack, no change occurs and no error results. For example, this sequence of statements results in the SUPER-PROCEDURES attribute returning the handle of Super2 followed by the handle of Super1:
  2. hProc:ADD-SUPER-PROCEDURE(hSuper1).
    hProc:ADD-SUPER-PROCEDURE(hSuper2).
    hProc:ADD-SUPER-PROCEDURE(hSuper2).

    And this sequence results in the SUPER-PROCEDURES attribute returning the handle of Super1 followed by the handle of Super2:

    hProc:ADD-SUPER-PROCEDURE(hSuper1).
    hProc:ADD-SUPER-PROCEDURE(hSuper2).
    hProc:ADD-SUPER-PROCEDURE(hSuper1).

  3. You can also rearrange the order of super procedure handles on the stack by invoking a sequence of REMOVE-SUPER-PROCEDURE and ADD-SUPER-PROCEDURE methods. Each REMOVE-SUPER-PROCEDURE method removes that handle from the list, wherever it is. And each ADD-SUPER-PROCEDURE method adds the named handle to the head of the list. A handle is never added to the list a second time.
Invoking behavior in super procedures

The Progress interpreter effectively adds the contents of the super procedures to the name space of the procedure that added it. Therefore, you can invoke the internal procedures and functions defined in a super procedure simply by referencing them as if they were actually implemented in the other procedure. The interpreter locates the routine and executes it.

In addition, because Progress compiles each procedure separately, each has its own compile-time name space, so you can define the same routine in one or more super procedures and also in the other procedures to which they are added. This functionality lets you build hierarchies of behavior for a single entry point name, effectively creating a set of classes that implement different parts of an application’s standard behavior. A local version of an internal procedure can invoke the same routine in its (first) super procedure using this statement:

RUN SUPER [ ( parameters ) ].

If the internal procedure takes any parameters (INPUT, OUTPUT, or INPUT-OUTPUT) it must pass the same parameter types to its super procedure in the RUN SUPER statement. Note that these parameter values do not have to be the same. You might want to change the parameter values before invoking the behavior in the next version of the internal procedure, depending on your application logic.

Likewise, a user-defined function can invoke behavior in a super procedure using the expression:

SUPER ( [ parameters ] ).

This invokes the same function name in the super procedure and passes any parameters to it just as an internal procedure RUN SUPER statement does. The SUPER() expression returns from the super procedure whatever value and data type the function itself returns.

Each super procedure in turn can invoke the next implementation of that same routine up the stack by using the same SUPER syntax.

You must place a RUN SUPER statement inside an implementation of the invoked internal procedure and you must use exactly the same calling sequence. You can place any other 4GL definitions or executable code before or after the SUPER reference. This placement lets you invoke the inherited behavior before, after, or somewhere in the middle of the local specialization of the routine.

Super procedure guidelines

There is nothing specific that identifies a Progress procedure file as a super procedure. However, some guidelines can help in using super procedures effectively and without confusion.

The best general statement is that it is a good practice to consider some procedure files to be application objects, which implement specific application behavior, and others to be service procedures that operate in the background to provide standard behavior for a whole class of application objects. These latter procedures should be your super procedures. Said another way, a super procedure should be a library of standard behavior that many individual application objects can use. What does this mean in practice?

Guideline 1: Use a single super procedure stack

Super procedures should generally not have a super procedure stack of their own. The stack of super procedures (if there is more than one) for an application object should be defined by the object procedure itself. This is done by always using the SEARCH-TARGET keyword in the ADD-SUPER-PROCEDURE statements. This gives you maximum flexibility to define and modify the stack as needed without the individual super procedures having to be aware of each other.

An example can help clarify how this works. In this example, you have an application object procedure and two super procedures from which it inherits standard behavior, as illustrated in Figure 15–2.

Figure 15–2: An object procedure and two super procedures

This is accomplished by running all the procedures persistent and then executing these statements from the object procedure:

THIS-PROCEDURE:ADD-SUPER-PROCEDURE(hTop, SEARCH-TARGET).
THIS-PROCEDURE:ADD-SUPER-PROCEDURE(h2nd, SEARCH-TARGET).

The object procedure executes the following statement:

RUN startMeUp.

The Progress interpreter searches for an internal procedure named startMeUp first in the object procedure itself, then in the code of the last super procedure added (h2nd), and finally in the code of the first super procedure added (hTop). There is an implementation of this internal procedure in all three places and they are all intended to run in sequence. The code in the top super procedure is the most general and is executed last. The code in the 2nd super procedure is more specific and is executed second. And the code in the object procedure itself is specific to that particular object and is executed first. Figure 15–3 shows the sequence of control that takes place at run time.

Figure 15–3: Super procedure stack execution

The code executes as follows:

  1. The interpreter locates the implementation of startMeUp in the object procedure instance and runs it. The startMeUp procedure executes its application-specific code. The RUN SUPER statement causes the interpreter to search up the super procedure stack for another version of the same internal procedure name.
  2. The interpreter searches the instance of the 2nd super procedure.
  3. The interpreter finds and executes the version of startMeUp in the 2nd super procedure. It runs some code and then does another RUN SUPER. Here is where the use of SEARCH-TARGET becomes significant. If you didn’t specify this keyword on the ADD-SUPER-PROCEDURE method, the default would be SEARCH-SELF, which means that the RUN SUPER statement in the 2nd super procedure would cause the interpreter to search up the 2nd super procedure’s own super procedure stack for another version of startMeUp. Because the 2nd super procedure has no super procedures of its own, the interpreter would find nothing and the RUN SUPER statement would return an error.
  4. Instead, the use of SEARCH-TARGET causes the interpreter to go back to the object procedure where startMeUp was run in the first place and continue to search up its stack. This causes it to search the top super procedure.
  5. Finally, the interpreter locates and executes the top-most version of startMeUp.

Now compare this with what is required if you do not use SEARCH-TARGET. For the RUN SUPER statement in the 2nd super procedure to execute properly, you need a double set of super procedure definitions, with this statement in the initialization code for the second super procedure:

THIS-PROCEDURE:ADD-SUPER-PROCEDURE(hTop).

Figure 15–4 diagrams this code.

Figure 15–4: Super procedure stack execution without SEARCH-TARGET

Now the RUN SUPER statement in the 2nd super procedure causes the interpreter to search up its own stack (4), finding the top-level code to execute (5).

So what’s the problem with this approach? This potentially complicates the application considerably. Each super procedure must have its own initialization code to establish its own procedure stack, which must duplicate that portion of the object procedure’s stack above itself (somewhat akin to the old song The Twelve Days of Christmas, each super procedure moving down the chain from the top must duplicate the chain from itself on up). This is not only more work than the other approach, but it also makes it more difficult to build any flexibility into the scheme. What if some object procedure wants to have a different procedure stack? This isn’t really possible because the super procedures themselves duplicate the stack and therefore make it almost impossible to change from object procedure to object procedure. Or what if an object procedure wants to insert an additional super procedure into the stack conditionally? This also can’t really be done. Simply put, unless there are specific circumstances dictating otherwise, it is the best practice always to use the SEARCH-TARGET keyword.

Guideline 2: Use TARGET-PROCEDURE to refer back to the object procedure

If code in a super procedure is to be general-purpose, so that it can serve any object procedure needing it, it must always refer back to the object procedure for handles or other values it needs to make calculations or execute code on behalf of the object. There are two ways you can do this:

  1. You can pass any needed values into the procedure call as input parameters. This is a simple technique but not necessarily an effective one. For one thing, putting values into parameters hard-codes the list of needed values forevermore. This can lead to serious maintainability problems. Keep in mind that not only every reference to the internal procedure or function, but also every RUN SUPER statement inside it, must duplicate the same parameter list. Any change to that list will be a maintenance nightmare. Also, specifying the needed parameters violates the object-oriented nature of the relationship between the two procedures in that it is forcing the object procedure to know something about the super procedure’s implementation of the internal procedure, namely what information it needs to operate on.
  2. Beyond this, it is often awkward to specify parameters for another reason. The object procedure itself probably doesn’t need the information itself—it’s presumably going to be available to it somehow, through local variable names or whatever else—so the parameter won’t be used by a localization of the routine in the object procedure, if there is one. The parameter is only needed when the local procedure passes control to a separately compiled procedure that doesn’t have access to the local variable. This makes use of the parameter look awkward.
  3. A better and more object-oriented technique is to allow the super procedure to refer back to values that the object procedure makes available. This technique provides more independence between the user of the standard behavior and the implementation of that behavior. The traditional Progress programming technique of using SHARED variables doesn’t work in this case, because there is no top-down hierarchy of procedures to let Progress make those values available. The alternative is to allow the super procedure to refer back into the object procedure in some other way.

As an example, Progress Dynamics uses a combination of two techniques that illustrate ways to do this, both using the TARGET-PROCEDURE built-in function.

When an object procedure runs a routine such as startMeUp and the interpreter locates it in a super procedure, then within the startMeUp code inside the super procedure the TARGET-PROCEDURE function evaluates to the procedure handle of the object procedure where the routine was originally run. Alternatively, if there is a local implementation of startMeUp in the object procedure, which then executes a RUN SUPER statement, then the super procedure code can likewise use TARGET-PROCEDURE to obtain the procedure handle of the original object.

Given this handle, the super procedure code can do one of two kinds of things:

Keep in mind that it is the nature of how handles are used in the Progress language that once a procedure object in a given OpenEdge session has the handle of something defined in another procedure instance, it can operate on that handle exactly as the other procedure can. This rule applies to handles of visual controls such as fields, browses, buttons, dynamic buffer handles, dynamic query handles, and dynamic temp-table handles. This is part of what makes the super procedure mechanism so powerful, though it compromises the object-oriented nature of the procedures as well-isolated individual objects. Once you let another procedure gain access to your procedure by giving away, say, the frame handle for the frame your procedure defines, everything else related to that is freely available.

Another important point to keep in mind about TARGET-PROCEDURE is that Progress re-evaluates it every time a new procedure or function name is invoked. For example, throughout the little example in this section, the value of TARGET-PROCEDURE inside any version of startMeUp is the procedure handle of the procedure in which startMeUp was originally invoked (that is, the handle of the procedure where the original RUN statement was located or, if that original RUN statement was RUN startMeUp IN some-other-proc-hdl, then the value of some-other-proc-hdl). This is true no matter how many nested levels of RUN SUPER statements you go through. However, as soon as some other routine is run, the value of TARGET-PROCEDURE changes to be the procedure handle where that routine was run. Once any and all versions of that new routine execute and control returns to some version of startMeUp, then the value of TARGET-PROCEDURE pops back to what it was before.

For this reason, it is important for super procedures to invoke other routines IN TARGET-PROCEDURE if there is any chance that the newly run routine needs to refer to TARGET-PROCEDURE itself. For example, if the startMeUp code in the 2nd super procedure needs to run another internal procedure called moreStartupStuff, then even if moreStartupStuff is also implemented in the 2nd super procedure, you should invoke it by using the RUN moreStartupStuff IN TARGET-PROCEDURE statement if it needs to refer to TARGET-PROCEDURE itself (for example, to retrieve another property value from the object procedure). If you don’t do this, then the value of TARGET-PROCEDURE inside moreStartupStuff becomes the h2nd super procedure handle, which is not useful. This statement also lets the object procedure localize moreStartupStuff if it needs to. If you don’t desire this behavior (that is, if you want this subprocedure to be invisible to object procedures), then you should define moreStartupStuff as PRIVATE, and then pass the value of TARGET-PROCEDURE into it as a parameter if it’s going to be needed.

Guideline 3: Make super procedure code shareable

If you write a super procedure as a library of general-purpose code, it makes sense that in most cases it should be shareable. The use of TARGET-PROCEDURE facilitates this. If the routines in a super procedure always refer back to the TARGET-PROCEDURE, then they always get data values from the procedure they currently support, that is, the procedure they were invoked from. To make sure that stale data isn’t left over from call to call, the general rule is to have no variables or any other definitions scoped to the super procedure main block. You should place all definitions within each individual internal procedure or function, so that they safely go out of scope when the routine exits.

This also means that you should structure your application so that only one instance of each super procedure starts for a session. Again Progress Dynamics, as an example, uses the simple mechanism of checking existing procedure handles and their filenames to see if the super procedure is already running, as shown in this procedure, used by all SmartObjects:

PROCEDURE start-super-proc :
/*---------------------------------------------------------------------
Purpose:     Procedure to start a super proc if it's not already
               running, and to add it as a super proc in any case.
  Parameters:  Procedure name to make super.
  Notes:       NOTE: This presumes that we want only one copy of an ADM
               super procedure running per session, meaning that they
               are stateless (i.e., that every call is independent of
               every other call). This is intended to be the case
               for ours, but may not be true for all super procs.
--------------------------------------------------------------------*/
  DEFINE INPUT PARAMETER pcProcName AS CHARACTER NO-UNDO.
  DEFINE VARIABLE        hProc      AS HANDLE    NO-UNDO.
  hProc = SESSION:FIRST-PROCEDURE.
  DO WHILE VALID-HANDLE(hProc) AND hProc:FILE-NAME NE pcProcName:
    hProc = hProc:NEXT-SIBLING.
  END.
  IF NOT VALID-HANDLE(hProc) THEN
    RUN VALUE(pcProcName) PERSISTENT SET hProc.
  THIS-PROCEDURE:ADD-SUPER-PROCEDURE(hProc, SEARCH-TARGET).
  RETURN.
END PROCEDURE.

Guideline 4: Avoid defining object properties and events in super procedures

This guideline is related to the previous ones about always referring back to TARGET-PROCEDURE for any data needed for an operation done by a super procedure. It also relates to keeping all super procedure data local to the individual internal procedure or function. You should define a property or other persistent data value in a super procedure only if it is truly global. (That is, if it is to be shared by all object procedures that use that super procedure.) This should definitely be the exception.

Similarly, it is a good rule that super procedures should not subscribe to or publish named events directly. You learn about the Progress PUBLISH and SUBSCRIBE syntax in the "PUBLISH and SUBSCRIBE statements" . A PUBLISH or SUBSCRIBE statement should always be done on behalf of the object procedures they serve. Super procedure code might need to subscribe object procedures to named events to set up relationships between procedures.

Forgetting to carry out program actions relative to TARGET-PROCEDURE is one of the most common mistakes in using super procedures.

Guideline 5: Never run anything directly in a super procedure

As discussed earlier, the whole mechanism of using TARGET-PROCEDURE as a means to identify the handle of the object on whose behalf an action is being taken depends on the routine that executes the action being invoked IN the object procedure. The Progress interpreter then takes care of the work of locating the routine in a super procedure and executing it there. This works properly only if application code never runs any routines directly in a super procedure handle. The example shown in Figure 15–5 helps to illustrate this point.

Figure 15–5: Running a procedure within a super procedure

Here’s the (undesired) sequence of events:

  1. Initialization code in the object procedure runs startMeUp, but runs it explicitly IN the handle of its super procedure, rather than letting the interpreter do this implicitly.
  2. Progress does what it’s told and runs startMeUp IN hSuper.
  3. Because startMeUp was specifically run IN hSuper, then hSuper becomes the TARGET-PROCEDURE, the handle of the procedure in which the routine was originally invoked.
  4. Thus, any reference to TARGET-PROCEDURE causes the interpreter to search the super procedure itself for the routine to execute.
  5. Progress never finds and executes the version of nowDoThis back in the object itself.

For the super procedure to get back to the object procedure, it needs to use the SOURCE-PROCEDURE built-in function, but even this won’t always work if the original RUN statement is located anywhere except in the actual source code for the object procedure. So, the whole relationship between the object procedure and its supporting super procedure becomes messed up, and you can expect confusing results.

Note that even in a case where you are defining a global property in a super procedure, as discussed in the previous section, an object that wants to get at that property value should still do it indirectly, by requesting it from itself, and letting the interpreter locate the routine that supplies the value in the super procedure. Figure 15–6 shows an example.

Figure 15–6: A global property in a super procedure

The sequence of events in Figure 15–6 executes as follows:

  1. The main block of the super procedure defines the value to return to any requesting object procedure, outside the scope of an internal procedure or function. This is not actually a Progress GLOBAL variable (which you should avoid), but because it is effectively global to all procedures using this super procedure, in the sense that its value is persistent across calls from many objects, you can use a naming convention of preceding the name with a g to make it clear that it’s defined at the scope of the whole super procedure.
  2. An object procedure requests the value by executing a function that’s defined in the super procedure. But the function is invoked in the object procedure itself, not directly in the handle of the super procedure. The interpreter locates the function in the super procedure and executes it there. Invoking the function directly in the super procedure by using the syntax DYNAMIC-FUNCTION (‘:valueEverybodyNeeds’ IN hSuper) is a bad idea. First, because it messes up any attempt by the function code to reference TARGET-PROCEDURE, and, second, because it hard-codes into the object procedure the fact that the supporting function is implemented in this particular super procedure. This makes the application more difficult to maintain.
  3. The supporting function returns the value, which is then used by the object procedure.

This simple example presumes that there is a function prototype defined in the object procedure so that the compiler knows how to evaluate the function. You can accomplish this easily by using the ProtoGen tool in the PRO*Tools palette of the AppBuilder, which generates a Progress include file containing a prototype for every routine defined in the super procedure. Your code can then include this file in the object procedure to make the prototypes available.

Using session super procedures

In addition to associating super procedures with a specific object procedure, you can also add them to the entire OpenEdge session so that every procedure that executes in the session can take advantage of them. You do this by executing the ADD-SUPER-PROCEDURE method from the SESSION handle:

SESSION:ADD-SUPER-PROCEDURE( hSuper ).

The search order the interpreter uses to locate internal procedures and functions now becomes this:

  1. The procedure file the routine is run in (that is, either the procedure that actually contains the RUN statement or function reference or, if you use the RUN IN syntax or the DYNAMIC-FUNCTION . . . IN syntax, the procedure handle following the IN keyword).
  2. Super procedures added to that procedure handle, starting with the last one added.
  3. Super procedures added to the SESSION handle, starting with the last one added.

In addition, in the case of a RUN statement, if an internal procedure of that name is not located anywhere, the final step of the search is for a compiled external procedure (.r file) of that name.

You should exercise caution when adding super procedures to the session, precisely because their contents become available to absolutely every procedure run in the session. Nevertheless, this technique can be effective in some cases, especially for extending the behavior of existing application procedures without having to edit them to put in ADD-SUPER-PROCEDURE statements. If the existing procedure was originally intended to run a compiled external procedure (.r file) and does not explicitly include the .p or .r extension on the filename reference in the RUN statement, then the external .r file can be replaced by a session super procedure that contains an internal procedure of the same name. This changes the behavior of the application without making any changes whatsoever to the existing application files (not even recompiling them).

Super procedure example

To show some of the principles of super procedures in action, you can create one that manipulates windows in a simple way. The super procedure needs to have a single entry point, an internal procedure called alignWindow, to position all windows to the same column.

To create an example super procedure:
  1. From the AppBuilder, select New Structured Procedure.
  2. Make sure the Procedures toggle box is checked so that this nonvisual program template is in the list.
  3. Add a new procedure called alignWindow:
  4. /*---------------------------------------------------------------------
      Purpose:   Aligns all windows to the same column position.
    ---------------------------------------------------------------------*/
      TARGET-PROCEDURE:CURRENT-WINDOW:COL = 20.0.
      RETURN.
    END PROCEDURE.

    The TARGET-PROCEDURE handle is the handle of the window procedure that this is a super procedure for. The code simply sets its current window’s column position to 20. Note the use of chained attribute references. You can chain together as many object attributes as you need in a single expression, as long as all the attributes (except the last) evaluate to handles. Also, the COL attribute is of type DECIMAL, so don’t forget the decimal on the value 20.0.
    Structured procedures don’t have any visualization, so there’s no real design window for them in the AppBuilder. Instead, you get a window with a tree view with all the code sections.
  5. Expand the tree view and double-click on any section to bring it up in the Section Editor:
  6. Now you need a standard procedure that knows how to start a super procedure. It needs to determine whether it’s already running, run it if it’s not there, and then make it a super procedure of the requesting procedure.
  7. Define a new external procedure called h-StartSuper.p with this code:
  8. /* h-StartSuper.p -- starts a super procedure if not already running,
      and returns its handle. */
      DEFINE INPUT  PARAMETER cProcName AS CHARACTER  NO-UNDO.
      DEFINE VARIABLE hProc AS HANDLE     NO-UNDO.
      /* Try to locate an instance of the procedure already running. */
      hProc = SESSION:FIRST-PROCEDURE.
      DO WHILE VALID-HANDLE(hProc):
          IF hProc:FILE-NAME = cProcName THEN
             LEAVE.    /* found it. */
          hProc = hProc:NEXT-SIBLING.
      END.
      /* If it wasn't found, then run it. */
      IF NOT VALID-HANDLE(hProc) THEN
          RUN VALUE(cProcName) PERSISTENT SET hProc.
      /* In either case, add it as a super procedure of the caller. */
      SOURCE-PROCEDURE:ADD-SUPER-PROCEDURE(hProc, SEARCH-TARGET).

    This code looks for a running instance of the procedure name passed in, using the SESSION procedure list. If it’s not there, it runs it. Then it adds it as a super procedure of the SOURCE-PROCEDURE, which is the procedure that ran h-StartSuper.p.
  9. Add statements to the main block of both h-CustOrderWin6.w and h-OrderWin.w to get h-StartSuper.p to start h-WinSuper.p and then to run alignWindow:
  10. MAIN-BLOCK:
    DO ON ERROR  UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK
      ON END-KEY UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK:
      RUN h-StartSuper.p (INPUT "h-WinSuper.p").  /* add this super procedure */
      RUN alignWindow.
      RUN enable_UI.
      ASSIGN cState = Customer.State
             iMatches = NUM-RESULTS("CustQuery").
      DISPLAY cState iMatches WITH FRAME CustQuery.
      APPLY "VALUE-CHANGED" TO OrderBrowse.
    END.

To review what’s happening when you run h-CustOrderWin6.w again, look at the diagram in Figure 15–7.

Figure 15–7: Running the sample procedure with a super procedure

The dotted lines represent transient relationships that are only present during startup. Both h-CustOrderWin6.w and h-OrderWin.w run h-StartSuper.p to get their super procedure. When each runs it in turn, SOURCE-PROCEDURE points back to the procedure that ran it. h-StartSuper.p establishes h-WinSuper.p as the super procedure for each one in turn, and then goes away because it was not run persistent itself. It was only needed to set up the relationships.

Once the persistent procedures are all set up, each one runs alignWindow. There’s no alignWindow procedure in either of the .w’s, so Progress searches the super procedure stack and locates it in h-WinSuper.p and runs that code. Now alignWindow can refer to TARGET-PROCEDURE to access the window handle inside each of the .w’s.

To add another small layer of complexity:
  1. Add an implementation of alignWindow in h-CustOrderWin6.w:
  2. /*---------------------------------------------------------------------
      Purpose:     Local version of alignWindow for h-CustOrderWin
                   to set ROW as well as column.
    ---------------------------------------------------------------------*/
      RUN SUPER.
      ASSIGN THIS-PROCEDURE:CURRENT-WINDOW:ROW = 10.0.
    END PROCEDURE.

    This code uses a RUN SUPER to invoke the standard behavior in h-WinSuper.p, and then adds some more code of its own, in this case to set the ROW attribute of the window.
    So the net effect of these two versions of alignWindow is to set both the COL and the ROW.
  3. Do the same thing in h-OrderWin.w. Define a version of alignWindow that sets ROW to something different from the local code in h-CustOrderWin6.w:
  4. /*---------------------------------------------------------------------
      Purpose:    Local version of alignWindow for OrderWin sets Row to 20.0.
      Parameters: <none>
      Notes:
    ---------------------------------------------------------------------*/
      RUN SUPER.
      THIS-PROCEDURE:CURRENT-WINDOW:ROW = 20.0.
    END PROCEDURE.

    Now all Order windows come up initially in the same place, on top of one another. You can drag them around to see all of them. The diagram in Figure 15–8 represents this second situation.

Figure 15–8: Variation on running the sample procedures with a super procedure

When Progress executes the RUN alignWindow statement, it finds the internal procedure locally and executes it. The local version first invokes the standard behavior in the super procedure and then extends it with its own custom code.

Super procedures might seem like a complicated mechanism, but consider that they are intended to allow you to provide standard behavior for many procedure objects. This means that you can carefully craft the super procedure code for a type of behavior, and then every other procedure that uses that super procedure inherits the behavior automatically and transparently. This is the power of super procedures.

PUBLISH and SUBSCRIBE statements

When you have multiple procedures running in a session, you might want them to communicate with each other without being strictly bound to each other. In other words, one procedure might want to send a message to other procedures that are interested in receiving that message without knowing or caring just how many such procedures there are, what their procedure handles are, or what they intend to do with the information. Likewise, a procedure might want to post an interest in receiving a message on a subject without necessarily knowing or caring where the message comes from.

Progress supports PUBLISH and SUBSCRIBE keywords for this purpose. One procedure can publish a named event when something of interest happens and other procedures running in the same session can subscribe to that same event name, either in a particular procedure handle or anywhere in the session. When the event occurs, an internal procedure in each subscriber runs. If there are no subscribers, then nothing happens and no error results. If there are many subscribers, they all get the message. In effect, a PUBLISH statement amounts to Progress executing this pseudo-code:

FOR EACH subscriber-handle:
   RUN event-procedure IN subscriber-handle NO-ERROR.
END.

The order in which multiple subscribers receive an event is undefined.

Just as with any procedure call, a PUBLISH statement can include one or more parameters. These are normally INPUT parameters, although under some circumstances other parameter modes can be useful.

Subscribing to an event

Since a procedure has to subscribe to an event before anything happens when another procedure publishes it, you’ll look at the SUBSCRIBE statement first. Here is its syntax:

SUBSCRIBE [ PROCEDURE subscriber-handle ]
  [ TO ] event-name-expr
  { IN publisher-handle | ANYWHERE }
  [ RUN-PROCEDURE local-internal-procedure ]
  [ NO-ERROR ]

By default, a SUBSCRIBE statement registers the request on behalf of the external procedure that contains the SUBSCRIBE statement. The value of the procedure handle is the built-in Progress handle THIS-PROCEDURE. For the subscriber to receive the event, it must be running at the time the event occurs, so normally this means that you should only include a SUBSCRIBE statement in a procedure that is run persistent.

You can, however, create a service procedure that subscribes other procedures to events. In that case, you can include the PROCEDURE subscriber-handle phrase and the SUBSCRIBE is done on behalf of that other procedure handle.

The event-name-expr is a string expression holding the name of the event to publish. This is a standard Progress name of the same type as an internal procedure name. The default action when the event is published is to run an internal procedure in the subscriber with the same name as the event.

When you subscribe to an event, you can either subscribe to it in a specific running procedure handle that is available, or you can use the ANYWHERE keyword to indicate that you want to be notified when this event occurs anywhere in your session. If you specify the IN publisher-handle phrase, then the publisher must be a procedure that is already running and that remains running until it publishes the event. There is no way to subscribe to events that are published by procedures in other OpenEdge sessions.

One common practice is to subscribe to events that are published by the procedure that started the subscriber. The handle of that procedure is available in the SOURCE-PROCEDURE handle. An example in the "PUBLISH/SUBSCRIBE example" shows you how this works.

A procedure can also subscribe to events in the SESSION handle. Other procedures can then publish events from the SESSION handle, thus using it as a kind of central coordinating point for events.

If the name of the internal procedure you want to run in response to the event must be different from the event name itself, then you include the RUN-PROCEDURE local-internal-procedure phrase in the SUBSCRIBE statement. You must implement the internal procedure in the subscriber (or one of its super procedures, if any).

If it is possible that the subscriber-handle or publisher-handle might not be valid at the time the statement is executed, you can include the NO-ERROR phrase to suppress any error messages that would result from this. In this case, the SUBSCRIBE has no effect, and the ERROR-STATUS handle holds a status and message describing the error.

Publishing an event

Once there are one or more subscribers to an event, then a procedure can publish that event and each of the subscribers receives it. The publisher does not have to preregister the intention of publishing the event, even though the subscriber must register the intent to receive it. This is the syntax for the PUBLISH statement:

PUBLISH event-name [ FROM publisher-handle ]
  [ ( parameter [ , . . .] ) ].

As with the SUBSCRIBE statement, the default is to publish the event from the procedure that contains the PUBLISH statement. A procedure can also publish events on behalf of another procedure by including the FROM publisher-handle phrase. Any subscriber that has subscribed to the event of the same name in the publisher-handle, whether implicit, explicit, or ANYWHERE, receives it.

Passing parameters

The publisher can pass parameters in the same way as for a RUN statement. Ordinarily, any such parameters are INPUT parameters so that the publishing procedure can pass one or more values into each subscriber. INPUT parameters are the norm because the publisher normally does not want to know the specifics of who the subscribers are and therefore is unlikely to be interested in output from them. If the publisher wants to get a specific response back, then it is better to use a RUN statement with a known procedure handle to run it in.

In particular, an OUTPUT parameter is almost always a bad idea because its value is received only from the last subscribing procedure to execute. If there are multiple subscribers, the OUTPUT parameter returned by the rest of them is discarded. Since there is no way to determine which of multiple subscribers will run last, this is not a reliable or useful mechanism.

On the other hand, there are times when it can be useful to pass an INPUT-OUTPUT parameter. In this case, each subscriber in turn receives the current value of the parameter and can act on it and modify it. The next subscriber receives the modified value and can modify it further. Finally, the publisher receives it as output from the final subscriber and can see the collective result. This is useful in cases where all the subscribers need to contribute something to a final total, perhaps adding a value to the current value of the parameter or appending something to the end of a delimited list. The publisher then sees the result of all the calls. Again, there is no determined order for the subscribers to be called, but if the order of execution is not important, then an INPUT-OUTPUT parameter is sometimes useful.

The final subscriber can also pass back a string in a RETURN statement that is received by the publisher as the RETURN-VALUE just as if the publisher had used a RUN statement. Again, there is no way to know which subscriber will be the final one called, so each subscriber could append text to the existing RETURN-VALUE in a way similar to using an INPUT-OUTPUT parameter in a form such as the one in the following example. In this code, the RETURN statement appends the value of the local CHARACTER variable cMyIdent to the current RETURN-VALUE:

RETURN RETURN-VALUE + “ “ + cMyIdent.

Each subscriber can see the value returned by the previous subscriber in its RETURN-VALUE. If this is the final statement in each subscriber’s internal procedure, then the publisher can look at the value of RETURN-VALUE and see all the accumulated values of cMyIdent for all the subscribers.

The PUBLISH statement always executes NO-ERROR. If there are no subscribers, no error message results. If one or more subscribers have a parameter list that does not match the parameters in the PUBLISH statement, then those subscribers do not receive the event and, likewise, no error results. For this reason, it is important to make sure that subscribers have the proper calling sequence. Otherwise, their procedures will not be run and it might not be clear to you why the PUBLISH statement seems to have failed.

Canceling a subscription

You can also cancel the effect of a SUBSCRIBE statement by using the UNSUBSCRIBE statement:

UNSUBSCRIBE [ PROCEDURE subscriber-handle ]
   [ TO ] { event-name | ALL } [ IN publisher-handle ]

As with the SUBSCRIBE statement, the default is that UNSUBSCRIBE cancels one or more events for the current procedure. Otherwise, you can use the PROCEDURE subscriber-handle phrase. You can unsubscribe a specific event-name or ALL events. If you include the IN publisher-handle phrase, then the event-name subscription or ALL events, as specified, are canceled only in that handle. If you specify ALL events without a publisher-handle, then all the subscriber-handle’s subscriptions are canceled.

PUBLISH/SUBSCRIBE example

The h-CustOrderWin6.w procedure runs a separate window called h-OrderWin.w to display details from the current Order. You can run this Order window multiple times.

To see how you can use PUBLISH and SUBSCRIBE statements to extend the communication between these procedures:
  1. Open h-OrderWin.w and save it as h-OrderWin1b.w.
  2. In h-OrderWin1b.w define this internal procedure called ShipDateChange:
  3. /*---------------------------------------------------------------------
      Procedure ShipDateChange:
      Purpose:    receives the event of the same name to update the
                  ShipDate field display when the field value changes.
      Parameters:  Order Number and new ShipDate SCREEN-VALUE.
      Notes:  ShipDateChange is PUBLISHed by h-CustOrderWin6.w
    ---------------------------------------------------------------------*/
    DEFINE INPUT  PARAMETER iOrderNum AS INTEGER    NO-UNDO.
    DEFINE INPUT  PARAMETER cShipDate AS CHARACTER  NO-UNDO.
    IF STRING(iOrderNum) = Order.OrderNum:SCREEN-VALUE IN FRAME OrderFrame THEN
    DO:
        Order.ShipDate:SCREEN-VALUE IN FRAME OrderFrame = cShipDate.
        shipDate:BGCOLOR = dateColor(PromiseDate, DATE (ShipDate:SCREEN-VALUE)).
    END.
    END PROCEDURE.

    This code responds to an event published by the main window whenever the value of the ShipDate field changes. It receives both the Order Number and the new ShipDate as INPUT parameters. If the Order Number matches the one displayed by this instance of OrderWin, then the displayed ShipDate is changed to the value passed in, and the dateColor procedure is run to flag it with a new background color if its value requires this warning signal.
  4. Add this SUBSCRIBE statement to the procedure’s main block:
  5. ASSIGN THIS-PROCEDURE:PRIVATE-DATA = STRING(OrderNum)
             hSource = SOURCE-PROCEDURE
             shipDate:BGCOLOR = dateColor(PromiseDate, ShipDate).
      SUBSCRIBE TO "ShipDateChange" IN SOURCE-PROCEDURE.
      IF NOT THIS-PROCEDURE:PERSISTENT THEN
        WAIT-FOR CLOSE OF THIS-PROCEDURE.

    This sets up every running instance of h-OrderWin.w to receive the ShipDateChange event when the SOURCE-PROCEDURE h-CustOrderWin publishes it. There might be multiple instances of OrderWin that have subscribed to the event. Under other circumstances, there could just as easily be multiple, completely different versions of the procedure ShipDataChange in multiple different procedure files that do different things with the changed ShipDate information. The publisher has no reason to know or care.
  6. In h-CustOrderWin, go into the property sheet for the OrderBrowse and enable the ShipDate column.
  7. Define this LEAVE trigger for the ShipDate field:
  8. DO:
        IF Order.ShipDate NE DATE(Order.ShipDate:SCREEN-VALUE IN BROWSE OrderBrowse)
        THEN PUBLISH "ShipDateChange"
            (INPUT Order.OrderNum,
             INPUT Order.ShipDate:SCREEN-VALUE).
    END.

    If the field value changes, the published event notifies all subscribers.
  9. Run the main window and choose the Order Details button several times for different Orders to bring up multiple instances of OrderWin.
  10. Modify the ShipDate for one of those Orders, then tab out of the field.
  11. The corresponding OrderWin instance for that Order shows the new ShipDate, possibly with a different color, depending on the value of ShipDate, as this sequence shows:
    1. Run h-CustOrderWin6.w and choose the Order Details button for several Orders:
    2. One of those orders shows that there’s no ShipDate:

    3. Enter a ShipDate for the Order, then tab out of the field:
    4. Now the Order Detail window for that Order shows the new date:

Conclusion

You’ve learned a lot about the Progress 4GL up to this point, but you still haven’t made a single change to a database record. It’s about time to get to that! The 4GL allows you to define and scope database updates and their transactions with a flexibility unmatched by any other programming environment. In the next chapter you learn how to do this.


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