At the end of part 6 the address form existed, but there was exactly one way to reach it: open the customer form, find a customer, click "Addresses". That is fine for a demo and bad for an application. Every new module (contacts, notes, a report) would need yet another button in a form whose layout was already full.
This post adds the two missing pieces:
-
CUSTOMER_LKP- a modal "pick a customer" dialog that any component can call and that returns the chosen customer. -
MAIN_MNU- a small start form with one button per module.
Neither is complicated. The interesting part is how a modal Uniface form hands a value back to its caller, because that is not obvious from the component editor and the answer is in the documentation, not in the IDE.
The contract first
Before painting anything, the lookup got a signature that a caller can rely on:
activate "CUSTOMER_LKP".exec(pSearchText, vId, vName)
-
pSearchText(IN) - optional start value for the search field. -
vId(OUT) - the selected customer ID, or 0 if the user cancelled. -
vName(OUT) - "First Last" of the selected customer.
"0 means cancelled" is the whole error handling a caller needs. No global flags, no status codes to interpret.
The form
CUSTOMER_LKP is a component of type Modal Form with three painted entities:
| Entity | Content |
|---|---|
SEARCH_DMY.NOMODEL |
SEARCH_TEXT, RESULT_INFO (hit count), BTN_SEARCH
|
CUSTOMER.CUSTOMER_MDL |
grid (EGRID) with CUSTOMER_ID, LAST_NAME, FIRST_NAME, EMAIL
|
ACTION_DMY.NOMODEL |
BTN_SELECT, BTN_CANCEL
|
The customer entity is the same modeled entity the customer form uses. The grid and its column headers work exactly as in part 6: the entity frame gets FRM Widget Type EGRID, and each header is a label painted one row above its field with the same name as the field.
Read-only without touching the model
A lookup must not let the user edit customers. The model fields are editable (the customer form needs that), and changing the model was off the table. Uniface lets you override the field syntax per component: in the property grid of the painted field, Field Syntax → "..." opens a dialog Define Field Syntax; untick Edit and the field gets NED (no edit) in this component only. The customer form is unaffected.
A painter detail
A label painted from a model field picks up the model's text. The first header over CUSTOMER_ID read "Prim key field" - the description from the model - and had to be overwritten with "ID".
The code
The whole lookup is about 70 lines of ProcScript:
variables
numeric selectedId
string selectedName
endvariables
operation exec
params
string pSearchText : IN
numeric pCustomerId : OUT
string pCustomerName : OUT
endparams
pCustomerId = 0
pCustomerName = ""
$selectedId$ = 0
$selectedName$ = ""
SEARCH_TEXT.SEARCH_DMY = pSearchText
call DO_SEARCH
edit
pCustomerId = $selectedId$
pCustomerName = $selectedName$
return 0
end
The component variables selectedId / selectedName live in the Declarations section of the script and are addressed as $selectedId$ in code.
The search reuses the service operation from part 5, so the lookup gets the same text search, the same quoting and the same "active customers only" rule as the main form, without a second copy of any SQL:
entry DO_SEARCH
variables
string vProps, vWhere
boolean vWithInactive
endvariables
clear/e "CUSTOMER"
vWithInactive = 0
activate "CUSTOMER_SVC".BUILD_SEARCH_WHERE(SEARCH_TEXT.SEARCH_DMY, vWithInactive, vWhere)
vProps = ""
if (vWhere != "")
putitem/id vProps, "WHERE", vWhere
endif
$entityproperties(CUSTOMER) = vProps
retrieve/e "CUSTOMER"
if ($status < 0 & $status != -1 & $status != -2)
message/error $concat("The customers could not be read (error ", $procerror, ").")
return -1
endif
sort "CUSTOMER", "LAST_NAME:a ci"
setocc "CUSTOMER", 1
if (CUSTOMER_ID.CUSTOMER = "")
RESULT_INFO.SEARCH_DMY = "No customers found"
else
RESULT_INFO.SEARCH_DMY = $concat($totocc(CUSTOMER), " customer(s)")
endif
return 0
end
And the two buttons:
entry DO_SELECT
if (CUSTOMER_ID.CUSTOMER = "")
message/info "Please select a customer."
return 0
endif
$selectedId$ = CUSTOMER_ID.CUSTOMER
$selectedName$ = $concat(FIRST_NAME.CUSTOMER, " ", LAST_NAME.CUSTOMER)
macro "^ACCEPT"
return 0
end
entry DO_CANCEL
$selectedId$ = 0
$selectedName$ = ""
macro "^QUIT"
return 0
end
How the value gets back: edit, ^ACCEPT, ^QUIT
This is the part worth understanding. In exec, the statement edit hands control to the user. The form is now interactive, and exec is suspended on that line. Code after edit does not run while the dialog is open.
When the user clicks Select, DO_SELECT stores the choice in component variables and sends the structure editor function ^ACCEPT via macro. Cancel does the same with ^QUIT. Either one ends the edit session: edit returns (with $status 9 or 10 according to the docs), execution continues on the next line of exec, the OUT parameters are filled from the component variables, and only after exec returns is the form closed and the values delivered to the caller.
Two design consequences:
-
The button handlers never touch the OUT parameters. They only record a decision.
execis the single place where the result is assembled, which makes "cancelled = 0" trivially true:execsets 0 at the start andDO_CANCELsets 0 again. -
Nothing is written to the database.
^ACCEPTin a form with a modeled entity could, depending on the triggers, lead to a store. The lookup has nostoreanywhere and the grid isNED, so there is nothing to write.
I did not want to guess this behaviour, so it was checked in the offline help before writing the code. The Community Edition ships the reference as ulibrary.chm; it was unpacked with 7z and the topics edit, exit, macro and the accept / quit triggers were searched with a small Python script. That was faster and more reliable than clicking through the help viewer.
The caller side
The whole point of the contract is that the caller stays short. The address module in the main menu:
entry DO_ADDRESSES
variables
numeric vId
string vName
endvariables
activate "CUSTOMER_LKP".exec("", vId, vName)
if (vId > 0)
activate "ADDRESS_FRM".exec(vId, vName)
endif
return 0
end
Two lines of logic. ADDRESS_FRM did not change at all: it was built in part 6 with exec(pCustomerId, pCustomerName) and no knowledge of who calls it. That decision now pays off.
The main menu
MAIN_MNU is a non-modal form with one entity MENU_DMY.NOMODEL and three buttons:
operation exec
edit
end
entry DO_CUSTOMERS
activate "CUSTOMER_FRM".exec()
return 0
end
entry DO_EXIT
macro "^QUIT"
return 0
end
plus DO_ADDRESSES from above. Each button's detail trigger calls its entry.
One rule of the project was that no existing setting is changed without asking. So the application's start component in the assignment file was not switched to the menu. The menu was started with Compile & Test in the IDE; making it the real start screen is a one-line change for later.
Compiler
| Component | Errors | Warnings |
|---|---|---|
CUSTOMER_LKP |
0 | 3 (2x 1016, 1076) |
MAIN_MNU |
0 | 1 (1016) |
All of them are known from earlier parts: 1016 comes from the non-modeled dummy entities, 1076 from painting a modeled entity without an outer entity it relates to - the lookup loads it with its own WHERE, so that is intended.
While working in the Define Field Syntax dialog, the Messages tab once showed Illegal parent for the new idxtype (error -100000). It had no visible effect and the next compile was clean; I mention it only so nobody else panics.
Test run
Starting from an empty customer table:
- Menu → Customers → two customers created (IDs 12 and 13) → form closed → back in the menu.
- Menu → Addresses... → the lookup shows "2 customer(s)", sorted by last name.
- Search text
look→ 1 hit → Select → the address form opens with the header "Addresses of customer 12 - Berta Lookuptest". - In the address form: Add, type a street, close with Ctrl+F4 → the quit trigger asks about unsaved changes. No keeps the form open, Yes closes it without saving. In the database: no address. (That also closed an open point from part 6, where this path had only been tested without changes.)
- Addresses... again → the lookup instance is created fresh → Cancel → no address form, back in the menu.
- Both test customers removed with Delete permanently (no addresses, so no cascade question).
Takeaways
A modal form is a function. Give it IN and OUT parameters, decide what "cancelled" returns, and callers become two lines long. Any future module that needs "pick a customer first" can reuse the lookup without changing a line in it.
Put the result assembly after edit. Button handlers record the decision; exec builds the result. One place, easy to reason about.
Override per component, not in the model. NED in the lookup and editable in the maintenance form is exactly what component-level field syntax is for.
Read the docs offline. A .chm is just an archive of HTML files. Unpacked and searched with a script, it answered the edit / ^ACCEPT question in minutes.
Next: with a menu in place there is room for functions that belong to no single form. The first one is a CSV export that opens correctly in Excel, umlauts included.
Top comments (0)