Progress
Programming
Handbook


Persistent Multi-window Management

An application that uses persistent multi-window management generally provides its basic application options in the default application window. These options then open one or more windows under persistent procedure control to provide functionality in addition to the default application window. All of these windows, while visible, remain available to the user in non-modal fashion. You can also provide modal capabilities anywhere in the non-modal interface using alert boxes and dialog boxes.

Creating Windows in Persistent Procedures

In general, you create and manage each persistent window using a persistent procedure. Each time you run a procedure persistently, it instantiates (creates) a context for itself. This context can include any windows that you create during instantiation. However, each persistent procedure can maintain its own current window by setting the CURRENT–WINDOW attribute of the THIS–PROCEDURE handle. This attribute overrides (but does not change) the setting of the CURRENT–WINDOW handle. If set to the widget handle of a valid window, all frames and dialog boxes defined in the procedure parent, by default, to the window specified by this attribute. Thus, a persistent procedure instance usually manages a single window in the multi-window application. For more information on procedure handles and persistent procedures, see Block Properties."

Managing Windows in Persistent Procedures

It is also often helpful to create a separate dynamic widget pool for the window you create in the persistent procedure. Usually, you want the window to exist for the life of the persistent procedure that manages it. The simplest way to guarantee this is by creating a single unnamed widget pool for the procedure. When you delete a persistent procedure that maintains its own widget pool, its persistent window is automatically deleted also. For more information on dynamic widget pools, see Using Dynamic Widgets."

Example — Persistent Multi-window Management

The following three procedures, p-perwn1.p, p-perwn2.p, and p-perwn3.p implement a small application for updating customer orders in the sports database using multiple windows. The main procedure (p-perwn1.p) displays a browse of the customer table in the default application window. You can browse the order table for a selected customer by choosing the bOpenorders button. This creates a persistent procedure (p-perwn2.p) that opens a child window for the order browse (p-perwn1.p (1)):

p-perwn1.p
    DEFINE QUERY custq FOR customer.
    DEFINE BROWSE custb QUERY custq
        DISPLAY name cust-num balance credit-limit phone sales-rep 
    WITH 10 DOWN.

    DEFINE BUTTON bExit LABEL "Exit".
    DEFINE BUTTON bOpenorders LABEL "Open Orders".
    DEFINE VARIABLE whand AS WIDGET-HANDLE.
    DEFINE VARIABLE lcust-redundant AS LOGICAL.

    DEFINE FRAME CustFrame 
        custb SKIP bExit bOpenorders
    WITH SIZE-CHARS 96.5 by 11.
    
    ON CHOOSE OF bExit IN FRAME CustFrame DO:
        RUN exit-proc.
    END.        

    ON CHOOSE OF bOpenorders IN FRAME CustFrame DO:
        IF custb:NUM-SELECTED-ROWS >= 1 THEN DO:
            RUN check-redundant(OUTPUT lcust-redundant).
            IF NOT lcust-redundant THEN DO:
                MESSAGE "Opening orders for" customer.name + ".".
(1)             RUN p-perwn2.p PERSISTENT 
                    (BUFFER customer, INPUT whand:TITLE, 
                     INPUT whand) NO-ERROR.
            END.
            ELSE DO:
                BELL.
                MESSAGE "Orders already open for" customer.name + ".".
            END.
        END.
        ELSE DO:
            BELL.
            MESSAGE "Select a customer to open orders ...".
        END.
    END.   
(2) CREATE WINDOW whand
        ASSIGN
            TITLE = "Customer Order Maintenance"
            SCROLL-BARS = FALSE
            RESIZE = FALSE
            HEIGHT-CHARS = FRAME CustFrame:HEIGHT-CHARS
            WIDTH-CHARS = FRAME CustFrame:WIDTH-CHARS.
    CURRENT-WINDOW = whand.

    OPEN QUERY custq PRESELECT EACH customer BY name.
    PAUSE 0 BEFORE-HIDE.
    ENABLE ALL WITH FRAME CustFrame.

    WAIT-FOR CHOOSE OF bExit IN FRAME CustFrame.

    PROCEDURE exit-proc:
        DEFINE VARIABLE phand AS HANDLE.
        DEFINE VARIABLE nhand AS HANDLE.

        MESSAGE "Exiting application ...".
