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:

  1. Define the cFieldHandles variable 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.
  2. Write a block of code to execute whenever a new Order is selected. This is the VALUE-CHANGED event for the browse, which you used in an earlier variation of this procedure.

    The code in the VALUE-CHANGED trigger 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 their SCREEN-VALUE to 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.
  3. Define a LEAVE trigger 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.  
  4. 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 cFieldsHandle variable:
      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-ERROR qualifier on the DELETE OBJECT statement 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 LEAVE trigger for the selection list, the field’s SCREEN-VALUE attribute 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.

  5. Save this value in a variable to keep the rest of the code from having to refer to the SCREEN-VALUE attribute over and over again:
      cFields = OLineFields:SCREEN-VALUE.
  6. Add a block to iterate through all the selections. You saw earlier how the BUFFER-FIELD attribute 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).
  7. 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 CREATE statement parents it to the frame, sets its data type, calculates a format and value for it using the LABEL attribute of the current buffer field, and positions it in the frame. The HEIGHT-CHARS of 1 makes the label text align properly with the value displayed next to it. The COLUMN positions it next to the browse, and the row is incremented each time through the loop to define a distinct position for each field.

  8. 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-HANDLE attribute connects this fill-in to its handle object. The COLUMN setting allows room for the label before displaying the field value. The SCREEN-VALUE assigns the value from the buffer field’s BUFFER-VALUE attribute. The HIDDEN attribute makes sure the field is viewed along with the frame that contains it.

  9. 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 */ 
  10. Make sure that the VALUE-CHANGED trigger 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.
  11. 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.
  12. 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 Z character to format leading zeros. The Z tells 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.