Back to Summary


Tutorial Chapter 13: Master/Detail using Multiple Dialogs

Summary:


The Master-Detail sample

The example discussed in this chapter is designed for the input of order information (headers and order lines), illustrating a typical master-detail relationship. The form used by the example contains fields from both the orders and items tables in the custdemo database. The result is very similar to the example of chapter 11. However, in this program the end user can input order and items data simultaneously, because the form is driven by a DIALOG instruction.

When the program starts, the existing rows from the orders and items tables have already been retrieved and are displayed on the form.  The user can browse through the orders and items to update or delete them, add new orders or items, and search for specific orders by entering criteria in the form. 

Display on Windows platforms

There are different ways to implement a Master/Detail form with multiple dialogs. This chapter shows one of them. Genero provides the basics bricks, then it's up to you to adapt the programming pattern, according to the ergonomics you want to expose to the end user.


The Customer List Form

The custlist.per form defines a typical 'zoom' form with a filter field and record list where the user can pick an element to be used in a field of the main form.  Using this form, the user can scroll through the list to pick a store, or can enter query criteria to filter the list prior to picking.

The fields that make up the columns of the table that display the list are defined as FORMONLY fields. When TYPE is not defined, the default data type for FORMONLY fields is CHAR.

Form custlist.per
001 SCHEMA custdemo
002 
003 LAYOUT
004 GRID
005 {
006 <g g1                                        >
007  Store name: [fc                      :fe   ]
008 <                                            >
009 <t t1                                        >
010   Id   Name                  City
011  [f01 |f02                  |f03            ]
012  [f01 |f02                  |f03            ]
013  [f01 |f02                  |f03            ]
014  [f01 |f02                  |f03            ]
015 <                                            >
016 }
017 END
018 END
019 
020 TABLES
021    customer
022 END
023 
024 ATTRIBUTES
025 GROUP g1: TEXT="Filter";
026 EDIT fc = customer.store_name;
027 BUTTON fe: fetch, IMAGE="filter";
028 EDIT f01=FORMONLY.s_num;
029 EDIT f02=FORMONLY.s_name;
030 EDIT f03=FORMONLY.s_city;
031 END
032 
033 INSTRUCTIONS
034 SCREEN RECORD sa_cust (FORMONLY.*);
035 END

Notes:


The Customer List Module

The custlist.4gl module defines a 'zoom' module, to let the user select a customer from a list. The module could be re-used for any application that requires the user to select a customer from a list. 

This module uses the custlist.per form and is implemented with a DIALOG instruction defining a CONSTRUCT sub-dialog and a DISPLAY ARRAY sub-dialog. The display_custlist() function in this module returns the customer id and the name.

In the application illustrated in this chapter, the main module orders.4gl will call the display_custlist() function to retrieve a customer selected by the user.

01   ON ACTION zoom1
02      CALL display_custlist() RETURNING id, name
03      IF (id > 0) THEN
04         ...

Here is the complete source code of the custlist.4gl module:

Module custlist.4gl
001 SCHEMA custdemo
002
003 TYPE cust_t RECORD
004            store_num     LIKE customer.store_num,
005            store_name    LIKE customer.store_name,
006            city          LIKE customer.city
007        END RECORD
008 
009 DEFINE cust_arr DYNAMIC ARRAY OF cust_t
010 
011 FUNCTION custlist_fill(where_clause)
012   DEFINE where_clause STRING
013   DEFINE idx SMALLINT
014   DEFINE cust_rec cust_t
015 
016   DECLARE custlist_curs CURSOR FROM
017     "SELECT store_num, store_name, city "||
018     "  FROM customer"||
019     "  WHERE "||where_clause||
020     " ORDER BY store_num"
021 
022   LET idx = 0
023   CALL cust_arr.clear()
024   FOREACH custlist_curs INTO cust_rec.*
025     LET idx = idx + 1
026     LET cust_arr[idx].* = cust_rec.*
027   END FOREACH
028 
029 END FUNCTION
030   
031 FUNCTION display_custlist()
033   DEFINE ret_num LIKE customer.store_num
034   DEFINE ret_name LIKE customer.store_name
035   DEFINE where_clause STRING
036   DEFINE idx SMALLINT
037 
038   OPEN WINDOW wcust WITH FORM "custlist"
039 
040   LET ret_num = 0
041   LET ret_name = NULL
042 
043   DIALOG ATTRIBUTES(UNBUFFERED)
044 
045      CONSTRUCT BY NAME where_clause ON customer.store_name
046      END CONSTRUCT
047 
048      DISPLAY ARRAY cust_arr TO sa_cust.* 
049      END DISPLAY
050 
051      BEFORE DIALOG
052         CALL custlist_fill("1 = 1")
053 
054      ON ACTION fetch
055         CALL custlist_fill(where_clause)
056 
057      ON ACTION accept
058         LET idx = DIALOG.getCurrentRow("sa_cust")
059         IF idx > 0 THEN
060            LET ret_num = cust_arr[idx].store_num
061            LET ret_name = cust_arr[idx].store_name
062            EXIT DIALOG
063         END IF
064 
065      ON ACTION cancel
066         EXIT DIALOG
067 
068   END DIALOG
069   
070   CLOSE WINDOW wcust
071 
072   RETURN ret_num, ret_name 
073 
074 END FUNCTION

Notes:


The Orders Form

The form specification file orderform.per defines a form for the orders program, and displays fields containing the values of a single order from the orders table.  The name of the store is retrieved from the customer table, using the column store_num, and displayed.

A screen array displays the associated rows from the items table.  Although order_num is also one of the fields in the items table, it does not have to be included in the screen array or in the screen record, since the order number will be the same for all the items displayed for a given order.  For each item displayed in the screen array,  the values in the description and unit columns from the stock table are also displayed.

The values in FORMONLY fields are not retrieved from a database; they are calculated by the BDL program based on the entries in other fields.  In this form FORMONLY fields are used to display the calculations made by the BDL program for item line totals and the order total. Their data type is defined as DECIMAL.

This form uses some of the attributes that can be assigned to fields in a form. See Form Specification Files Attributes for a complete list of the available attributes.

The form defines a toolbar and a topmenu. The decoration of toolbar or topmenu action views is centralized in an Action Defaults section.