(3)     phand = SESSION:FIRST-PROCEDURE.
        DO WHILE VALID-HANDLE(phand):
            nhand = phand:NEXT-SIBLING.
            IF LOOKUP(whand:TITLE, phand:PRIVATE-DATA) > 0 THEN
                RUN destroy-query IN phand NO-ERROR.
            phand = nhand.
        END.
        DELETE WIDGET whand.
    END PROCEDURE.

    PROCEDURE check-redundant:
        DEFINE OUTPUT PARAMETER lcust-redundant AS LOGICAL INITIAL FALSE.
    
        DEFINE VARIABLE phand AS HANDLE.
        DEFINE VARIABLE nhand AS HANDLE.

(4)     phand = SESSION:FIRST-PROCEDURE.
        DO WHILE VALID-HANDLE(phand):
            nhand = phand:NEXT-SIBLING.
            IF LOOKUP(whand:TITLE, phand:PRIVATE-DATA)   > 0 AND
               LOOKUP(customer.name, phand:PRIVATE-DATA) > 0 THEN DO:
                    lcust-redundant = TRUE.
                    RETURN.
            END.
            phand = nhand.
        END.
    END PROCEDURE. 

You can repeatedly select another customer and choose the bOpenorders button to browse orders for as many customers as you want. The customer and order browses displayed for every customer all remain available to the user for input simultaneously.

This application makes ample use of procedure and SESSION handle attributes to help manage the persistent windows of the application. The main procedure uses them in two ways:

When you terminate the application by choosing the bExit button, p-perwn1.p calls a local internal procedure, exit–proc. After locating the first persistent procedure instance, exit–proc loops through all persistent procedures in the session and deletes each one created by the application as specified by the PRIVATE–DATA procedure handle attribute (p-perwn1.p (3)). Note that exit–proc deletes each persistent procedure by calling the destroy–query procedure owned by the persistent procedure. This ensures that each persistent procedure manages its own resource clean-up and deletion.

When you attempt to open the order browse for a customer, p-perwn1.p also checks to see if there is already an order browse open for that customer. It does this by looking for a persistent procedure with the customer name as part of its private data (p-perwn1.p (4)).

Note also that p-perwn1.p creates its own default application window instead of using the static window (p-perwn1.p (2)). This allows the setting of the SCROLL–BARS and RESIZE window attributes.

The persistent procedure, p-perwn2.p, displays a browse of all orders for the selected customer. Like the customer browse, you can repeatedly select orders and choose the bOpenlines button (which runs p-perwn3.p) to browse order lines in a child window of a selected order for as many orders as you want (p-perwn2.p (1)). In p-perwn2.p, you can also update each order by choosing the bUpdate button and close all orders for a customer by choosing the bClose button.

Note that as persistent procedures, both p-perwn2.p and p-perwn3.p are written to be run non-persistently for testing or other application purposes. This is accomplished by making some of the code conditionally executable based on the value of the PERSISTENT attribute of the THIS–PROCEDURE system handle. For example, the bClose button in p-perwn2.p runs the destroy–query procedure or just exits p-perwn2.p, depending on how it is run (p-perwn2.p (4)). You can devise many variations on this approach. One possible variation includes using the preprocessor to conditionally compile the PERSISTENT attribute tests based on whether you are compiling for a development or production environment.

Each persistent procedure easily maintains the current window that it creates by setting the CURRENT–WINDOW attribute of the THIS–PROCEDURE handle (p-perwn2.p (3) and p-perwn3.p (2)). Thus, there is no need to set and reset the CURRENT–WINDOW handle:

p-perwn2.p
    DEFINE PARAMETER BUFFER custbuf FOR customer.
    DEFINE INPUT PARAMETER appdata AS CHARACTER.
    DEFINE INPUT PARAMETER wparent AS WIDGET-HANDLE.

    DEFINE QUERY orderq FOR order.
    DEFINE BROWSE orderb QUERY orderq
        DISPLAY 
            order.order-num order.order-date 
            order.ship-date order.promise-date order.carrier
        WITH 5 DOWN.
    DEFINE BUTTON bClose LABEL "Close Orders".
    DEFINE BUTTON bOpenlines LABEL "Open Order Lines".
    DEFINE BUTTON bUpdate LABEL "Update Order".
    DEFINE VARIABLE whand AS WIDGET-HANDLE.
    DEFINE VARIABLE lorder-redundant AS LOGICAL.
    DEFINE FRAME OrderFrame SKIP(.5)
        custbuf.name COLON 11 VIEW-AS TEXT SKIP
        custbuf.cust-num COLON 11 VIEW-AS TEXT SKIP(.5) 
        orderb SKIP
        bClose bOpenlines bUpdate
    WITH SIDE-LABELS SIZE-CHARS 65.8 by 9.
    
    ON CHOOSE OF bOpenlines IN FRAME OrderFrame DO:
        IF orderb:NUM-SELECTED-ROWS >= 1 THEN DO:
            RUN check-redundant(OUTPUT lorder-redundant).
            IF NOT lorder-redundant THEN DO:
                MESSAGE "Opening order lines for order" 
                        STRING(order.order-num) + ".".
