Storing a list of handles
- Last Updated: July 22, 2025
- 6 minute read
- OpenEdge
- Version 13.0
- Documentation
You could store the object handles in a HANDLE variable array that has an
EXTENT, but this is almost certainly a bad idea. The first rule of
using a variable with an extent is that you should do it only when the proper value for
the extent is clear, based on the nature of the data it is holding, such as values for
the seven days in a week or the twelve months in a year. If you just try to pick a value
that seems big enough, you may regret it later when that number turns out to be too
small for some case you did not anticipate.
The method used in the example is just to store the handles in a list, in character form. For a modest number of values, this is quite reasonable, and the conversion effort back and forth between a handle and its character representation is not significant.
Always keep in mind the alternative of using a temp-table to store a set of values during
program execution. Although the overhead of having to perform a FIND on
what amounts to a special database table may seem significant, in fact temp-tables are
extremely fast. Most or all of the records you need to work with are likely in memory
anyway and, with the ability to index fields that you need to retrieve or filter on,
even a large temp-table should provide very good performance. A temp-table is well
suited to situations where the number of possible values you need to keep track of can
grow large. How large is large? There is no precise answer to this, but it is probably a
good rule of thumb that if you are storing more than a few dozen values, it is cleaner
and possibly faster to use a temp-table. A temp-table is also the right choice when you
need to store several related pieces of information for each item, each of which can
become a field in the temp-table definition.
For this example you simply use a character variable. Its value needs to persist for the life of the procedure, because the handles are saved off by one internal procedure or trigger block and used by another.
To create dynamic fields in the sample window:
- Define the
cFieldHandlesvariable in the Definitions section of h-CustOrderWin7.w, which scopes the variable definition to the whole procedure:/* Local Variable Definitions --- */ DEFINE VARIABLE cFieldHandles AS CHARACTER NO-UNDO. - Write a block of code to execute whenever a new Order is
selected. This is the
VALUE-CHANGEDevent for the browse, which you used in an earlier variation of this procedure.The code in the
VALUE-CHANGEDtrigger needs to find the first OrderLine for the Order. For the sake of simplicity, the example does not navigate through all the OrderLines, but you could easily extend it to do this. Then, it looks at the existing list of dynamic field handles (if any) and clears them out by setting theirSCREEN-VALUEto blank:/* ON VALUE-CHANGED OF OrderBrowse */ DO: DEFINE VARIABLE iField AS INTEGER NO-UNDO. DEFINE VARIABLE hField AS HANDLE NO-UNDO. FIND FIRST OrderLine OF Order. DO iField = 2 TO NUM-ENTRIES(cFieldHandles) BY 2: hField = WIDGET-HANDLE(ENTRY(iField, cFieldHandles)). IF VALID-HANDLE(hField) THEN hField:SCREEN-VALUE = "". END. END. - Define a
LEAVEtrigger for the OLineFields selection list. The trigger uses these variables:DEFINE VARIABLE iField AS INTEGER NO-UNDO. DEFINE VARIABLE hField AS HANDLE NO-UNDO. DEFINE VARIABLE hLabel AS HANDLE NO-UNDO. DEFINE VARIABLE cFields AS CHARACTER NO-UNDO. DEFINE VARIABLE cField AS CHARACTER NO-UNDO. DEFINE VARIABLE hBufField AS HANDLE NO-UNDO. DEFINE VARIABLE dRow AS DECIMAL NO-UNDO INIT 10.0. - To allow for the case where this is not the first time the user has selected a list
of fields, add code that first deletes the existing fields using their object
handles, which are stored in a list in the
cFieldsHandlevariable:DO iField = 1 TO NUM-ENTRIES(cFieldHandles): hField = WIDGET-HANDLE(ENTRY(iField,cFieldHandles)). DELETE OBJECT hField NO-ERROR. END.Remember that if you neglect to do this, each new request would add more objects to the session that are not being used anymore. The
NO-ERRORqualifier on theDELETE OBJECTstatement simply suppresses any error message in the event that the object has already been deleted in some other way.How about when the procedure is terminated? Do you need code to delete the dynamic fields that are around at that time to prevent a memory leak? The answer is no, but only because of the widget pool created in the Definitions section, which cleans up all dynamic objects created by the procedure when the procedure terminates. That is why the widget pool convention is so valuable. Without the widget pool created for the procedure, you could leave dynamic objects in memory for the duration of the session, even after the procedure exits.
Since this code is the
LEAVEtrigger for the selection list, the field’sSCREEN-VALUEattribute holds the value the user selected. In the case of a multiple-selection list such as this, the value is actually a comma-separated list of all the entries the user selected. - Save this value in a variable to keep the rest of the code from having to refer to
the
SCREEN-VALUEattribute over and over again:cFields = OLineFields:SCREEN-VALUE. - Add a block to iterate through all the selections. You saw earlier how the
BUFFER-FIELDattribute on a buffer handle can take the ordinal position of the field in the buffer as an identifier. You can also pass the field name, as the code does here. Once you retrieve the handle of the selected field, the code can query a number of different field attributes through that handle:DO iField = 1 TO NUM-ENTRIES(cFields): ASSIGN cField = ENTRY(iField, cFields) hBufField = BUFFER OrderLine:BUFFER-FIELD(cField). - Create the text label for the fill-in. As you learned earlier, the label must be a
separate text object:
CREATE TEXT hLabel ASSIGN FRAME = FRAME CustQuery:HANDLE DATA-TYPE = "CHARACTER" FORMAT = "X(" + STRING(LENGTH(hBufField:LABEL) + 1) + ")" SCREEN-VALUE = hBufField:LABEL + ":" HEIGHT-CHARS = 1 ROW = dRow COLUMN = 85.0.The
CREATEstatement parents it to the frame, sets its data type, calculates a format and value for it using theLABELattribute of the current buffer field, and positions it in the frame. TheHEIGHT-CHARSof 1 makes the label text align properly with the value displayed next to it. TheCOLUMNpositions it next to the browse, and the row is incremented each time through the loop to define a distinct position for each field. - Create the fill-in object itself:
CREATE FILL-IN hField ASSIGN DATA-TYPE = hBufField:DATA-TYPE FORMAT = hBufField:FORMAT FRAME = FRAME CustQuery:HANDLE SIDE-LABEL-HANDLE = hLabel COLUMN = 85.0 + LENGTH(hBufField:LABEL) + 4 ROW = dRow SCREEN-VALUE = hBufField:BUFFER-VALUE HIDDEN = NO.The data type, format, and value all come from the buffer field object handle. The
SIDE-LABEL-HANDLEattribute connects this fill-in to its handle object. TheCOLUMNsetting allows room for the label before displaying the field value. TheSCREEN-VALUEassigns the value from the buffer field’sBUFFER-VALUEattribute. TheHIDDENattribute makes sure the field is viewed along with the frame that contains it. - Increment the row counter to set the position of the next field, and save off the
handles of the labels and fill-ins in a list:
ASSIGN dRow = dRow + 1.0 cFieldHandles = cFieldHandles + (IF cFieldHandles = "" THEN "" ELSE ",") + STRING(hLabel) + "," + STRING(hField). END. /* END DO iField */ - Make sure that the
VALUE-CHANGEDtrigger for the Order browse fires whenever a different record is displayed. This includes when the procedure first starts up, so make this addition to the main block:MAIN-BLOCK: DO ON ERROR UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK ON END-KEY UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK: RUN enable_UI. RUN h-StartSuper.p("h-dynsuper.p"). RUN changeFields. APPLY "VALUE-CHANGED" TO OrderBrowse. RUN initSelection. IF NOT THIS-PROCEDURE:PERSISTENT THEN WAIT-FOR CLOSE OF THIS-PROCEDURE. END. - Make the same addition (
APPLY "VALUE-CHANGED" TO OrderBrowse.) to each of the navigation button triggers, as you did earlier in Add APPLY statements to the procedure:... {&OPEN-BROWSERS-IN-QUERY-CustQuery} APPLY "VALUE-CHANGED" TO OrderBrowse. END. - Run the window. Now you can select one or more fields from
the selection list, tab out of it, and see those fields displayed as dynamic
fill-ins with dynamic labels next to the browse:
If a few of the fields seem to be positioned rather far to the right (the Order Num for instance), it is because they are right-justified numeric fields with overly generous display formats as defined in the Data Dictionary. Specifically, the OrderNum and ItemNum fields are defined in the schema with a long format that uses the
Zcharacter to format leading zeros. TheZtells the AVM to replace leading zeros with spaces, which pushes the displayed value out to the right. Others, such as the Price, are formatted with the>character, which tells the AVM to suppress leading zeros, effectively left-justifying the value. This is just a result of the formatting choices made by the database designer and has nothing to do with the display of dynamic values.