Form orderform.per
001 SCHEMA custdemo
002 
003 ACTION DEFAULTS
004   ACTION find (TEXT="Find", IMAGE="find", COMMENT="Query database")
005   ACTION new (TEXT="New", IMAGE="new", COMMENT="New order")
006   ACTION save (TEXT="Save", IMAGE="disk", COMMENT="Check and save order info")
007   ACTION append (TEXT="Line", IMAGE="new", COMMENT="New order line")
008   ACTION delete (TEXT="Del", IMAGE="eraser", COMMENT="Delete current order line")
009   ACTION first (TEXT="First", COMMENT="Move to first order in list")
010   ACTION previous (TEXT="Prev", COMMENT="Move to previous order in list")
011   ACTION next (TEXT="Next", COMMENT="Move to next order in list")
012   ACTION last (TEXT="Last", COMMENT="Move to last order in list")
013   ACTION quit (TEXT="Quit", COMMENT="Exit the program", IMAGE="quit")
014 END
015
016 TOPMENU
017   GROUP ord (TEXT="Orders")
018     COMMAND find
019     COMMAND new
020     COMMAND save
021     SEPARATOR
022      COMMAND quit
023   END
024   GROUP ord (TEXT="Items")
025     COMMAND append
026     COMMAND delete
027   END
028   GROUP navi (TEXT="Navigation")
029     COMMAND first
030     COMMAND previous
031     COMMAND next
032     COMMAND last
033   END
034   GROUP help (TEXT="Help")
035     COMMAND about (TEXT="About")
036   END
037 END
038
039 TOOLBAR
040   ITEM find
041   ITEM new
042   ITEM save
043   SEPARATOR
044   ITEM append
045   ITEM delete
046   SEPARATOR
047   ITEM first
048   ITEM previous
049   ITEM next
050   ITEM last
051   SEPARATOR
052   ITEM quit
053 END
054 
055 LAYOUT 
056 VBOX
057 GROUP
058 GRID
059 {
060   Store #:[f01  ] [f02                                          ]
061   Order #:[f03  ]  Order Date:[f04         ] Ship By:[f06       ]
062   Factory:[f05  ]             [f07                              ]
063                                      Order Total:[f14           ]
064 }
065 END
066 END -- GROUP
067 TABLE
068 {
069  Stock#  Description       Qty     Unit    Price       Total
070 [f08    |f09              |f10    |f11    |f12        |f13      ]
071 [f08    |f09              |f10    |f11    |f12        |f13      ]
072 [f08    |f09              |f10    |f11    |f12        |f13      ]
073 [f08    |f09              |f10    |f11    |f12        |f13      ]
074 }
075 END
076 END
077 END
078 
079 TABLES
080   customer, orders, items, stock
081 END
082 
083 ATTRIBUTES
084  BUTTONEDIT f01 = orders.store_num, REQUIRED, ACTION=zoom1;
085  EDIT       f02 = customer.store_name, NOENTRY;
086  EDIT       f03 = orders.order_num, NOENTRY;
087  DATEEDIT   f04 = orders.order_date;
088  EDIT       f05 = orders.fac_code, UPSHIFT;
089  EDIT       f06 = orders.ship_instr;
090  CHECKBOX   f07 = orders.promo, TEXT="Promotional",
091                  VALUEUNCHECKED="N", VALUECHECKED="Y";
092  BUTTONEDIT f08 = items.stock_num, REQUIRED, ACTION=zoom2;
093  LABEL      f09 = stock.description;
094  EDIT       f10 = items.quantity, REQUIRED;
095  LABEL      f11 = stock.unit;
096  LABEL      f12 = items.price;
097  LABEL      f13 = formonly.line_total TYPE DECIMAL(9,2);
098  EDIT       f14 = formonly.order_total TYPE DECIMAL(9,2), NOENTRY;
099 END
100 
101 INSTRUCTIONS
102 SCREEN RECORD sa_items(
103   items.stock_num,
104   stock.description,
105   items.quantity,
106   stock.unit,
107   items.price,
108   line_total
109 )
110 END

Notes:


The Orders Program orders.4gl

The orders.4gl source implements the main form controller. Most of the functionality has been described in previous chapters. In this section we will only focus on the DIALOG instruction programming. The program implements a DIALOG instruction, including an INPUT BY NAME sub-dialog for the order fields input, and an INPUT ARRAY sub-dialog for the items input.

Unlike traditional 4GL programs using singular dialogs, you typically start the program in the multiple dialog instruction, eliminating the global MENU instruction.


Module variables of orders.4gl

The module variables listed below are used by the orders.4gl module.

