New Order

Introduction

The New Order window introduces the following Inq GUI features:

  • Table displays, including rendering, editable cells and totals
  • Property bindings, including enabling and disabling components according to model events
  • The reusable item chooser bundled with Inq
  • Popup menus
  • Custom dialogs

Also covered are client/server exception handling and raising events in the server.

Using the New Order Window

This is self-explanatory other than to explain the use of the item chooser

newordermenu

The Lookup text field and the check buttons in the popup are bound to the fields of the key Item.Search. Here is how that key is defined in Item.inq

  key Search max=100
  (
    fields (Product.Description SearchValue,
            typedef Flag ItemActive,
            typedef Flag Attr1Active,
            typedef Flag NameActive,
            typedef Flag DescrActive)
           
    #include <{db}/Item.Search.sql>
  )

The Misc button is bound to the Search.Attr1Active field and Item to Search.ItemActive and the fields are initialised so the menu is as shown.

The SQL for Search is this (see mysql/Item.Search.sql):

    "prepared", true,
    "read-sql",
        "
        {select-stmt},
            product P

        -- joins
        WHERE I.productid = P.productid

        -- values
        AND (    (? = ''Y'' AND UPPER(I.itemid) LIKE UPPER(CONCAT(?, ''%'')))
              OR (? = ''Y'' AND UPPER(I.attr1)  LIKE UPPER(CONCAT(''%'', ?, ''%'')))
              OR (? = ''Y'' AND UPPER(P.name)   LIKE UPPER(CONCAT(?, ''%'')))
              OR (? = ''Y'' AND UPPER(P.descn)  LIKE UPPER(CONCAT(''%'', ?, ''%'')))
            )
        ",
    "read-order",
        array a = (
            "ItemActive",       "SearchValue",
            "Attr1Active",      "SearchValue",
            "NameActive",       "SearchValue",
            "DescrActive",      "SearchValue"
        )

We can see how the logical expression uses the values of the various Active flags to control which columns (including a join with the product table) are considered in the search. Hence, if the user types in "doubles" then the following results are presented:

itemchooser

If the user checks the Item menu option then things like "EST-1" would also work.

Try playing with New Order. Once items have been added the Quantity column in the Items table can be edited to modify them.

neworder
Note
When placing an order and entering credit card numbers, the petstore client has a validation routine. If you are using the online demonstration, please don't enter your real credit card number - any old digits will do. This is not a harvesting exercise!

Placing an Order

Clicking the Place Order button raises a custom dialog for filling in shipping and payment details, as shown here;

placeorder

This dialog is scripted in the file gui/placeOrder.inq.

About Dialogs

Creating a Dialog

A dialog is created with a declaration like this:

gDialog(parent) placeOrder;

The argument parent must be a window or another dialog, though a dialog can also have no parent if it is declared as: gDialog() fooDialog;

Dialogs with no parent cannot be parent-modal (see below). By default, a dialog will hide itself on Window Manager close. For placeOrder we take over management of the dialog with explicit event handling so this behaviour is disabled:

placeOrder.properties.defaultCloseOperation = DO_NOTHING_ON_CLOSE;

Dialog Modality

Inq dialogs support the following modalities:

MODAL_ALL
All other application windows will not respond to events until the dialog is dismissed
MODAL_PARENT
The dialog's parent window will not respond to events until the dialog is dismissed; other application windows remain active
MODAL_NONE
All application windows continue to be active while the dialog is showing.

The default is MODAL_ALL.

Note
In Inq, modality does not imply synchronous execution of script in-line with the call to show(). Inq uses event call-backs for dialogs just like any of the basic components.

When input events are received in parts of the application a visible dialog is modal to, Inq attempts to raise the dialog. Whether it becomes visible depends on how the host windowing system manages this request or what other gesture (which may be configurable) it uses.

Dialog Events

An Inq dialog defines events for Ok and Cancel, called gDialogok and gDialogcancel. Like any other component, functions can be attached to these events using gEvent(). In order for these events to fire, a dialog needs to know what events from its components, "real" events if you like, represent them.

In placeOrder the cancel button's default and the dialog's window-closing events are set up as the gDialogcancel event:

gEvent(bCancel,    gDialog=gDialogcancel);
gEvent(placeOrder, gDialog=gDialogcancel, event=(gWclosing));

Picking up the cancel event, whether it arises from closing the window or clicking the cancel button, is then achieved with:

gEvent(placeOrder, call dialogCancel(), event=(gDialogcancel));

Discarding a Dialog

To discard a dialog (or in fact any top-level window) simply remove it from the node space. The placeOrder dialog is a context node, meaning that the dialog itself is $this when any of its (or its components) event handlers run. The cancel callback is therefore simply:

local function dialogCancel()
{
  // The dialog cancel event handler. Remove ourselves from the
  // node space, thus discarding the window.
  remove($this);
}

Other GUI Features

