Whenever your code is done using a persistent procedure, you must remember to delete it to clean up after yourself. Use this statement:
DELETE PROCEDURE procedure-handle [ NO-ERROR ].
The NO-ERROR option suppresses any error message, for example, if the procedure has already been deleted elsewhere. You can also use the VALID-HANDLE function to check whether a handle is still valid or not, such as in this example:
IF VALID-HANDLE(hProc) THEN
  DELETE PROCEDURE hProc.

When you close one of the Order windows by clicking the close icon in the right-hand corner, the window disappears. Is its procedure really gone? Yes, but this does not happen automatically. It is very important for you to make sure that you remember to clean up persistent procedures when your application is done with them, if the AppBuilder does not do it for you.

In this case, the AppBuilder generated just the code you need to make sure the procedure is deleted when you close the window. You might remember this code from Introduction to the OpenEdge AppBuilder but it should mean a lot more to you now because you understand about the THIS-PROCEDURE handle, the PERSISTENT attribute, and procedure handles. The code is in the disable_UI internal procedure. To review, here is the sequence again:

  1. The main block, which executes as soon as the procedure starts up, sets up a trigger to RUN the internal procedure disable_UI on the CLOSE event:
    /* The CLOSE event can be used from inside or outside the procedure to */
    /* terminate it. */
    ON CLOSE OF THIS-PROCEDURE
      RUN disable_UI.
  2. The AppBuilder generates code for the WINDOW-CLOSE event of the window, which is what fires when you click the Close icon in the window:
    DO:
      /* This event will close the window and terminate the procedure. */
      APPLY "CLOSE":U TO THIS-PROCEDURE.
      RETURN NO-APPLY.
    END.
  3. The APPLY "CLOSE" statement sets off the CLOSE event for the procedure handle itself. This runs disable_UI, which not only deletes the window with the DELETE WIDGET statement but also has the statement to delete the procedure, which might have looked cryptic when you first saw it, but which you should fully understand now:
      /* Delete the WINDOW we created */
      IF SESSION:DISPLAY-TYPE = "GUI":U AND VALID-HANDLE(C-Win)
      THEN DELETE WIDGET C-Win.
      IF THIS-PROCEDURE:PERSISTENT THEN DELETE PROCEDURE THIS-PROCEDURE.
    END PROCEDURE.
  4. The PERSISTENT attribute on the procedure handle identifies it as a persistent procedure, and the DELETE PROCEDURE statement deletes it by identifying its handle, THIS-PROCEDURE.

If the AppBuilder did not do this for you, the procedure and all its memory and other resources would sit around until your session ended. This might create quite a mess, not just because of the memory it uses, but because of the possibility of records it might be holding and any other resources that could cause errors or contention in your application. The worst part is that the window itself would be gone, so you would have no visual clue that the procedure is still there.

To test your application so you can see what procedures are in memory:

  1. Run h-CustOrderWin6.w.
  2. Select several Orders to display in their window.
  3. Select the Procedures icon from the PRO*Tools palette:

    The Procedure Object Viewer window appears:

    This tool shows that you have the main window and three copies of h-OrderWin.w running. Remember that whenever you run a procedure from the AppBuilder, it saves it to a temporary file and runs the temporary file, so you see a temporary filename such as p41539_h-CustOrderWin6.ab in your temporary file directory instead of its actual procedure filename h-CustOrderWin6.w.

    The Procedure Object Viewer window has a host of useful information in it, including all the INTERNAL-ENTRIES for any procedure you select from the list. You can also run any internal entry by clicking the Run entry button.

    If you want to get rid of a running procedure from here, you can click the Delete button. If it is an AppBuilder-generated procedure such as those you are looking at, you can also click the Apply “Close” button, which should execute all the proper cleanup code for the procedure that is associated with the CLOSE event.

  4. Close the Customers and Orders window and select View > Refresh List of Procedure Objects in the Procedure Object Viewer.

You should see that all the running procedures, including both the main window and any Order windows you have open, are gone. So, are you done cleaning up?

Not yet! The AppBuilder is doing you another favor here, and it is one that is rather dangerous, because if you forget to test your application outside the AppBuilder, you might not see that you still have work to do. When you click Stop, the AppBuilder deletes any persistent procedures that were started since you first chose the Run button, to make it easier for you to test parts of applications over and over. But when you run your application from outside the OpenEdge tools, you need to take care of your own cleanup. If you were to run h-CustOrderWin6.w from another procedure, open several Order windows, and then close h-CustOrderWin6, the Order windows (and their procedures, of course) would still be running.

To add code to delete those procedures when the main window that starts them up is deleted:

  1. Go into the Definitions section for h-CustOrderWin6.w and add these definitions to the end of the section:
    DEFINE VARIABLE cOrderHandles AS CHARACTER NO-UNDO.
    DEFINE VARIABLE iHandle AS INTEGER NO-UNDO.
    DEFINE VARIABLE hOrderWindow AS HANDLE NO-UNDO.

    Your new code uses these variables to save off a list of all the procedure handles of the procedures you create, and then later walks through them and deletes them.

  2. Add another line to the CHOOSE trigger for the BtnOrder/Order Detail button to save off the new procedure handle in a list:
    DO:
      DEFINE VARIABLE hOrder AS HANDLE NO-UNDO.
      RUN h-OrderWin.w PERSISTENT SET hOrder (BUFFER Order).
      cOrderHandles = cOrderHandles +
        (IF cOrderHandles NE "" THEN "," ELSE "") + STRING(hOrder).
    END.

    To save a list of handles, you need to turn each handle into a STRING. The commas act as a delimiter between entries in the list. The IF-THEN-ELSE clause inside parentheses adds a comma only if there is already something in the list.

  3. Go into the main block and add some code just before it runs disable_UI to clean up those procedure handles:
    ON CLOSE OF THIS-PROCEDURE
    DO:
      /* Cleanup any OrderLine windows that might still be open. */
      DO iHandle = 1 TO NUM-ENTRIES(cOrderHandles):
        hOrderWindow = WIDGET-HANDLE(ENTRY(iHandle, cOrderHandles)).
        IF VALID-HANDLE (hOrderWindow) THEN
          APPLY "CLOSE" TO hOrderWindow.
      END.
    
      RUN disable_UI.
    
    END.

This DO block turns each handle value back into the right data type using the WIDGET-HANDLE function. (The function you use is the same one for all handles, even though this is a procedure handle, not a widget handle, for a visual object like a button.)

You could execute the DELETE PROCEDURE hOrderWindow NO-ERROR statement. The NO-ERROR option would let you use the DELETE PROCEDURE statement without bothering to check whether the handle is still valid. Remember that the user might or might not have closed it on his own.

But because the h-OrderWin.w procedure has special code to process the CLOSE event and do additional types of cleanup, it is always better to APPLY "CLOSE" to an AppBuilder-generated procedure window or any other procedure that uses the same convention. The VALID-HANDLE check makes sure that the window has not already been closed and the procedure deleted.