Ask Reuben
fgl_zoom
What is fgl_zoom?
Fgl_zoom is not something you will find in our documentation, it is a library I have made available in our GitHub repository. It is a solution to the design pattern where you have a BUTTONEDIT to enter a code value …
… the user clicks on the BUTTONEDIT button, a window is opened that displays a list of possible values and possibly additional detail about each value i.e. a code and its corresponding description …
… the user selects a row, and the appropriate code value is entered into the BUTTONEDIT widget. If a user happens to know the code they can type it directly but for most occasions the user will be selecting from the displayed list.
You might recognise the pattern from your system to enter various codes e.g. customer code, product code, supplier code, account code, debtor codes, creditor codes, employee codes, currency codes, tax codes, region codes, state codes, country codes, group codes, type codes, classification codes etc
You might ask why not use a COMBOBOX and that answer might depend on your GUI design standards. My rule of thumb is that if the number of expected entries is less than 20 then use a COMBOBOX, otherwise use a BUTTONEDIT with fgl_zoom. I don’t like it when a COMBOBOX has more than a screen full of values, whereas with BUTTONEDIT + fgl_zoom, the user can use the Find and Navigate functionality and/or a QBE to find and select a value from a long list of potential values.
To illustrate the power of fgl_zoom, I invite you to download the repository and run the test program. The first FOLDER PAGE labelled Example has 6 BUTTONEDIT’s. Click on the button in each of the BUTTONEDIT’s and note the window that appears and a list of values (the Country and Store BUTTONEDIT will come up with a QBE, press ENTER to select all values and bring up the list). The question I pose to you is.
There are 6 BUTTONEDITS…
- How many .per or .4fd are there?
- How many DECLARE CURSORS have been coded?
- How many DISPLAY ARRAY statements have been coded?
The answer most people give for each question is 6, one for each of the BUTTONEDIT’s they can see on the screen and click on. Using fgl_zoom, the answer for each question is effectively 1 in TOTAL.
The next question I ask these people is how many of these BUTTONEDIT’s do you have in your system? The answer can often exceed 100. That is they have over 100 separate .per or .4fd files, over 100 pieces of code with a different SQL statement and database cursor, over 100 DISPLAY ARRAY statements. That adds up to a lot of code to maintain where in most cases the only thing unique in each case is the SQL statement and the window and column titles. fgl_zoom is an answer to the question of how to code this so that the only thing each developer needs to code is what is unique, they do not need to repeatedly code the same thing with some slight variation for what is unique.
The final point I then make using the Example FOLDER PAGE is to click on a BUTTONEDIT so it has the focus, and then click on View Source on the right. The dialog that pops up has the unique source for each window. The six windows and code blocks below relate to each of the six BUTTONEDIT’s in the Example tab …
PRIVATE FUNCTION zoom_state(l_current_value STRING) DEFINE state_zoom fgl_zoom.zoomType CALL state_zoom.init() LET state_zoom.noqbe = TRUE LET state_zoom.cancelvalue = l_current_value LET state_zoom.title = "Select State" LET state_zoom.sql = "SELECT state_name, state_code FROM fgl_zoom_state ORDER BY state_name" CALL state_zoom.column[1].quick_set("state_name", FALSE, "c", 20, "State") CALL state_zoom.column[2].quick_set("state_code", TRUE, "c", 0, "Code") LET state_zoom.column[2].excludelist = TRUE RETURN state_zoom.call() END FUNCTION
PRIVATE FUNCTION zoom_customer(l_current_value STRING) DEFINE customer_zoom zoomType CALL customer_zoom.init() LET customer_zoom.cancelvalue = l_current_value LET customer_zoom.title = "Select Customer Code" LET customer_zoom.sql = "SELECT %2 FROM fgl_zoom_customer WHERE %1 ORDER BY customer_num" CALL customer_zoom.column[1].quick_set("customer_num", TRUE, "i", 4, "Code") CALL customer_zoom.column[2].quick_set("(trim(lname) ||', '||trim(fname))", FALSE, "c", 10, "Name") CALL customer_zoom.column[3].quick_set("company", FALSE, "c", 10, "Company") CALL customer_zoom.column[4].quick_set("(trim(address1)||' '||trim(address2))", FALSE, "c", 20, "Address") CALL customer_zoom.column[5].quick_set("city", FALSE, "c", 10, "City") CALL customer_zoom.column[6].quick_set("state", FALSE, "c", 5, "State") LET customer_zoom.freezeleft = 1 RETURN customer_zoom.call() END FUNCTION
PRIVATE FUNCTION zoom_store(l_current_value STRING) DEFINE store_zoom fgl_zoom.zoomType CALL store_zoom.init() LET store_zoom.cancelvalue = l_current_value LET store_zoom.title = "Select Store Code" LET store_zoom.sql = "SELECT %2 FROM fgl_zoom_store WHERE %1 ORDER BY store_num" CALL store_zoom.column[1].quick_set("store_num", TRUE, "i", 4, "Number") CALL store_zoom.column[2].quick_set("store_name", FALSE, "c", 20, "Name") CALL store_zoom.column[3].quick_set("addr", FALSE, "c", 20, "Address 1") CALL store_zoom.column[4].quick_set("addr2", FALSE, "c", 20, "Address 2") CALL store_zoom.column[5].quick_set("city", FALSE, "c", 15, "City") CALL store_zoom.column[6].quick_set("state", FALSE, "c", 2, "State") CALL store_zoom.column[7].quick_set("zipcode", FALSE, "c", 5, "Zipcode") CALL store_zoom.column[8].quick_set("phone", FALSE, "c", 18, "Phone") RETURN store_zoom.call() END FUNCTION
PRIVATE FUNCTION zoom_country(l_current_value STRING) DEFINE country_zoom zoomType CALL country_zoom.init() LET country_zoom.cancelvalue = l_current_value LET country_zoom.title = "Select Country" LET country_zoom.sql = "select %2 FROM fgl_zoom_country WHERE %1 ORDER BY country_3letter" LET country_zoom.gotolist = TRUE CALL country_zoom.column[1].quick_set("country_3letter", TRUE, "c", 3, "Code") CALL country_zoom.column[2].quick_set("country_name", FALSE, "c", 30, "Name") RETURN country_zoom.call() END FUNCTION
PRIVATE FUNCTION zoom_auto(l_current_value STRING) DEFINE auto_zoom fgl_zoom.zoomType CALL auto_zoom.init() LET auto_zoom.noqbe = TRUE LET auto_zoom.cancelvalue = l_current_value LET auto_zoom.title = "Select Value" LET auto_zoom.sql = "SELECT id, desc, date_created, time_created, quantity, price FROM fgl_zoom_test WHERE %1 ORDER BY id" -- Derive the column settings from the SQL statement CALL auto_zoom.column_auto_set() RETURN auto_zoom.call() END FUNCTION
PRIVATE FUNCTION zoom_state_label() DEFINE l_state_code CHAR(2) DEFINE l_state_name CHAR(20) DEFINE state_label_zoom zoomType CALL state_label_zoom.init() LET state_label_zoom.sql = "SELECT state_code, state_name FROM fgl_zoom_state ORDER BY state_name" LET state_label_zoom.header = FALSE LET state_label_zoom.combobox = TRUE CALL state_label_zoom.column[1].quick_set("state_code", TRUE, "c", 2, "Code") CALL state_label_zoom.column[2].quick_set("state_name", TRUE, "c", 20, "Name") CALL state_label_zoom.execute() IF state_label_zoom.ok() THEN LET l_state_code = state_label_zoom.result[1, 1] LET l_state_name = state_label_zoom.result[1, 2] END IF RETURN l_state_code, l_state_name END FUNCTION
Most of the code is self-explanatory and you can see that various options are being set from the property or function name. The %1 in the SQL statement is where the QBE where clause will be placed, the %2 takes the database columns from the column settings, the column quick_set function defines the most common settings (name, include in result, datatype, width, column title) for a column in one line, and the auto_set defines the column setting from the SQL statement. More explanation for these and the others can be found in the repository.
Hopefully you have noticed that each example has a small number of lines of code. There is no .per/.4fd, no database cursor declared, no dialog statement(s) coded. The only code required is enough to define what is unique about each particular zoom window. That is an SQL statement, some properties about each column e.g. the column title, some properties about the window itself e.g. title, and some properties about its behaviour e.g. maximum rows, display list or QBE first.
The value of a library such as fgl_zoom is that the developer is not having to repeatedly code the framework that is the same across each zoom window, the developer is only having to code what is unique about that zoom window. The benefits of this are
- quicker development
- simpler development for junior developers
- more consistent development as each zoom window will have a similar appearance
- easier to make bulk changes as you just change inside fgl_zoom once rather than repeating for each
If you want to learn more about fgl_zoom, I invite you to read the README. I also did a presentation on this at our WWDC19, see slides 34-60 from my presentation (don’t worry about the number of slides in my supposedly 1 hour presentation, I included a lot for reference). As a quick further explanation, the FOLDER PAGE labelled Custom allows you to turn the various options on/off, test it by clicking Execute, and then click View Source to see what code you need to put in your Function, and the FOLDER PAGE labelled Functional Test has many BUTTONEDIT’s that each test a particular option.
What is happening inside fgl_zoom is too much for one single Ask-Reuben but I will talk about Dynamic Dialogs, base.SqlHandle, Dom Tree manipulation, Methods separately in future Ask-Reubens. The key point for today is making you aware of fgl_zoom, where it can be found, and a little bit about it so that you will be motivated to try it and see if you can use it to cut down on your code base and improve the quality of your application. If you do decide to use it in your application, feel free to tinker with it, and tell me of any potential improvements.