DEV Community

Franz
Franz

Posted on

A customer maintenance form in Uniface 10, part 7 - a reusable lookup dialog and a main menu

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:

  1. CUSTOMER_LKP - a modal "pick a customer" dialog that any component can call and that returns the chosen customer.
  2. 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)
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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. exec is the single place where the result is assembled, which makes "cancelled = 0" trivially true: exec sets 0 at the start and DO_CANCEL sets 0 again.
  • Nothing is written to the database. ^ACCEPT in a form with a modeled entity could, depending on the triggers, lead to a store. The lookup has no store anywhere and the grid is NED, 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Menu → Customers → two customers created (IDs 12 and 13) → form closed → back in the menu.
  2. Menu → Addresses... → the lookup shows "2 customer(s)", sorted by last name.
  3. Search text look → 1 hit → Select → the address form opens with the header "Addresses of customer 12 - Berta Lookuptest".
  4. 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.)
  5. Addresses... again → the lookup instance is created fresh → Cancel → no address form, back in the menu.
  6. 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)