(1)             RUN p-perwn3.p PERSISTENT 
                    (BUFFER order, INPUT THIS-PROCEDURE:PRIVATE-DATA,
                     INPUT whand) NO-ERROR.
            END.
            ELSE DO:
                BELL.
                MESSAGE "Order lines already open for order" 
                        STRING(order.order-num) + ".".
            END.
        END.
        ELSE DO:
            BELL.
            MESSAGE "Select an order to open order lines ...".
        END.
    END.   
    ON CHOOSE OF bUpdate IN FRAME OrderFrame DO:
        IF orderb:NUM-SELECTED-ROWS >= 1 THEN DO:
            RUN update-order.
        END.
        ELSE DO:
            BELL.
            MESSAGE "Select an order to update ...".
        END.
    END.  

(2) IF THIS-PROCEDURE:PERSISTENT THEN DO:
        THIS-PROCEDURE:PRIVATE-DATA = appdata + "," + custbuf.name.
        CREATE WIDGET-POOL.
    END.

(3) CREATE WINDOW whand
        ASSIGN
            TITLE = "Orders for Customer ..."
            PARENT = wparent
            RESIZE = FALSE
            SCROLL-BARS = FALSE
            HEIGHT-CHARS = FRAME OrderFrame:HEIGHT-CHARS
            WIDTH-CHARS = FRAME OrderFrame:WIDTH-CHARS.
        
    THIS-PROCEDURE:CURRENT-WINDOW = whand.

    OPEN QUERY orderq PRESELECT EACH order
        WHERE order.cust-num = custbuf.cust-num BY order-num.
    DISPLAY custbuf.cust-num custbuf.name WITH FRAME OrderFrame.
    ENABLE ALL WITH FRAME OrderFrame.

(4) IF THIS-PROCEDURE:PERSISTENT THEN DO:
        ON CHOOSE OF bClose IN FRAME OrderFrame DO:
            RUN destroy-query.
        END.
    END.
    ELSE DO: 
        WAIT-FOR CHOOSE OF bClose IN FRAME OrderFrame.
    END. 
    PROCEDURE destroy-query:
        DEFINE VARIABLE phand AS HANDLE.
        DEFINE VARIABLE nhand AS HANDLE.
    
(5)     MESSAGE "Exiting orders for" custbuf.name "...".
        phand = SESSION:FIRST-PROCEDURE.
        DO WHILE VALID-HANDLE(phand):
            nhand = phand:NEXT-SIBLING.
            IF LOOKUP(custbuf.name, phand:PRIVATE-DATA) > 0 AND
               phand <> THIS-PROCEDURE THEN
                    RUN destroy-query IN phand NO-ERROR.
            phand = nhand.
        END.
        DELETE PROCEDURE THIS-PROCEDURE NO-ERROR.
        DELETE WIDGET-POOL.
    END.

    PROCEDURE check-redundant:
        DEFINE OUTPUT PARAMETER lorder-redundant AS LOGICAL INITIAL FALSE.
    
        DEFINE VARIABLE phand AS HANDLE.
        DEFINE VARIABLE nhand AS HANDLE.

(6)     phand = SESSION:FIRST-PROCEDURE.
        DO WHILE VALID-HANDLE(phand):
            nhand = phand:NEXT-SIBLING.
            IF LOOKUP(appdata, phand:PRIVATE-DATA) > 0 AND
               LOOKUP(STRING(order.order-num), 
                               phand:PRIVATE-DATA) > 0 THEN DO:
                    lorder-redundant = TRUE.
                    RETURN.
            END.
            phand = nhand.
        END.
    END PROCEDURE.

    PROCEDURE update-order:
        DEFINE VARIABLE rid AS ROWID.
        DEFINE VARIABLE choice AS LOGICAL.
        DEFINE BUTTON bSave LABEL "Save Changes".
        DEFINE BUTTON bCancel LABEL "Cancel". 