Module variables of orders.4gl
001 SCHEMA custdemo
002
003 TYPE order_t RECORD
004            store_name   LIKE customer.store_name,
005            order_num    LIKE orders.order_num,
006            order_date   LIKE orders.order_date,
007            fac_code     LIKE orders.fac_code,
008            ship_instr   LIKE orders.ship_instr,
009            promo        LIKE orders.promo
010       END RECORD,
011       item_t RECORD
012            stock_num    LIKE items.stock_num,
013            description  LIKE stock.description,
014            quantity     LIKE items.quantity,
015            unit         LIKE stock.unit,
016            price        LIKE items.price,
017            line_total   DECIMAL(9,2)
018       END RECORD
019 
020 DEFINE order_rec order_t,
021      arr_ordnums DYNAMIC ARRAY OF INTEGER,
022      orders_index INTEGER,
023      arr_items DYNAMIC ARRAY OF item_t,
024      order_total DECIMAL(9,2)
025 
026 CONSTANT title1 = "Orders"
027 CONSTANT title2 = "Items"
028 
029 CONSTANT msg01 = "You must query first"
030 CONSTANT msg02 = "Enter search criteria"
031 CONSTANT msg03 = "Canceled by user"
032 CONSTANT msg04 = "No rows found, enter new search criteria"
033 CONSTANT msg05 = "End of list"
034 CONSTANT msg06 = "Beginning of list"
035 CONSTANT msg07 = "Invalid stock number"
036 CONSTANT msg08 = "Row added to the database"
037 CONSTANT msg09 = "Row updated in the database"
038 CONSTANT msg10 = "Row deleted from the database"
039 CONSTANT msg11 = "New order record created"
040 CONSTANT msg12 = "This customer does not exist"
041 CONSTANT msg13 = "Quantity must be greater than zero"
042 CONSTANT msg14 = "%1 orders found in the database"
043 CONSTANT msg15 = "There are no orders selected, exit program?"
044 CONSTANT msg16 = "Item is not available in current factory %1"
045 CONSTANT msg17 = "Order %1 saved in database"
046 CONSTANT msg18 = "Order input program, version 1.01"
047 CONSTANT msg19 = "To save changes, move focus to another row or to the order header"
048 
049 CONSTANT move_first = -2
050 CONSTANT move_prev  = -1
051 CONSTANT move_next  = 1
052 CONSTANT move_last  = 2

Notes:


Function orditems_dialog

This is the most important function of the program. It implements the multiple dialog instruction to control order and items input simultaneously.

The function uses the opflag variable to determine the state of the operations for items:

Function orditems_dialog  (orders.4gl)
001 FUNCTION orditems_dialog()
002   DEFINE query_ok SMALLINT,
003          id INTEGER,
004          name LIKE customer.store_name,
005          opflag CHAR(1),
006          curr_pa INTEGER
007 
008   DIALOG ATTRIBUTES(UNBUFFERED)
009 
010    INPUT BY NAME order_rec.*, order_total
011      ATTRIBUTES(WITHOUT DEFAULTS, NAME="order")
012 
013     ON ACTION find
014        IF NOT order_update(DIALOG) THEN NEXT FIELD CURRENT END IF
015        CALL order_query()
016 
017     ON ACTION new
018        IF NOT order_update(DIALOG) THEN NEXT FIELD CURRENT END IF
019        IF NOT order_new() THEN
020           EXIT PROGRAM
021        END IF
022 
023     ON ACTION save
024        IF NOT order_update(DIALOG) THEN NEXT FIELD CURRENT END IF
025 
026     ON CHANGE store_num
027        IF NOT order_check_store_num() THEN NEXT FIELD CURRENT END IF
028 
029     ON ACTION zoom1
030        CALL display_custlist() RETURNING id, name
031        IF id > 0 THEN
032           LET order_rec.store_num = id
033           LET order_rec.store_name = name
034           CALL DIALOG.setFieldTouched("store_num", TRUE)
035        END IF
036 
037     AFTER INPUT
038        IF NOT order_update(DIALOG) THEN NEXT FIELD CURRENT END IF
039 
040     ON ACTION first
041        IF NOT order_update(DIALOG) THEN NEXT FIELD CURRENT END IF
042        CALL order_move(move_first)
043     ON ACTION previous
044        IF NOT order_update(DIALOG) THEN NEXT FIELD CURRENT END IF
045        CALL order_move(move_prev)
046     ON ACTION next
047        IF NOT order_update(DIALOG) THEN NEXT FIELD CURRENT END IF
048        CALL order_move(move_next)
049     ON ACTION last
050        IF NOT order_update(DIALOG) THEN NEXT FIELD CURRENT END IF
051        CALL order_move(move_last)
052 
053   END INPUT
054 
055   INPUT ARRAY arr_items FROM sa_items.*
056     ATTRIBUTES (WITHOUT DEFAULTS, INSERT ROW = FALSE)
057 
058     BEFORE INPUT
059       MESSAGE msg19
060 
061     BEFORE ROW
062       LET opflag = "N"
063       LET curr_pa = DIALOG.getCurrentRow("sa_items")
064       CALL DIALOG.setFieldActive("stock_num", FALSE)
065 
066     BEFORE INSERT
067       LET opflag = "T"
068       LET arr_items[curr_pa].quantity = 1
069       CALL DIALOG.setFieldActive("stock_num", TRUE)
070 
071     AFTER INSERT
072       LET opflag = "I"
073 
074     BEFORE DELETE
075       IF opflag="N" THEN
076          IF NOT item_delete(curr_pa) THEN
077             CANCEL DELETE
078          END IF
079       END IF
080 
081     AFTER DELETE
082       LET opflag="N"
083 
084     ON ROW CHANGE 
085       IF opflag != "I" THEN LET opflag = "M" END IF
086 
087     AFTER ROW
088       IF opflag == "I" THEN
089          IF NOT item_insert(curr_pa) THEN
090             NEXT FIELD CURRENT
091          END IF
092          CALL items_line_total(curr_pa)
093       END IF
094       IF opflag == "M" THEN
095          IF NOT item_update(curr_pa) THEN
096             NEXT FIELD CURRENT
097          END IF
098          CALL items_line_total(curr_pa)
099       END IF
100 
101     ON ACTION zoom2
102        LET id = display_stocklist()
103        IF id > 0 THEN
104           IF NOT get_stock_info(curr_pa,id) THEN
105              LET arr_items[curr_pa].stock_num = NULL
106           ELSE
107              LET arr_items[curr_pa].stock_num = id
108           END IF
109           CALL DIALOG.setFieldTouched("stock_num", TRUE)
110        END IF
111 
112     ON CHANGE stock_num
113        IF NOT get_stock_info(curr_pa,
114                   arr_items[curr_pa].stock_num) THEN
115           LET arr_items[curr_pa].stock_num = NULL
116           CALL __mbox_ok(title2,msg07,"stop")
117           NEXT FIELD stock_num
118        ELSE
119           CALL items_line_total(curr_pa)
120        END IF
121 
122     ON CHANGE quantity
123        IF arr_items[curr_pa].quantity <= 0 THEN
124           CALL __mbox_ok(title2,msg13,"stop")
125           NEXT FIELD quantity
126        ELSE
127           CALL items_line_total(curr_pa)
128        END IF
129 
130   END INPUT
131 
132   BEFORE DIALOG
133      IF NOT order_select("1=1") THEN
134        CALL order_query()
135      END IF
136 
137   ON ACTION about
138      CALL __mbox_ok(title1,msg18,"information")
139 
140   ON ACTION quit
141      EXIT DIALOG
142 
143   END DIALOG
144 
145 END FUNCTION

Notes:


Function order_update

This function validates that the values in the order_rec program record are correct, and then executes an SQL statement to update the row in the orders database table.

Function order_update  (orders.4gl)
01 FUNCTION order_update(d)
02   DEFINE d ui.Dialog
03
04   IF NOT order_validate(d) THEN RETURN FALSE END IF
05
06   WHENEVER ERROR CONTINUE
07   UPDATE orders SET
08           store_num  = order_rec.store_num,
09           order_date = order_rec.order_date,
10           fac_code   = order_rec.fac_code,
11           ship_instr = order_rec.ship_instr,
12           promo      = order_rec.promo
13     WHERE orders.order_num = order_rec.order_num
14   WHENEVER ERROR STOP
15
16   IF SQLCA.SQLCODE <> 0 THEN
17     CALL __mbox_ok(title1,SQLERRMESSAGE,"stop")
18     RETURN FALSE
19   END IF
20
21   CALL d.setFieldTouched("orders.*", FALSE)
22   MESSAGE SFMT(msg17, order_rec.order_num)
23
24   RETURN TRUE
25
26 END FUNCTION