The placeOrder dialog uses a number of other features of Inq GUI construction

Rendering an Enumeration in a Combo Box

Inq can render enumerations in a combo box, displaying their external value and updating a single data node with the corresponding internal value. The Card Type combo box does this for the CardType typedef, defined in defs.inq as

typedef string CardType label={i18n}.ps.general.CARD_TYPE
(
  VISA   : "VISA"   : "Visa/Delta/Electron";
  MASTER : "MASTER" : "MasterCard/EuroCard";
  AMEX   : "AMEX"   : "American Express";
  DINERS : "DINERS" : "Diners Club";
);
enumcombo

The combo box is set up with the following lines:

gComboBox  cbCCType;
  .
  .
any ccTypeModel.internal  = renderinfo(typedef = Order.CardType);
cbCCType.properties.model = ccTypeModel;
cbCCType.properties.renderInfo = renderinfo($this.vars.Order.CardType, typedef = Order.CardType);

Combo boxes can also be loaded with dynamic lists by rendering a node set. We look at that method when discussing browseItems.inq.

Text Field Validation

Text fields support the validateInsert property. This is a function that Inq will call whenever text is typed or pasted into the component. Here are the relevant lines in gui/placeOrder.inq:

// Set up a custom validateInsert function for the credit card and expiry fields
tfExpMonth.properties.validateInsert = cfunc f = call validateCCField(len = 2);
tfExpYear.properties.validateInsert  = cfunc f = call validateCCField(len = 4);
tfCCNumber.properties.validateInsert = cfunc f = call validateCCField(len = 16);

The property must be a call statement. In this example, the function validateCCField() will be called. As well as including any arguments included in the call Inq passes the following, as illustrated in the definition of validateCCField():

local function validateCCField(any text,
                               any value,
                               any formatter,
                               any component,
                               any len)

The arguments are:

component
The component with the focus
text
The characters that would enter the text field
value
The current value of the component's rendered data node
formatter
A formatter suitable for parsing the value

In this example, the function determines that entering the given text would not exceed the additional len argument, in other words there is a maximum limit to the total number of characters that can be entered. Here is the function in full:

local function validateCCField(any text,
                               any value,
                               any formatter,
                               any component,
                               any len)
{
  // Do we already have enough characters?
  any t = component.properties.text;
  int l = length(t);
  if (l < len)
  {
    // Otherwise would adding the length of the would-be text
    // take us over the desired length (remember we could paste several
    // characters)
    if (l + length(text) <= len)
    {
      // Now we just need to check if the text is numeric. See
      // if it will assign to an integer. Note we can't validate the
      // actual value because we don't know where in the string
      // text is being inserted.
      l = text;
      isnull(l) ? null : text;
    }
    else
      null;
  }
  else
    null;
}
Note
A validateInsert function is called before the characters are added to the text field. The function returns null to veto the insertion or the text to be inserted (which can be different to the text argument if required).

Furthermore validateCCField() checks if the text that would be entered is numeric. This is most easily achieved by assigning it to an integer. The result will be null if the characters are not convertible to a number.

Complex GUI State Control

One of the most important aspects a of GUI is to perform adequate validation so that downstream processing is most likely to succeed. In placeOrder we want to prevent confirmation of the order if the dialog is not complete. The confirm button has its enabled property bound like this:

gProperty(bConfirm, enabled, renderinfo(call checkOk()));

The renderinfo expression was discussed in GUI Basics. In this example it is a call to the function checkOk():

local function checkOk()
{
  // All these things must be true for the order to be confirmed
  $this.vars.Order.ShipToFirstName &&
  $this.vars.Order.ShipToLastName &&
  $this.vars.Order.ShipAddr1 &&
  $this.vars.Order.ShipAddr2 &&
  $this.vars.Order.ShipCity &&
  $this.vars.Order.ShipState &&
  $this.vars.Order.ShipZIP &&
  $this.vars.Order.ShipCountry &&
    ($this.vars.sameAddr ||
      (
        $this.vars.Order.BillToFirstName &&
        $this.vars.Order.BillToLastName &&
        $this.vars.Order.BillAddr1 &&
        $this.vars.Order.BillAddr2 &&
        $this.vars.Order.BillCity &&
        $this.vars.Order.BillState &&
        $this.vars.Order.BillZIP &&
        $this.vars.Order.BillCountry
      )
    ) &&
  ($this.vars.expMonth < 13) &&
  ($this.vars.expMonth > 0)  &&
  ($this.vars.expYear >= datepart(YEAR, getdate())) &&
  $this.vars.Order.CardType &&
  $this.vars.Order.CreditCard  // full validation on confirmation
  ;
}

This makes bConfirm's enabled property dependent on all the variables in the context $this.vars.... (even though the references are within checkOk()).

In turn, all the components rendering these variables have suitable events established on them with the firemodel=true argument specified:

local function setupFireModels(any components)
{
  // Set up various components whose rendered value is part of a
  // properly completed order.
  gEvent(components.tfShipToFirstName, event=(gDocchange, gDocinsert, gDocremove), firemodel=true);
  gEvent(components.tfShipToLastName, event=(gDocchange, gDocinsert, gDocremove), firemodel=true);
  gEvent(components.tfShipAddr1, event=(gDocchange, gDocinsert, gDocremove), firemodel=true);
    .
    .

Thus, models (the variables $this.vars.Order.ShipToFirstName...), views (bConfirm's enabled property) and controllers (the text fields) can be tied together quite simply to make robust and effective GUIs.

A Bit About Credit Card Validation

As a demonstration of the use of regular expressions and a bit of string handling, gui/placeOrder.inq contains the functions ccBadCheckSum(any CreditCard) and ccBadPattern(any CardType, any CreditCard). These might be worth a look as potentially useful resources.

Calling newOrder Entry Points

When placeOrder is created, its parent, the newOrder window, is passed as an argument to gui/placeOrder.inq:createGUI(parent, Account). As well as using parent as the graphical owner of the dialog, the script retains it as $this.svars.parent:

// Remember our parent here so we can use any function entry points
// it defines (see funcs.getQuestionDialog in newOrder.inq for another
// example). It must be saved under a simple map so as not to violate
// Inq hierarchy constraints.
smap placeOrder.svars;
any placeOrder.svars.parent = parent;

In gui/newOrder.inq the following func variables are defined:

// Create a function that placeOrder.inq can use to get at the question
// dialog we have created. This is an example of how different contexts can
// cooperate. The function must be a func, so it encapsulates the current
// context.
func $this.funcs.getQuestionDialog = {
                                       $this.questionDialog;
                                     };

// Similarly a function to recover the items
func $this.funcs.getItems = $this.vars.items;

These are func variables. so they encapsulate the context they were declared in. This means they can be called from other contexts such as placeOrder and offer structured access to data in newOrder.

GUI Construction

The New Order window introduces several new areas of GUI building compared to My Account. We'll look at each of these.

The Items Table

As items are added to the order they appear in the Items table. The simplest tables are read-only without any need to configure rendering, however this example, shared with the items table in the My Orders window, adds an editor component and some rendering nicities. Included also is a fixed row used for a totals line.

Dimensioning Properties

An Inq table supports two properties that affect its dimensioning during layout.

visibleRows
Sets the initial height of the table so the specified number of rows will display without scrolling. The default is 10.
visibleColumns
Sets the initial width of the table so the specified number of columns will display without scrolling. Setting this property to zero attempts to make all columns visible. By default this property is not set.

Setting Up The Columns

A table's columns are established by setting its columns property to an ordered map containing suitable renderinfo expressions. The label implied by the renderinfo is used for the column headings and its width to dimension them.

Note
columns cannot be set until the table knows its context node, so that Inq can arrange to listen for the appropriate node events.

The function gui/newOrder.inq:itemTableColumns() creates and returns a suitable map, varying slightly for the New Order and My Order usage. Once assigned to a table's columns property, such as

omap columns;
any columns.itemid      = renderinfo(typedef=Item.Item);
any columns.Category    = renderinfo(typedef=Product.Category);
  .
  .
itemTable.properties.columns = columns;

properties relating to the columns within the specific table can be set.

Cell Rendering and Editing

In New Order the Quantity column is editable. Inq supports cell editing by creating the component in the usual way and establishing it as the column's editor property. The following excerpt from gui/newOrder.inq does this:

gTextField tf;
any context.vars.qtyEdit = clone(context.vars.LineItem.Qty);
tf.properties.renderInfo = renderinfo($this.vars.qtyEdit, typedef=LineItem.Qty, editable=true);
tf.properties.validateInsert = $catalog.guiFuncs.positiveInteger;
itemTable.properties.columns.qty.editor.properties.component = tf;

The component tf is set up to render a data node created at context.vars.qtyEdit, uses a validateInsert function to limit input to positive integer values and is set as the qty column's editor.

A column editor supports the following properties:

Name Type Notes
component A component The editor component
canStartEdit A cfunc that must be a call statement Returns true or false. With an editor component established this property can be used to decide whether editing is possible on a given occasion.
onStopEdit A cfunc that must be a call statement. Called as editing completes, once before and once after the underlying cell's data node is updated with the editor component's value.
clickCountToStart int The number of mouse clicks required to start editing. Default value is 2.

Cell functions, such as onStopEdit, accept at most the following arguments:

Name Description
parent The parent component, that is the gTable
component The editor component
rowRoot The node-set child of the row being edited
row The row number
rowKey The map key of the row (that is the map key of the node-set child)
column The column number
columnName The column name (that is the name the column was given in the original omap set as the table's columns property).
mouseCell For rendering (not editing) functions - true if the mouse is in the cell being rendered, false if it is not. Always false for editing functions.
isUser Whether editing was stopped because of user action or by the underlying system (for example because focus was lost)
value The current value within the table's data structure this cell is rendering.
after An onStopEdit function is called at least once and at most twice. The first call passes after == false and at this point the underlying cell value has not been changed. The function can veto the edit by assignment newValue = value. The second call only occurs if value != newValue and is passed after == true. At this point the underlying cell value has been updated.
newValue the value from the editor component that the cell will become (after = false) or is now (after = true)
formatter the formatter associated with the column
level the level name (trees only)
isLeaf true is the cell represents a tree leaf, false otherwise (trees only)
expanded true is the cell represents a tree branch and is expanded, false otherwise (trees only)

itemTable sets the qty column's onStopEdit function with this line:

itemTable.properties.columns.qty.editor.properties.onStopEdit = cfunc f = call qtyChanged();

The qtyChanged() function generates an event to update the total for the current row and recalculates the order total:

local function qtyChanged(any parent,
                          any component,
                          any rowRoot,
                          any row,
                          any rowKey,
                          any column,
                          any columnName,
                          any isMouseCell,
                          any isUser,
                          any value,
                          any newValue,
                          any after,
                          any formatter)
{
  // This assignment simply kicks out an event on rowRoot.LineItem.UnitPrice
  // We do it because we want the Total column to be re-rendered.
  rowRoot.LineItem.UnitPrice = rowRoot.Item.ListPrice;
  call calcOrderTotal();
}

When used in the My Orders window, the items table includes a column for OrderStatus.Status. This uses a different cell function, one for rendering, as well as a rendering component to achieve the effect as shown:

myorders

Firstly, the column is added to the ordered map in the same way as the others:

any columns.status      = renderinfo(typedef=OrderStatus.Status);

As for the editor setup, a rendering function can be added once the table's columns property has been set. This line sets style property of the status column's renderer:

itemTable.properties.columns.status.renderer.properties.style = cfunc f = call statusStyle();

This property is a call statement to the scripted cell function statusStyle(). We'll look at what this function does shortly.

A status cell renders text and an icon. If you resize the column you will see that the icon stays at the right-hand edge of the cell and the area for text resizes. By default, Inq uses a gLabel for rendering table cells, however using property access this can be overridden with any component. To render the text and icon in this way we use a small layout comprising two labels:

gLabel cxlIcon;
gLabel statusValue;

// Create a box explicitly (instead of using Row{...}) as this
// becomes the renderer component for the column
gBox   statusRenderer;
statusRenderer.properties.axis = X_AXIS;
layout(., statusRenderer, "statusValue Geometry xy:fv cxlIcon");

This script uses a horizontal box and lays out the two labels within it using a simple Geometry constraint. The statusRenderer box is then set as the renderer's component:

// Make it the renderer component of the status column
itemTable.properties.columns.status.renderer.properties.component = statusRenderer;

Inq will try so set the rendered value into whatever rendering component has been established. When using a "composite" component this behaviour is still supported, however Inq has to be told which component to use:

// Tell Inq which component the value should be set into
itemTable.properties.columns.status.renderer.properties.setValueToComponent = statusValue;

When there is no suitable component (for example is the cell is only displaying an icon) then setValueToComponent can be set to null. In that case all responsibility for rendering lies with the style function. In our example the style function is statusStyle(). Here's what it looks like:

local function statusStyle(any component,
                           any value,
                           any rowRoot,
                           any isMouseCell)
{
  // Decide what style and icon, if any, to apply to the value and
  // icon parts of the custom renderer
  switch
  {
    when (rowRoot.OrderStatus.Status == enum(OStatus, O))
    {
      component.statusValue.properties.style = $catalog.ps.styles.open;
      component.cxlIcon.properties.icon = $catalog.icons.cxlItem;
    }

    when (rowRoot.OrderStatus.Status == enum(OStatus, A))
    {
      component.statusValue.properties.style = $catalog.ps.styles.allocated;
      component.cxlIcon.properties.icon = $catalog.icons.cxlItem;
    }
      
    when (rowRoot.OrderStatus.Status == enum(OStatus, C))
    {
      component.statusValue.properties.style = $catalog.ps.styles.cancelled;
      component.cxlIcon.properties.icon = null;
    }
      
    when (rowRoot.OrderStatus.Status == enum(OStatus, S))
    {
      component.statusValue.properties.style = $catalog.ps.styles.shipped;
      component.cxlIcon.properties.icon = null;
    }
    
    otherwise
      component.cxlIcon.properties.icon = null;
  }
  
  // returns no style for Inq to apply - everything required is already done
  null;
}

A style function returns a style that Inq will apply to the renderer component or null if all rendering issues are handled in the function. As the component is only a box this example always returns null.

The work of statusStyle() is to apply a style (in these cases specifying the foreground colour) to the text and perhaps display an icon.

Table GUI Events

Petstore's Order Processor progresses items and orders through to the shipped state. Until then individual items can be cancelled by clicking on the icon in the status cell. We can solicit the gMclicked event from the table as a way to do this:

gEvent(itemTable, call maybeCxl(), event=(gMclicked));

When the event occurs and maybeCxl() is called Inq passes the event to the function as @event. The event is a map containing various information about the event. When occurring on a table this example yields something like:

cellX=63
cellY=10
rowRoot={ ... }
component={ ... }
cell={height=17, width=69, y=0, x=744}
column=7
columnName=status
y=10
x=807
row=0

with these meanings:

@event info Comments
x=807, y=10 The coordinates of the event in the component's (gTable) geometry
cellX=63, cellY=10 The coordinates of the event in the cell's geometry
row=0, column=7 The row and column numbers for the event
columnName=status The column name for the event
rowRoot The node-set child for the table row
component The component (the gTable)
cell={height=17, width=69, y=0, x=744} The cell dimensions and location in the gTable's geometry.

Using this information the maybeCxl event callback function can determine whether the mouse click occurred in the icon region of the status cell:

local function maybeCxl()
{
  // We only respond to the mouse event if:
  //   1) The status is Allocated or Open
  //   2) The event occurred in the status column
  //   2) The event is in the cell region where we have placed the cancel icon

  if (@event.cellX &&       // No coordinates if event is outside the data/cell region
      (@event.rowRoot.OrderStatus.Status == enum(OStatus, O) ||
       @event.rowRoot.OrderStatus.Status == enum(OStatus, A)) &&
      @event.columnName == "status" &&   // Must be in the 'status' column
      @event.cell.width - @event.cellX <= $catalog.icons.cxlItem.properties.width) // In the icon area
  {
    send cxlItem(@event.rowRoot.OrderStatus);
  }
}

The Totals Line

The items table includes a fixed row to display the order total. This is set up as follows:

gTable itemTable;

// Add a totals table. We only need one row. This must be done prior
// to layout

gTable totalsTable;
totalsTable.properties.visibleRows  = 1;
totalsTable.properties.showGrid     = false;
itemTable.properties.totalsTable    = totalsTable;

By setting the visibleRows the surrogate table is dimensioned appropriately. This property can be set to any value if, for example, there is a requirement for more fixed rows.

The totalsTable must have the same name and number of columns as its parent, however the renderinfo information can be different. Rendering components and style functions can also be particular to to it.

In gui/newOrder.inq the totals table is set up in :

// Finalise the setup of the totals row
local function setupTotalTable(any context,
                               any itemTable,
                               any columns,
                               any i18n)
{
  // Fetch any totals table out of itemTable
  any totalsTable = itemTable.properties.totalsTable;
  if (totalsTable)
  {
    // Make the data for totalsTable to render. As stated elsewhere, there is
    // only one row. The ValueHelper type is used just to get a suitably typed
    // field to hold the order total.
    any context.vars.orderTotal.row.OrderTotal = new(ValueHelper);
    context.vars.orderTotal.row.OrderTotal.String = {i18n}.ps.title.TOTAL;
  
    // Establish its columns. The columns are the same as the parent
    // table with the exception of the description and total columns,
    // so replace those.
    any columns.total  = renderinfo($this.OrderTotal.Price, typedef=ValueHelper.Price, format="\u00A4#,##0.00");
    any columns.description = renderinfo($this.OrderTotal.String);
    totalsTable.properties.columns = columns;
  
    // Tell it where the total root is
    totalsTable.properties.modelRoot  = path($this.vars.orderTotal);
    
    // Bold the font used for the totals
    totalsTable.properties.font = itemTable.properties.font.properties.bold;

    totalsTable.properties.columns.total.renderer.properties.style = cfunc f = call totalStyle();
  }
}

The steps can be summarised:

  1. Create the data in as a "one row node-set" structure form. OrderTotal is an instance of the typedef ValueHelper, which exists only to provide formatting information here and in the pdf reports.
  2. The total and description columns from the original columns map are overwritten with new renderinfos.
  3. totalsTable's columns and modelRoot properties are established.
  4. The font (for the entire table) is bolded, based on the font of the parent table.
  5. A style function is placed on the total column.

Deploying The Item Chooser

The Item Chooser is a reusable element contained within the Inq distribution Jar file. It is an example of how significant levels of (in this case GUI) functionality can be written ahead of time and applied in unrelated applications designed much later.

Item Chooser adopts a component supplied by the client and applies the input value to a search algorithm. The search may return none, one or more than one items found. If more than one item is found a popup dialog gives the user the opportunity to select amongst those available. The dialog includes a table to display the choices and some buttons to confirm or cancel the operation. The following sequence summarises how Item Chooser, the application client and server message one-another:

itemmessageflow

Creating an Item chooser

The first step to using the item chooser is to create one. In gui/newOrder.inq:setupPetChooser() this is done by:

gTable table;

call inq.gui.util:createItemChooser(parent,
                                    title = {i18n}.ps.title.CHOOSE_PET,
                                    table,
                                    at = path($root.psChooser),
                                    items  = {i18n}.ps.title.CHOOSE_PET);
Note
To use the Item Chooser the application must first have parsed its source. In petstore this is done in gui/userStart.inq with the statement exec("cp:///inq/gui/itemChooser.inq");.

The signature of inq.gui.util:createItemChooser() includes some further arguments for customising the Item Chooser GUI. Looking at cp://inq/gui/itemChooser.inq we have

function createItemChooser(any    parent,
                           any    title,
                           any    table,
                           any    at,
                           string ok     = "OK",
                           string cancel = "Cancel",
                           string items  = "Items")

These arguments are as follows:

Argument Description
parent A window to parent the dialog
title The dialog title
table The gTable to display any choices
at A path specifying where in the node space the Item Chooser will be placed.
ok Text for the dialg's OK button
cancel Text for the dialg's cancel button
items Text used for the table's caption border

Calling createItemChooser() creates and lays out its GUI, part of which is the table to display any item choices. The application is opaque to the Item Chooser so setting up the table columns, any rendering and so on is the application's responsibility. This is done by the remainder of setupPetChooser() - the source code refers.

Registering An Input Component

Setting up an Item Chooser is a two-stage process, the second being registering a text field to provide its input. The search algorithm can use any number of data items, such as the flags discussed in Using the New Order Window but the Item Chooser initiates it using a single text field.

The text field, any other input components and the data they are rendering are the application's responsibility. In our example these are created in setupLookupComponent(). This function creates the text field (called tfLookup) and attaches a popup menu to it containing a number of check button menu items. These are bound to the various fields of an instance of the key Item.Search.

The only other global function in cp://inq/gui/itemChooser.inq is:

function registerItemChooserComponent(any component,
                                      any itemChooser,
                                      any validateF,
                                      any searchF,
                                      any foundF,
                                      any notFoundF,
                                      any valuePath)

These are its arguments:

Argument Description
component The component to be registered. Item Chooser assumes this will be a gTextField.
itemChooser The item chooser (previously created with createItemChooser()) to register the component with.
validateF A func the Item Chooser calls to validate the value entered to component.
searchF A cfunc the Item Chooser calls to invoke the application's search.
foundF A func the Item Chooser calls when a valid item has been found. This occurs either when the application search returns only one result or the user chooses from those presented.
notFoundF A func the Item Chooser calls when the search returns no results. Optional argument.
valuePath A path applied the node returned as the result, that resolves to the value to set as the component's rendered value. See further discussion below.

In gui/newOrder.inq:contextEstablished() petstore registers tfLookup with the Item Chooser like this

// Register tfLookup with the pet chooser
call inq.gui.util:registerItemChooserComponent(component   = $this.tfLookup,
                                               itemChooser = $root.psChooser,
                                               validateF,
                                               searchF,
                                               foundF,
                                               notFoundF,
                                               valuePath = path($this.Item.Item));

The various function arguments are Inq statements (typically block statements) that the Item Chooser executes at various times. They are passed as arguments, having been defined as function variables. Even though they are not formally defined, these closure style functions can still themselves have arguments passed to them when they are executed. Therefore, as well as the interface defined by the two formal global functions in cp://inq/gui/itemChooser.inq, client code of Item Chooser needs to know the interface supplied and expected of these arguments.

The validateF Argument

The purpose of this function is to validate the user-entered value. Item Chooser passes the following arguments:

  • value - this is whatever the text field (tfLookup in this example) is rendering.
  • vars - a map that the client can use as it wishes to store any data. This map is later passed to searchF. The purpose of this is to limit dependency on the client context to validateF.

    In our example $this.vars.Search is returned via this route. Note that tfLookup is rendering $this.vars.Search.SearchValue but this is something the function arguments do not need to be aware of.

    The function returns true if value passes validation, false otherwise. For New Order's Item search, value is valid if it is at least three characters long.

The searchF Argument

If validateF returns true then Item Chooser immediately calls searchF with the following arguments:

  • id - a token for use in the client/server interface, see below.
  • value - the argument as for validateF.
  • vars - the argument as for validateF. Whatever was placed into vars by validateF can be accessed.

Client/Server Interface

Petstore's searchF function invokes the server-side service searchPet() passing vars.Search as discussed above and id.

send searchPet(vars.Search, id);

The search service is the client's responsibility but its contract with Item Chooser is to respond by invoking its chooserResult() service. Looking at psOrders.inq, the searchPet() service is as shown:

service searchPet(any Search, any id)
{
  hmap m;

  read(Item, Search, target=m, setname="list");
  aggregate(Product, m.list[@first].Item);

  // Whatever we've got send it back to the chooserResult service
  // passing back the id we were given as well.
  send inq.gui.util:chooserResult(value = Search.SearchValue,
                                  id,
                                  m.list);
}

The id argument is a token the Item Chooser provides and with which the client's invocation of chooserResult() must respond. If the Item Chooser is registered with more than one client component the token allows it to discard stale responses. The chooserResult() service defines the following arguments:

Argument Description
id The token exchanged across the client/server interface (see above>
value The user's original input value
list The results as a node set Must be an empty list for no results or containing one or more children.

The foundF Argument

If the client's search service returns a single result Item Chooser calls foundF with the following arguments:

  • component - the component originally registered
  • value - the value used in the search
  • result - the node-set child

When multiple results are returned and the user chooses one using the Item Chooser GUI foundF is called in the same way.

The notFoundF Argument

When no results are returned notFoundF is called. The only argument is the original search value.

func and cfunc - What's The Difference?

When registering with an Item Chooser some of the function arguments are declared as func while others are cfunc. In gui/newOrder.inq these functions are set up thus:

func validateF = {
                   // On the stack we have the smap "vars" and the search
                   // value as "value". Pass back $this.vars.Search via "vars"
                   // so it will be given back to us when searchF is called
                   any vars.Search = $this.vars.Search;
                   
                   // Do the validation and return true/false
                   length(value) >= 3;
                 };

// The search function's contract is to invoke a service that, in
// turn, invokes back on chooserResult (see cp://inq/gui/itemChooser.inq)
// A cfunc does not encapsulate a context, so when it is invoked the
// context remains unchanged, and is that of the item chooser.
cfunc searchF  = {
                   // At vars.Search we have the Search key whose
                   // fields are being rendered by the tfLookup
                   // component and the various popup menu check
                   // items. Its all ready to go so just invoke it
                   send searchPet(vars.Search, id);
                 };
                 
func foundF    = {
                   // Put the designated value within the result
                   // into the component.
                   component.renderedValue = result.{component.ic.valuePath};
                   
                   // Copy the instances within the result to those
                   // our components are rendering
                   $this.vars.Product = result.Product;
                   $this.vars.Item    = result.Item;
                   
                   // Copy over the unit cost and Item fields from
                   // the chosen Item to the LineItem
                   $this.vars.LineItem.UnitPrice = result.Item.ListPrice;
                   $this.vars.LineItem.Item      = result.Item.Item;
                 };

func notFoundF = {
                   writeln($catalog.system.out, "Not found");
                 };

validateF, foundF and notFoundF are all funcs. This ensures their body is executed in the context newOrder (the New Order window) making its permanent variables accessible.

searchF is a cfunc. This means that it executes in the caller's context, that of the Item Chooser. The client/server exchange takes place in this context so that Item Chooser likewise has access to its state.

Use of valuePath

One of the arguments passed to registerItemChooserComponent() is valuePath. When using Item Chooser, the search value entered by the user is generally some sort of free text. If the search yields a result, part of user feedback is likely to be replacing the input text with a formal result value. This happens with this line in foundF:

component.renderedValue = result.{component.ic.valuePath};

In the petstore example the formal result value is the selected Item's item code.

Item Chooser also uses valuePath to keep track of the current successful search.

Conclusion

Item Chooser is a useful, complex and generally applicable GUI component that can be configured by setting up suitable table for rendering the choices, supplying function variables according to the required interface and supplying an input component.

We look at its implementation elsewhere [TODO] or refer to the source within the Inq distrubition Jar file at inq/gui/itemChooser.inq.

Creating Popup Menus

As stated earlier, New Order uses a popup menu, in its case containing check items to control the various Active flags used by the Item search. Created along with tfLookup in setupLookupComponent(), the first thing to do is create the components:

// Create some menu check components to make the popup menu for the
// lookup component.
gMenuCheck mcSearchOnName;
gMenuCheck mcSearchOnDescr;
gMenuCheck mcSearchOnAttr1;
gMenuCheck mcSearchOnItem;

A menu may contain gMenuCheck, gMenuButton, or gMenuRadio items. The components are then bound to the data node they are rendering:

// Bind the check items to their data
mcSearchOnName.properties.renderInfo  = renderinfo($this.vars.Search.NameActive,  label = {i18n}.ps.button.NAME);
mcSearchOnDescr.properties.renderInfo = renderinfo($this.vars.Search.DescrActive, label = {i18n}.ps.button.DESCR);
mcSearchOnAttr1.properties.renderInfo = renderinfo($this.vars.Search.Attr1Active, label = {i18n}.ps.button.MISC);
mcSearchOnItem.properties.renderInfo  = renderinfo($this.vars.Search.ItemActive,  label = {i18n}.ps.button.ITEM);

The gMenuCheck component (like its gCheck equivalent) supports properties to specify the value it represents when checked or unchecked. These default respectively to true and false but can be set to values suited to the application. When toggled by the user these values are copied to the rendered data. For the petstore search we want them to set the flags to Y or N:

mcSearchOnName.properties.checkedValue =
  mcSearchOnDescr.properties.checkedValue =
  mcSearchOnAttr1.properties.checkedValue =
  mcSearchOnItem.properties.checkedValue = enum(Flag, Y);

mcSearchOnName.properties.uncheckedValue =
  mcSearchOnDescr.properties.uncheckedValue =
  mcSearchOnAttr1.properties.uncheckedValue =
  mcSearchOnItem.properties.uncheckedValue = enum(Flag, N);

Creating the popup menu and laying out the items within it is then accomplished as follows:

// Create a popup on the component which allows the user to select
// the field(s) the search should extend to
gPopupMenu popupMenu;

// Layout the menu items
layout(., popupMenu, "mcSearchOnName
                      mcSearchOnDescr
                      mcSearchOnAttr1
                      mcSearchOnItem");

A popup menu can be placed on any (and more than one) component. Here we add it to tfLookup:

// Put the popup menu on the text field
gPopup(tfLookup, popupMenu);

Client/Server

The server-side code for placing new orders can be found in psOrders.inq. When the user confirms an order from the Place Order dialog the following flow takes place:

neworderflow

The Order and its constituent LineItems are passed to the server's placeOrder(any Order, any items, any ack) service.

The ack and @exception Arguments

In the client, the invocation looks like this (see gui/placeOrder.inq):

$this.properties.disabledText = "Processing order, please wait...";

send placeOrder(Order,
                items = xfunc($this.svars.parent.funcs.getItems),
                ack = func f =
                {
                  // Unglass the GUI
                  $this.properties.disabledText = null;
                  
                  // Dispose the dialog
                  call dialogCancel();
                  
                  // Confirm to the user
                  call inq.gui.util:messageDialog(dialog       = $root.dialogs.ps.message,
                                                  messageText  = renderf($catalog.{$root.i18n}.ps.message.ORDER_CONFIRMED,
                                                                         item.Order)
                                                 );
                },
                @exception = func f =
                {
                  // Unglass the GUI
                  $this.properties.disabledText = null;

                  // Leave the dialog showing                    

                  // Alert the user something has gone wrong.
                  call inq.gui.util:messageDialog(dialog       = $root.dialogs.ps.message,
                                                  messageText  = msg + "\n" + stackTrace
                                                 );
                }
                );

The placeOrder service uses the pattern of a nested transaction with an acknowledgement. If the order is placed successfully the server replies with this line in psOrders.inq:

send updateOk(item = Order, ack);
Note
The updateOk service is found in gui/userMain.inq and is simply:

service updateOk(any item, any ack)
{
  xfunc(ack, item);
}

The context of the func argument ack is preserved across the client/server exchange and item is an opaque pass-through argument for use in the target function.

The @exception argument gives the option to specify exception handling for this particular service invocation. If an exception occurs in the server the func is executed to provide some graceful control. In this example it is important to re-enable the GUI whether the order is placed successfully or something goes wrong. If no @exception is specified the exception is dumped to a window (supplied by the Inq client and outside the application).

An exception handler function has the following arguments passed to it:

Exception Handler Arguments
Argument Notes
msg The exception message
stackTrace The stack trace, whose top-most line identifies the source URL, function (or service) and line number where the exception occurred
isUser true if the exception was thrown by script, false if the exception was incurred
exInfo Any user-supplied data that was supplied in the throw statement (isUser exceptions only)
isCommit true if the exception occurred during the transaction's commit phase, false otherwise
exTime The time the exception was thrown or incurred

In psOrders.inq:newOrder() (called by the placeOrder service) this line is commented out:

// Croak test. Try uncommenting this and see that the GUI is
// still un-glassed even if an exception occurs.
// a = b;

You can try uncommenting this line, re-parsing the file into the server and placing an order. As well, restart the client with and without the @exception argument.

neworderexception

Without @exception the exception is displayed in the Inq client's log window, instead of being caught by the application.

Create Event Data

When a new typedef instance is created by the committing transaction, Inq raises an event. In petstore this event is listened for by the My Orders part of the application, so that as new orders are raised the My Orders window updates with them automatically.

Listening for and processing the event is covered in My Orders. There is an implicit contract between sender and receiver defined by the event data. When the Order instance is created the optional second argument is specified - this is a data item (usually a map) that acts as a discriminator to allow a listener to receive only those orders it is interested in. The function makeCreateRaiseData() creates this data:

any createData = call makeCreateRaiseData(Order);
create(Order, createData);
  .
  .
/**
 * Create a map to characterise events raised on Order creation.
 */
local function makeCreateRaiseData(any Order)
{
  any createData = new(Order.Filter);
  createData.Account  = Order.Account;
  createData.Status   = Order.Status;
  createData.FromDate = Order.OrderDate;
  createData.ToDate   = Order.OrderDate;
  
  // returns
  createData;
}

An instance of Order.Filter conveniently acts as a definition for all the things the event contract requires, although any map however created is suitable.

nextpage