(7)     DEFINE FRAME UpdateFrame SKIP(.5)
            order.order-num COLON 12 VIEW-AS TEXT 
                order.sales-rep VIEW-AS TEXT "For..." VIEW-AS TEXT
                custbuf.name VIEW-AS TEXT SKIP
            custbuf.cust-num COLON 48 VIEW-AS TEXT SKIP(1) SPACE(1)
            order.order-date order.ship-date order.promise-date 
            SKIP SPACE(1)
            order.carrier order.instructions SKIP SPACE(1)
            order.PO order.terms SKIP(1)
            bSave bCancel        
        WITH TITLE "Update Order" SIDE-LABELS VIEW-AS DIALOG-BOX.

        ON CHOOSE OF bSave IN FRAME UpdateFrame 
           OR GO OF FRAME UpdateFrame DO:
            MESSAGE "Are you sure you want to save your changes?"
                VIEW-AS ALERT-BOX QUESTION BUTTONS YES-NO
                UPDATE choice AS LOGICAL.
            CASE choice:
(8)             WHEN TRUE THEN DO TRANSACTION ON ERROR UNDO, RETRY:
                    rid = ROWID(order).
                    FIND order WHERE ROWID(order) = rid EXCLUSIVE-LOCK.
                    ASSIGN
                        order.order-date
                        order.ship-date
                        order.promise-date
                        order.carrier
                        order.instructions
                        order.PO order.terms.
                    DISPLAY 
                        order.order-num order.order-date 
                        order.ship-date order.promise-date order.carrier
                    WITH BROWSE orderb.
                    APPLY "WINDOW-CLOSE" TO FRAME UpdateFrame.
                END.
                WHEN FALSE THEN DO:
                   MESSAGE "Changes not saved."
                       VIEW-AS ALERT-BOX INFORMATION BUTTONS OK.
                   RETURN NO-APPLY.
                END.
            END CASE.
        END. 
        ON CHOOSE OF bCancel DO:
            MESSAGE 
                "Are you sure you want to cancel your current updates?"
                VIEW-AS ALERT-BOX QUESTION BUTTONS YES-NO
                UPDATE choice AS LOGICAL.
            CASE choice:
                WHEN TRUE THEN DO:
                   MESSAGE "Current updates cancelled."
                       VIEW-AS ALERT-BOX INFORMATION BUTTONS OK.
                   APPLY "WINDOW-CLOSE" TO FRAME UpdateFrame.
                END.
                WHEN FALSE THEN DO:
                   MESSAGE "Current update is continuing ..."
                       VIEW-AS ALERT-BOX INFORMATION BUTTONS OK.
                END.
            END CASE.
        END.

        DISPLAY 
            order.order-num order.sales-rep custbuf.name custbuf.cust-num
            order.order-date order.ship-date order.promise-date
            order.carrier order.instructions order.PO order.terms
        WITH FRAME UpdateFrame IN WINDOW ACTIVE-WINDOW.
        ENABLE
            order.order-date order.ship-date order.promise-date
            order.carrier order.instructions order.PO order.terms
            bSave bCancel
        WITH FRAME UpdateFrame IN WINDOW ACTIVE-WINDOW.
    
        WAIT-FOR WINDOW-CLOSE OF FRAME UpdateFrame.
    END PROCEDURE. 

Two other important features of persistent management include setting the PRIVATE–DATA attribute and creating a widget pool so that the context of each persistent procedure can be more easily managed as a unit (p-perwn2.p (2) and p-perwn3.p (1)).

Like p-perwn1.p, p-perwn2.p uses procedure and SESSION handle attributes:

Note that each persistent procedure has a unique destroy–query procedure. This allows any other procedure to delete the persistent procedures under its control while allowing each persistent procedure to manage its own area of control appropriately.