Notes:


Function order_new

This function inserts a new row in the database table orders, using the values from the order_rec program record.

Function order_new (orders.4gl)
01 FUNCTION order_new()
02   SELECT MAX(order_num)+1 INTO order_rec.order_num
03     FROM orders
04   IF order_rec.order_num IS NULL
05    OR order_rec.order_num == 0 THEN
06      LET order_rec.order_num = 1
07   END IF
08   LET order_total = 0
09   -- We keep the same store...
10   LET order_rec.order_date = TODAY
11   LET order_rec.fac_code = "ASC"
12   LET order_rec.ship_instr = "FEDEX"
13   LET order_rec.promo = "N"
14
15   WHENEVER ERROR CONTINUE
16   INSERT INTO orders (
17      store_num,
18      order_num,
19      order_date,
20      fac_code,
21      ship_instr,
22      promo
23   ) VALUES (
24      order_rec.store_num,
25      order_rec.order_num,
26      order_rec.order_date,
27      order_rec.fac_code,
28      order_rec.ship_instr,
29      order_rec.promo
30   )
31   WHENEVER ERROR STOP
32   IF SQLCA.SQLCODE <> 0 THEN
33      CLEAR FORM
34      CALL __mbox_ok(title1,SQLERRMESSAGE,"stop")
35      RETURN FALSE
36   END IF
37   CALL arr_ordnums.insertElement(1)
38   LET arr_ordnums[1] = order_rec.order_num
39   CALL arr_items.clear()
40   MESSAGE msg11
41   RETURN TRUE
42 END FUNCTION

Notes:


Function order_validate

This function validates the entries in the fields of the orders screen record.

Function order_validate (orders.4gl)
01 FUNCTION order_validate(d)
02   DEFINE d ui.Dialog
03   IF NOT d.getFieldTouched("orders.*") THEN
04      RETURN TRUE
05   END IF
06   IF d.validate("orders.*") < 0 THEN
07      RETURN FALSE
08   END IF
09   IF NOT order_check_store_num() THEN
10      RETURN FALSE
11   END IF
12   RETURN TRUE
13 END FUNCTION

Notes:


Function order_query

This function allows the user to search for a specific order by entering criteria into the form (Query by Example). This CONSTRUCT statement is not a subdialog of a DIALOG statement. It is a stand-alone statement called by the action find, triggered when the user selects the corresponding menu item or toolbar icon on the form orderform.

Function order_query (orders.4gl)
01 FUNCTION order_query()
02   DEFINE where_clause STRING,
03         id INTEGER, name STRING
04
05   MESSAGE msg02
06   CLEAR FORM
07
08   WHILE TRUE
09     LET int_flag = FALSE
10     CONSTRUCT BY NAME where_clause ON
11       orders.store_num,
12       customer.store_name,
13       orders.order_num,
14       orders.order_date,
15       orders.fac_code
16
17       ON ACTION zoom1
18         CALL display_custlist() RETURNING id, name
19         IF id > 0 THEN
20           DISPLAY id TO orders.store_num
21           DISPLAY name TO customer.store_name
22         END IF
23
24       ON ACTION about
25         CALL __mbox_ok(title1,msg18,"information")
26
27     END CONSTRUCT
28
29     IF int_flag THEN
30       MESSAGE msg03
31       IF arr_ordnums.getLength()==0 THEN
32         IF __mbox_yn(title1,msg15,"stop") THEN
33           EXIT PROGRAM
34         END IF
35         CONTINUE WHILE
36       END IF
37       RETURN
38     ELSE
39       IF order_select(where_clause) THEN
40         EXIT WHILE
41       END IF
42     END IF
43   END WHILE
44
45 END FUNCTION

Notes: