by guest author Michael Höhne, 4D developer (Munich, Germany)
There’s a feature in 4D v18 R5 that may have been overlooked, or at least hasn’t gotten much attention so far: Form macros. To be honest, I hadn’t spent much time on them either, until recently. In this blog post, I’ll show you a macro that saves a lot of time when applying naming conventions to list box columns, column headers, and footers. You can easily change it to fit your needs. A dedicated repo is also available on Github.
context first …
We use macros in our daily work to ease development and it’s a really cool feature. Like many other companies, we also have coding guidelines. They’re essential when working in larger teams to understand each other’s code more easily. The 4D language didn’t change for quite a long time and neither did our guidelines. Then came ORDA, classes, and new syntaxes for variable and method declarations! It was time to incorporate these new language elements and perform a general review of our internal specs. Part of that review was form object names (though totally unrelated to the new stuff). We talked about checkboxes, radio buttons, fields, and finally list boxes. I’m not going to write about our naming styles. The important thing is that a list box doesn’t have a single object name. It has names for the list itself, each column, and each column header and footer. A simple list box with 5 columns has 16 object names. If you want to apply naming conventions to a list box, you may find yourself in a tedious process. Wouldn’t it be nice to automate it?
Object names for a list box containing companies could be:
- listCompanies for the list itself,
- listCompaniesCol1, listCompaniesCol2, …, for the column names,
- listCompaniesCol1Header, listCompaniesCol2Header, …, for the column headers,
- listCompaniesCol1Footer, listCompaniesCol2Footer, …, for the column footers.
This is just one possible way to do it. If you ask developers about their naming preferences, you’ll most likely get many different opinions. However, there may be a similar pattern as the names above: all column, header, and footer names are based on the list box name. These names can be assigned by a Form macro!
Quick Start
Form macros are available through the contextual menu of the Form designer:
If you haven’t used Form macros before, then it’s likely that your context menu will not include a Macros menu at all:
In this case, the first thing to do is to create a formMacros.json file and place it in the Sources folder of your Project:
The formMacros.json file defines the names of the menu items and the classes implementing the code. The file I’m using in this example is very simple:
{ "macros": { "Rename Listbox columns": { "class": "RenameListboxColumns" } } }
It tells 4D to add a menu item named “Rename Listbox columns” to the Macros menu (shown in the context menu of the form designer). When you click on the menu item, it’ll call the onInvoke method of a class named “RenameListboxColumns” (more on that shortly). The formMacros.json file is read once when the project is loaded. Changes to the content are not applied to the UI until you reload the project. If you’ve created the formMacros.json file right now, then close your project and open it again to see the Macros menu in the Form designer. Detailed information about Form macros is available here.
Form macros are classes
The implementation of a Form macro is inside a class. The “class” property in the macro definition (“class”: “RenameListboxColumns”) defines its name. To make our macro do anything, we need to create that class:
To see if it works, add the following code to it:
// Macro invoked in the form designer.
Function onInvoke($editor : Object)->$return : Object
ALERT("Hello world!") // Return the modified page object
$return:=New object("currentPage"; $editor.editor)
When you select the “Rename Listbox columns” in the Macros menu, you should get the alert:
This is a basic check to verify that everything is set up correctly. To show a possible pitfall, change the code to:
// Macro invoked in the form designer.
Function onInvoke($editor : Object)->$return : Object
This.showHelloWorldMessage() // Return the modified page object
$return:=New object("currentPage"; $editor.editor)
Function showHelloWorldMessage()
ALERT("Hello world!")
Select the “Rename Listbox columns” menu again:
4D loads the class definition only once. You can change the implementation of the class methods, but new methods or changes to method parameters are not available until you reload the project. This is somewhat inconvenient, but you get used to it. After reloading the project, the code will work just fine.
Adding functionality
Now that you know how to set up a Form macro, let’s add some functionality! Displaying a Hello World message is nice, but it would make sense that a macro named “Rename Listbox columns” does more. Renaming columns for instance.
The onInvoke method passes an object named $editor. This object contains information about the form you are currently working on. currentSelection ($editor.editor.currentSelection) contains the object names of all selected objects. If you haven’t selected any, it will be empty.
currentPage ($editor.editor.currentPage) contains all objects that are located on the current form page. form ($editor.editor.form) contains the entire form. These objects are simply parts of the forms’ JSON declaration. If you want to have an overview of the object structures, simply open the form.4DForm file you’re working on in a text editor.
Since we want to rename the object names of list box columns, column headers, and footers, the main application logic will be:
- Loop through the selected objects. For each list box found, do the following:
- Loop through the list box columns and calculate the object names for each column, column header, and footer based on a specific pattern.
- If one of the calculated object names is already used on the form, then abort the renaming process and display an error message to the user.
- Otherwise, rename the object.
Let’s start with the first topic:
- Loop through the selected objects. For each list box found, do the steps as described before:
// Macro invoked in the form designer.
Function onInvoke($editor : Object)->$return : Object
var $objectName : Text
var $formObject : Object
This.editor:=$editor.editor
If (This.editor.currentSelection.length>0)
For each ($objectName; This.editor.editor)
$formObject:=This.editor.currentPage.objects[$objectName]
If (This.isListbox($formObject))
This.renameListboxColumns($objectName; $formObject)
End if
End for each
End if
// Return the modified page object
$return:=New object("currentPage"; This.editor.currentPage)
This code matches the sentence (loop through the selected objects, check for list boxes, and rename their columns). But how do we identify a list box? Let’s put a breakpoint into the code and investigate:
Each object has a type property. For a list box, it is set to “listbox”. This is the same you see when opening the form.4dform file in a text editor:
// Check if the form object is a listbox
Pretty easy! Of course, this one-liner could be directly included in onInvoke instead of declaring a new method, but after roughly 30 years of object-oriented development, I’m used to creating small methods. It helps writing easy-to-understand and easy-to-maintain code.
Function isListbox($formObject : Object)->$isListbox: Boolean
$isListbox:=($formObject.type="listbox")
Now that we know how to identify a list box, let’s do the next step:
- Loop through the list box columns and calculate the object names for each column, column header, and footer based on a specific pattern.
// Renames all columns, column headers and footers based on
// the listbox object name.
Function renameListboxColumns($lbxName : Text; $listbox : Object)
var $col : Object
var $index : Integer
var $newObjectName : Text
$index:=1
For each ($col; $listbox.columns)
This.setObjectName($col; $lbxName+"Col"+String($index))
This.setObjectName($col.footer; $col.name+"Footer")
This.setObjectName($col.header; $col.name+"Header")
$index:=$index+1
End for each
Renaming an object should not lead to duplicate object names.
- If one of the calculated object names is already used on the form, then abort the renaming process and display an error message to the user.
- Otherwise, rename the object.
// Changes the object name of $formObject to the $newObjectName.
// If the new object name is different than the current name
// and form object with that name already exists on any page
// of the form, the processing is aborted and a message is shown in the designer.
Function setObjectName($formObject : Object; $newObjectName : Text)
If ($newObjectName#$formObject.name)
If (This.isObjectNameUsedInForm(This.editor.form; $newObjectName))
ALERT("Object name "$newObjectName+" already used. Renaming canceled.")
ABORT // abort further processing
Else
$formObject.name:=$newObjectName
End if
End if
Checking for an existing object name is done with a few more methods: isObjectNameUsedInForm, isObjectNameUsedInPage, and isObjectNameUsedInListbox. They’re included in the final sample available on Github. It also contains a form that you can use to test the macro:
Using form macros in your daily work
If the implementation of a Form macro is specific to a single project, then it totally makes sense to directly add it to that project. But macros like this one could be useful in any project containing forms. You can, of course, copy the class to each project and add the macro definition to each formMacros.json file, but it’s easier to create a component instead.
A Form macro component is nothing more than a 4D project with a formMacros.json file and the classes defined in it. The only difference is that you build a component and place it into the appropriate components folder of your project, or put it into the components folder of the 4D application to have it available in all projects by default.
If you just want to use the macro and don’t care about the implementation: the sample on GitHub includes the compiled component in the Build folder.