The persistent procedure, p-perwn3.p, browses and updates order lines very much like p-perwn2.p browses and updates orders, except that it is at the bottom of the hierarchy of persistent management. When you choose the bClose button in p-perwn3.p, destroy–query only has to delete the current instance of p-perwn3.p (p-perwn3.p (3)). Depending on your application, you can also use a management model that is more or less hierarchical. Because Progress procedures are recursive, you can even create persistent procedures and windows recursively. However, there is no necessary connection between a persistent context and its recursive instance unless you establish it, for example, using the PRIVATE–DATA attribute or a shared temporary table.

p-perwn3.p
    DEFINE PARAMETER BUFFER orderbuf FOR order.
    DEFINE INPUT PARAMETER appdata AS CHARACTER.
    DEFINE INPUT PARAMETER wparent AS WIDGET-HANDLE.

    DEFINE QUERY ordlineq FOR order-line, item.
    DEFINE BROWSE ordlineb QUERY ordlineq
        DISPLAY 
            order-line.line-num order-line.item-num item.item-name 
            order-line.price order-line.qty order-line.extended-price 
            order-line.discount order-line.backorder
        WITH 5 DOWN.
    DEFINE BUTTON bClose LABEL "Close Order Lines".
    DEFINE BUTTON bUpdate LABEL "Update Order Line".
    DEFINE VARIABLE whand AS WIDGET-HANDLE.
    DEFINE FRAME OrdlineFrame SKIP(.5)
        orderbuf.order-num COLON 12 VIEW-AS TEXT SKIP(.5)
        ordlineb SKIP
        bClose bUpdate
    WITH SIDE-LABELS SIZE-CHARS 98.8 by 8.5.
    
    ON CHOOSE OF bUpdate IN FRAME OrdlineFrame DO:
        IF ordlineb:NUM-SELECTED-ROWS >= 1 THEN DO:
            RUN update-order-line.
        END.
        ELSE DO:
            BELL.
            MESSAGE "Select an order line to update ...".
        END.
    END.  

(1) IF THIS-PROCEDURE:PERSISTENT THEN DO:
        THIS-PROCEDURE:PRIVATE-DATA = appdata + "," 
                                      + STRING(orderbuf.order-num).
        CREATE WIDGET-POOL.
    END. 
(2) CREATE WINDOW whand
        ASSIGN
            TITLE = "Order Lines for Order ..."
            PARENT = wparent
            RESIZE = FALSE
            SCROLL-BARS = FALSE
            HEIGHT-CHARS = FRAME OrdlineFrame:HEIGHT-CHARS
            WIDTH-CHARS = FRAME OrdlineFrame:WIDTH-CHARS.
    THIS-PROCEDURE:CURRENT-WINDOW = whand.

    OPEN QUERY ordlineq PRESELECT EACH order-line 
        WHERE order-line.order-num = orderbuf.order-num, EACH item
        WHERE item.item-num = order-line.item-num
        BY order-line.line-num.
    DISPLAY orderbuf.order-num WITH FRAME OrdlineFrame.
    ENABLE ALL WITH FRAME OrdlineFrame.

    IF THIS-PROCEDURE:PERSISTENT THEN DO:
        ON CHOOSE OF bClose IN FRAME OrdlineFrame DO:
            RUN destroy-query.
        END.
    END.
    ELSE DO: 
        WAIT-FOR CHOOSE OF bClose IN FRAME OrdlineFrame.
    END.

    PROCEDURE destroy-query:
        MESSAGE "Exiting order lines for order"
                STRING(orderbuf.order-num) "...".
(3)     DELETE PROCEDURE THIS-PROCEDURE NO-ERROR.
        DELETE WIDGET-POOL.
    END.

    PROCEDURE update-order-line:
        DEFINE VARIABLE rid AS ROWID.
        DEFINE VARIABLE choice AS LOGICAL.
        DEFINE BUTTON bSave LABEL "Save Changes".
        DEFINE BUTTON bCancel LABEL "Cancel".
    
(4)     DEFINE FRAME UpdateFrame SKIP(.5)
            orderbuf.order-num COLON 12 VIEW-AS TEXT 
                order-line.line-num VIEW-AS TEXT SKIP(1) SPACE(1)
            order-line.qty order-line.discount order-line.backorder
            SKIP(1)
            bSave bCancel        
        WITH TITLE "Update Order Line" SIDE-LABELS VIEW-AS DIALOG-BOX. 
        ON CHOOSE OF bSave IN FRAME UpdateFrame 
           OR GO OF FRAME UpdateFrame DO:
            MESSAGE "Are you sure you want to save your changes?"
                VIEW-AS ALERT-BOX QUESTION BUTTONS YES-NO
                UPDATE choice AS LOGICAL.
            CASE choice:
(5)             WHEN TRUE THEN DO TRANSACTION ON ERROR UNDO, RETRY:
                    rid = ROWID(order-line).
                    FIND order-line WHERE ROWID(order-line) 
                        = rid EXCLUSIVE-LOCK.
                    ASSIGN
                        order-line.qty
                        order-line.discount
                        order-line.backorder
                        order-line.extended-price = INPUT order-line.qty 
                            * order-line.price
                            * (1 - (INPUT order-line.discount * 0.01))
                    .
                    DISPLAY 
                        order-line.qty order-line.discount 
                        order-line.backorder order-line.extended-price
                    WITH BROWSE ordlineb.
                    APPLY "WINDOW-CLOSE" TO FRAME UpdateFrame.
                END.
                WHEN FALSE THEN DO:
                   MESSAGE "Changes not saved."
                       VIEW-AS ALERT-BOX INFORMATION BUTTONS OK.
                   RETURN NO-APPLY.
                END.
            END CASE.
        END.   
        ON CHOOSE OF bCancel DO:
            MESSAGE 
                "Are you sure you want to cancel your current updates?"
                VIEW-AS ALERT-BOX QUESTION BUTTONS YES-NO
                UPDATE choice AS LOGICAL.
            CASE choice:
                WHEN TRUE THEN DO:
                   MESSAGE "Current updates cancelled."
                       VIEW-AS ALERT-BOX INFORMATION BUTTONS OK.
                   APPLY "WINDOW-CLOSE" TO FRAME UpdateFrame.
                END.
                WHEN FALSE THEN DO:
                   MESSAGE "Current update is continuing ..."
                       VIEW-AS ALERT-BOX INFORMATION BUTTONS OK.
                END.
            END CASE.
        END.    
    
(6)     DISPLAY 
            orderbuf.order-num order-line.line-num order-line.qty 
            order-line.discount order-line.backorder
        WITH FRAME UpdateFrame IN WINDOW ACTIVE-WINDOW.
        ENABLE
            order-line.qty order-line.discount order-line.backorder
            bSave bCancel
        WITH FRAME UpdateFrame IN WINDOW ACTIVE-WINDOW.
    
        WAIT-FOR WINDOW-CLOSE OF FRAME UpdateFrame.
    END PROCEDURE. 

To help the user manage their screen, the application organizes the windows for orders and order lines into a window family, with the customer browse window as the root (p-perwn1.p (1), p-perwn2.p (3), p-perwn2.p (1), and p-perwn3.p (2)). Thus, the user can minimize the entire application or any order along with all of its child windows for order lines.

Another feature of this application is the use of dialog boxes to provide modal functionality in an otherwise non-modal, multi-window application (p-perwn2.p (7) and p-perwn3.p (4)). The p-perwn2.p and p-perwn3.p procedures each use a dialog box to update the order and order–lines table, respectively. This can help to control transaction size during an update. In these examples, the transaction size is limited to a single trigger block (p-perwn2.p (8) or p-perwn3.p (5)). So, the update windows in this case could also be persistently managed non-modal windows without affecting the scope of transactions.

Note also that these dialog boxes are parented to the window specified by the ACTIVE–WINDOW system handle (for example, p-perwn3.p (6)). This handle specifies the window that has received the most recent input focus in the application. Using this handle guarantees that the dialog box appears in the Progress window where the user is working, even if it is not the current window of the procedure that displays the dialog box. For more information on the ACTIVE–WINDOW handle, see Interface Design."

NOTE: This example updates the qty and extended–price field in the order–line table. However, it does not update the corresponding balance field in the customer table, or the on–hand and allocated fields in the item table. You might want to add the necessary update code to maintain your working sports database. For example, you could update the customer balance field by passing the procedure handle of p-perwn1.p down to p-perwn3.p and calling an internal procedure in p-perwn1.p that updates the balance field in the customer record specified by orderbuf.cust–num (in p-perwn3.p). Calling an internal procedure in p-perwn1.p keeps the customer table and browse management all together in p-perwn1.p.


Copyright © 2004 Progress Software Corporation
www.progress.com
Voice: (781) 280-4000
Fax: (781) 280-4095