BH3509 Macros
BH3509 Macros
Chapter 9
Macros
Copyright
This document is Copyright © 2013 by its contributors as listed below. You may distribute it and/or
modify it under the terms of either the GNU General Public License
(http://www.gnu.org/licenses/gpl.html), version 3 or later, or the Creative Commons Attribution
License (http://creativecommons.org/licenses/by/3.0/), version 3.0 or later.
All trademarks within this guide belong to their legitimate owners.
Contributors
Jochen Schiffers Robert Großkopf Jost Lange
Hazel Russman Andrew Pitonyak
Feedback
Please direct any comments or suggestions about this document to:
documentation@global.libreoffice.org.
Caution Everything you send to a mailing list, including your email address and any other
personal information that is written in the mail, is publicly archived and cannot be
deleted.
Acknowledgments
This chapter is based on an original German document and was translated by Hazel Russman.
Macros 3
General remarks on macros
In principle a database in Base can manage without macros. At times, however, they may become
necessary for:
• More effective prevention of input errors
• Simplifying certain processing tasks (changing from one form to another, updating data
after input into a form, and so on)
• Allowing certain SQL commands to be called up more easily than with the separate SQL
editor.
You must decide for yourself how intensively you wish to use macros in Base. Macros can improve
usability but are always associated with small reductions in the speed of the program, and
sometimes with larger ones (when coded poorly). It is always better to start off by fully utilizing the
possibilities of the database and the provisions for configuring forms before trying to provide
additional functionality with macros. Macros should always be tested on larger databases to
determine their effect on performance.
Macros are created using Tools > Macros > Organize macros > LibreOffice Basic. A window
appears which provides access to all macros. For Base, the important area corresponds to the
filename of the Base file.
The New button in the LibreOffice Basic Macros dialog opens the New Module dialog, which asks
for the module name (the folder in which the macro will be filed). The name can be altered later if
desired.
As soon as this is given, the macro editor appears. Its input area already contains the Start and the
End for a subroutine:
REM ***** BASIC *****
Sub Main
End Sub
4 Macros
If macros are to be usable, the following steps are necessary:
• Under Tools > Options > Security > Macro security the security level should be reduced
to Medium. If necessary you can additionally use the Trusted sources tab to set the path to
your own macro files to prevent later queries about the activation of macros.
• The database file must be closed and then reopened after the creation of the first macro
module.
Some basic principles for the use of Basic code in LibreOffice:
• Lines have no line numbers and must end with a hard return.
• Functions, reserved expressions, and similar elements are not case-sensitive. So "String" is
the same as "STRING" or "string" or any other combination of upper and lower case. Case
should be used only to improve legibility. Names for constants and enumerations, however,
are case sensitive the first time that they are seen by the macro compiler, so it is best to
always write those using the proper case.
• There is a basic difference between subroutines (beginning with SUB) and functions
(beginning with FUNCTION). Subroutines are program segments without return values.
Functions return a value.
For further details see Chapter 13, Getting Started with Macros, in the Getting Started guide.
Macros in the PDF and ODT versions of this chapter are colored according to the
rules of the LibreOffice macro editor:
Macro designation
Macro comment
Note Macro operator
Macro reserved expression
Macro number
Macro character string
Improving usability
For this first category of macro use, we show various possibilities for improving the usability of
Base forms.
Improving usability 5
type of the variables, we preface their names with an "o". In principle, though, you can choose
almost any variable names you like.
DIM oDoc AS OBJECT
DIM oDrawpage AS OBJECT
DIM oForm AS OBJECT
The form lies in the currently active document. The container, in which all forms are stored, is
named drawpage. In the form navigator this is the top-level concept, to which all the forms are
subsidiary.
In this example, the form to be accessed is named Display. Display is the name visible in the form
navigator. So, for example, the first form by default is called Form1.
oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm = oDrawpage.forms.getByName("Display")
Since the form has now been made accessible and the point at which it can be accessed is saved
in the variable oForm, it is now reloaded (refreshed) with the reload() command.
oForm.reload()
END SUB
The subroutine begins with SUB so it must end with END SUB.
This macro can now be selected to run when another form is saved. For example, on a cash
register (till), if the total number of items sold and their stock numbers (read by a barcode scanner)
are entered into one form, another form in the same open window can show the names of all the
items, and the total cost, as soon as the form is saved.
Filtering records
The filter itself can function perfectly well in the form described in Chapter 8, Database Tasks. The
variant shown below replaces the Save button and reads the listboxes again, so that a chosen filter
from one listbox can restrict the choices available in the other listbox.
SUB Filter
DIM oDoc AS OBJECT
DIM oDrawpage AS OBJECT
DIM oForm1 AS OBJECT
DIM oForm2 AS OBJECT
DIM oFieldList1 AS OBJECT
DIM oFieldList2 AS OBJECT
oDoc = thisComponent
oDrawpage = oDoc.drawpage
First the variables are defined and set to access the set of forms. This set comprises the two forms
"Filter" and "Display". The listboxes are in the "Filter" form and have the names "List_1" and
"List_2".
oForm1 = oDrawpage.forms.getByName("Filter")
oForm2 = oDrawpage.forms.getByName("Display")
oFieldList1 = oForm1.getByName("List_1")
oFieldList2 = oForm1.getByName("List_2")
First the contents of the listboxes are transferred to the underlying form using commit(). The
transfer is necessary, because otherwise the change in a listbox will not be recognized when
saving. The commit() instruction need only be applied to the listbox that has just been accessed.
After that the record is saved using updateRow(). In principle, our filter table contains only one
record, which is written once at the beginning. This record is therefore overwritten continuously
using an update command.
6 Macros
oFieldList1.commit()
oFieldList2.commit()
oForm1.updateRow()
The listboxes are meant to influence each other. For example, if one listbox is used to restrict
displayed media to CDs, the other listbox should not include all the writers of books in its list of
authors. A selection in the second listbox would then all too often result in an empty filter. That is
why the listboxes must be read again. Strictly speaking, the refresh() command only needs to
be carried out on the listbox that has not been accessed.
After this, form2, which should display the filtered content, is read in again.
oFieldList1.refresh()
oFieldList2.refresh()
oForm2.reload()
END SUB
Listboxes that are to be influenced using this method can be supplied with content using various
queries.
The simplest variant is to have the listbox take its content from the filter results. Then a single filter
determines which data content will be further filtered.
SELECT "Field_1" || ' - ' || "Count" AS "Display", "Field_1"
FROM ( SELECT COUNT( "ID" ) AS "Count", "Field_1" FROM "Table_Filter_result"
GROUP BY "Field_1" )
ORDER BY "Field_1"
The field content and the number of hits is displayed. To get the number of hits, a sub-query is
used. This is necessary as otherwise only the number of hits, without further information from the
field, will be shown in the listbox.
The macro creates listboxes quite quickly by this action; they are filled with only one value. If a
listbox is not NULL, it is taken into account by the filtering. After activation of the second listbox,
only the empty fields and one displayed value are available to both listboxes. That may seem
practical for a limited search. But what if a library catalog shows clearly the classification for an
item, but does not show uniquely if this is a book, a CD or a DVD? If the classification is chosen
first and the second listbox is then set to "CD", it must be reset to NULL in order to carry out a
subsequent search that includes books. It would be more practical if the second listbox showed
directly the various media types available, with the corresponding hit counts.
To achieve this aim, the following query is constructed, which is no longer fed directly from the filter
results. The number of hits must be obtained in a different way.
SELECT
IFNULL( "Field_1" || ' - ' || "Count", 'empty - ' || "Count" ) AS "Display",
"Field_1"
FROM
( SELECT COUNT( "ID" ) AS "Count", "Field_1" FROM "Table" WHERE "ID" IN
( SELECT "Table"."ID" FROM "Filter", "Table" WHERE "Table"."Field_2" =
IFNULL( "Filter"."Filter_2", "Table"."Field_2" ) )
GROUP BY "Field_1" )
ORDER BY "Field_1"
This very complex query can be broken down. In practice it is common to use a VIEW for the sub-
query. The listbox receives its content from a query relating to this VIEW.
The query in detail: The query presents two columns. The first column contains the view seen by a
person who has the form open. This view shows the content of the field and, separated by a
hyphen, the hits for this field content. The second column transfers its content to the underlying
table of the form. Here we have only the content of the field. The listboxes thus draw their content
from the query, which is presented as the filter result in the form. Only these fields are available for
further filtering.
Improving usability 7
The table from which this information is drawn is actually a query. In this query the primary key
fields are counted (SELECT COUNT( "ID" ) AS "Count"). This is then grouped by the search
term in the field (GROUP BY "Field_1"). This query presents the term in the field itself as the
second column. This query in turn is based on a further sub-query:
SELECT "Table"."ID" FROM "Filter", "Table" WHERE "Table"."Field_2" =
IFNULL( "Filter"."Filter_2", "Table"."Field_2" )
This sub-query deals with the other field to be filtered. In principle, this other field must also match
the primary key. If there are further filters, this query can be extended:
SELECT "Table"."ID" FROM "Filter", "Table" WHERE
"Table"."Field_2" = IFNULL( "Filter"."Filter_2", "Table"."Field_2" )
AND
"Table"."Field_3" = IFNULL( "Filter"."Filter_3", "Table"."Field_3" )
This allows any further fields that are to be filtered to control what finally appears in the listbox of
the first field, "Field_1".
Finally the whole query is sorted by the underlying field.
What the final query underlying the displayed form, actually looks like, can be seen from Chapter 8,
Database Tasks.
The following macro can control through a listbox which listboxes must be saved and which must
be read in again.
The following subroutine assumes that the “Additional information” property for each listbox
contains a comma-separated list of all listbox names with no spaces. The first name in the list must
be the name of that listbox.
SUB Filter_more_info(oEvent AS OBJECT)
DIM oDoc AS OBJECT
DIM oDrawpage AS OBJECT
DIM oForm1 AS OBJECT
DIM oForm2 AS OBJECT
DIM oFieldList1 AS OBJECT
DIM oFieldList2 AS OBJECT
DIM sTag AS String
sTag = oEvent.Source.Model.Tag
An array (a collection of data accessible via an index number) is established and filled with the field
names of the listboxes. The first name in the list is the name of the listbox linked to the event.
aList() = Split(sTag, ",")
oDoc = thisComponent
oDrawpage = oDoc.drawpage
oForm1 = oDrawpage.forms.getByName("Filter")
oForm2 = oDrawpage.forms.getByName("Display")
The array is run through from its lower bound ('Lbound()') to its upper bound ('Ubound()') in
a single loop. All values which were separated by commas in the additional information, are now
transferred successively.
FOR i = LBound(aList()) TO UBound(aList())
IF i = 0 THEN
The listbox that triggered the macro must be saved. It is found in the variable aList(0). First the
information for the listbox is carried across to the underlying table, and then the record is saved.
oForm1.getByName(aList(i)).commit()
oForm1.updateRow()
ELSE
The other listboxes must be refreshed, as they now contain different values depending on the first
listbox.
8 Macros
oForm1.getByName(aList(i)).refresh()
END IF
NEXT
oForm2.reload()
END SUB
The queries for this more usable macro are naturally the same as those already presented in the
previous section.
Improving usability 9
SQL formulas in macros must first be placed in double quotes like normal character
strings. Field names and table names are already in double quotes inside the SQL
formula. To create final code that transmits the double quotes properly, field names
and table names must be given two sets of these quotes.
Note
stSql = "SELECT ""Name"" FROM ""Table"";"
becomes, when displayed with the command msgbox stSql,
SELECT "Name" FROM "Table"
The index of the array, in which the field names are written is initially set to 0. Then the query
begins to be read out. As the size of the array is unknown, it must be adjusted continuously. That is
why the loop begins with 'ReDim Preserve arContent(inI)' to set the size of the array and
at the same time to preserve its existing contents. Next the fields are read and the array index
incremented by 1. Then the array is dimensioned again and a further value can be stored.
InI = 0
IF NOT ISNULL(oQuery_Result) THEN
WHILE oQuery_result.next
ReDim Preserve arContent(inI)
arContent(inI) = oQuery_Result.getString(1)
inI = inI + 1
WEND
END IF
stSql = "DROP TABLE ""Searchtmp"" IF EXISTS"
oSQL_Statement.executeUpdate (stSql)
Now the query is put together within a loop and subsequently applied to the table defined at the
beginning. All case combinations are allowed for, since the content of the field in the query is
converted to lower case.
The query is constructed such that the results end up in the Searchtmp table. It is assumed that
the primary key is the first field in the table (arContent(0)).
stSql = "SELECT """+arContent(0)+""" INTO ""Searchtmp"" FROM """ + stTable
+ """ WHERE "
FOR inK = 0 TO (inI - 1)
stSql = stSql+"LCase("""+arContent(inK)+""") LIKE '%"+stContent+"%'"
IF inK < (inI - 1) THEN
stSql = stSql+" OR "
END IF
NEXT
oSQL_Statement.executeQuery(stSql)
ELSE
stSql = "DELETE FROM ""Searchtmp"""
oSQL_Statement.executeUpdate (stSql)
END IF
The display form must be reloaded. Its data source is a query, in this example Searchquery.
oForm2 = oDrawpage.forms.getByName("Display")
oForm2.reload()
End Sub
This creates a table that is to be evaluated by the query. As far as possible, the query should be
constructed so that it can subsequently be edited. A sample query is shown:
SELECT * FROM "Searchtable" WHERE "Nr." IN ( SELECT "Nr." FROM
"Searchtmp" ) OR "Nr." = CASE WHEN ( SELECT COUNT( "Nr." ) FROM
"Searchtmp" ) > 0 THEN '0' ELSE "Nr." END
All elements of the Searchtable are included, including the primary key. No other table appears
in the direct query; therefore no primary key from another table is needed and the query result
remains editable.
10 Macros
The primary key is saved in this example under the name Nr. The macro reads precisely this field.
There is an initial check to see if the content of the Nr. field appears in the Searchtmp table. The
IN operator is compatible with multiple values. The sub-query can also yield several records.
For larger amounts of data, value matching by using the IN operator quickly slows down.
Therefore it is not a good idea to use an empty search field simply to transfer all primary key fields
from Searchtable into the Searchtmp table and then view the data in the same way. Instead an
empty search field creates an empty Searchtmp table, so that no records are available. This is the
purpose of the second half of the condition:
OR "Nr." = CASE WHEN ( SELECT COUNT( "Nr." ) FROM "Searchtmp" ) > 0
THEN '-1' ELSE "Nr." END
If a record is found in the Searchtmp table, it means that the result of the first query is greater than
0. In this case: "Nr." = '-1' (here we need a number which cannot occur as a primary key, so
'-1'is a good value). If the query yields precisely 0 (which will be the case if no records are
present), then "Nr." = "Nr.". This will list every record which has a Nr. As Nr. is the primary
key, this means all records.
Improving usability 11
IF isMissing(NameTable2) THEN NameTable2 = ""
IF isMissing(NameTab12ID) THEN NameTab12ID = ""
IF isMissing(Position) THEN Position = 2
The IF condition here is only one line, so it does not require an END IF.
After this, the variables are declared. Some variables have already been declared globally in a
separate module and are not declared here again.
DIM oForm AS OBJECT
DIM oSubForm AS OBJECT
DIM oSubSubForm AS OBJECT
DIM oField AS OBJECT
DIM oFieldList AS OBJECT
DIM stFieldValue AS STRING
DIM inID AS INTEGER
DIM oCtlView AS OBJECT
oDoc = thisComponent
oDrawpage = oDoc.Drawpage
oForm = oDrawpage.forms.getByName(NameForm)
The position of the field in the corresponding form is determined. All forms are held on the
Drawpage of the current document thisComponent. This subroutine uses arguments to cover
the possibility that the field is contained in a subform of a subform, that is two levels down from the
actual form. The field that contains the foreign key is named oField. The combobox, which now
exists instead of a listbox is named oFieldList.
IF NameSubform <> "" THEN
oSubForm = oForm.getByName(NameSubform)
IF NameSubSubform <> "" THEN
oSubSubForm = oSubForm.getByName(NameSubSubform)
oField = oSubSubForm.getByName(NameIDField)
oFieldList = oSubSubForm.getByName(NameField)
ELSE
oField = oSubForm.getByName(NameIDField)
oFieldList = oSubForm.getByName(NameField)
END IF
ELSE
oField = oForm.getByName(NameIDField)
oFieldList = oForm.getByName(NameField)
END IF
oFieldList.Refresh()
The combobox is read in again using Refresh(). It might be that the content of the field has been
changed by the entry of new data. This must be made possible.
After this, the value of the foreign key is read. Only if a value is entered here will a connection be
made to the data source.
IF NOT IsEmpty(oField.getCurrentValue()) THEN
inID = oField.getCurrentValue()
oDataSource = ThisComponent.Parent.CurrentController
IF NOT (oDataSource.isConnected()) Then
oDataSource.connect()
End IF
oConnection = oDataSource.ActiveConnection()
oSQL_Statement = oConnection.createStatement()
The SQL statement is formulated according to the fields used in the combobox. This requires the
testing of various combinations. The most general is that the combobox must be provided with a
query that refers to two table fields from different tables. This subroutine is not designed for further
possibilities. The test begins with this most important possibility.
IF NameTableField2 <> "" THEN
12 Macros
If a second table field exists,
IF NameTable2 <> "" THEN
... and if a second table exists, the following SQL code will be produced:
IF Position = 2 THEN
stSql = "SELECT """ + NameTable1 + """.""" + NameTableField1 + """||'" +
Fieldseparator + "'||""" + NameTable2 + """.""" + NameTablenField2 + """ FROM
""" + NameTable1 + """,""" + NameTable2 + """ WHERE ""ID""='" +inID+ "' AND
""" + NameTable1 + """.""" + NameTab12ID + """=""" + NameTable2 + """.""ID"""
ELSE
stSql = "SELECT """ + NameTable2 + """.""" + NameTableField2 + """||'" +
Fieldseparator + "'||""" + NameTable1 + """.""" + NameTableField1 + """ FROM
""" + NameTable1 + """,""" + NameTable2 + """ WHERE ""ID""='" + inID + "' AND
""" + NameTable1 + """.""" + NameTab12ID + """=""" + NameTable2 + """.""ID"""
END IF
When written out in the form required by Basic, this SQL command is pretty incomprehensible.
Each field and each table name must be written into the SQL input with two sets of double quotes
as shown above. As double quotes are normally interpreted by Basic as indicating text, they
disappear when the code is transferred. Only when a second set of double quotes is used are
terms passed on in simple quotes. ""ID"" therefore means that the "ID" field (with a single set
of quotation marks for the SQL relationship) is accessed in the query. A partial simplification for this
subroutine is that all primary keys in this database carry the name ID.
The code is further complicated by the fact that, as well as requiring duplicate quotation marks,
some of the included table fields are entered as variables. These are not text; they are simply
concatenated with the preceding text by using + . But these variables must also be masked. That
is why these variables are shown above with three sets of quotation marks:
"""+NameTable1+"""."""+NameTableField1+""" finallly translates into the familiar query
language "Table1"."Field1". Fortunately when we create the macro, the coloring of the code
shows if any double quotes have been omitted. The quotation marks change their color
immediately if they are not recognised by the macro as string delimiters.
ELSE
... and if the second table does not exist, the following SQL code is created:
IF Position = 2 THEN
stSql = "SELECT """ + NameTableField1 + """||'" + Fieldseparator +
"'||""" + NameTableField2 + """ FROM """ + NameTable1 + """ WHERE ""ID""='" +
inID + "'"
ELSE
stSql = "SELECT """ + NameTableField2 + """||'" + Fieldseparator +
"'||""" + NameTableField1 + """ FROM """ + NameTable1 + """ WHERE ""ID""='" +
inID + "'"
END IF
END IF
ELSE
If a second table field does not exist there can be only one table. This yields the following query:
stSql = "SELECT """ + NameTableField1 + """ FROM """ + NameTable1 +
""" WHERE ""ID""='" + inID + "'"
END IF
The query stored in the variable stSql is now run and the result of the query is stored in the
variable oQuery_result.
oQuery_result = oSQL_Statement.executeQuery(stSql)
The query result is read in a loop. Here, as in GUI queries, several fields and records can be
created. But the construction of this query will produce only one result. This result will be found in
the first column (1) of the query. It is the record which provides the content which the combobox is
Improving usability 13
to display. The content is a text string ('getString()'), so here we see
'oQuery_result.getString(1)'.
IF NOT IsNull(oQuery_result) THEN
WHILE oQuery_result.next
stFieldValue = oQuery_result.getString(1)
WEND
The combobox must now be set to the text values resulting from the query. This requires access to
the Controller.
oDocCrl = ThisComponent.getCurrentController()
oCtlView = oDocCrl.GetControl(oFieldList)
oCtlView.setText(stFieldValue)
END IF
END IF
If there is no value in the field for the foreign key 'oField', it means that the query has failed. The
combobox is then set to an empty display.
IF IsEmpty(oField.getCurrentValue()) THEN
oDocCrl = ThisComponent.getCurrentController()
oCtlView = oDocCrl.GetControl(oFieldList)
oCtlView.setText("")
END IF
END SUB
This subroutine handles the contact between the foreign keys in a hidden field and the combobox.
This is sufficient for the display of the correct values in the combobox. To store a new value
requires a further subroutine.
14 Macros
DIM inID1 AS INTEGER
DIM inID2 AS INTEGER
DIM Field1Length AS INTEGER
DIM Field2Length AS INTEGER
The maximum length allowed for an entry is determined using the function Columnsize, described
below. Just setting a limit on the size of the combobox is not safe here, as we need to include the
possibility of entering two fields together.
Field1Length = Columnsize(NameTable1,NameTableField1)
IF NameTableField2 <> "" THEN
IF NameTable2 <> "" THEN
Feld2Length = Columnsize(NameTable2,NameTableField2)
ELSE
Feld2Length = Columnsize(NameTable1,NameTableField2)
END IF
ELSE
Feld2Length = 0
END IF
The form is loaded, and the comobox is read.
oDoc = thisComponent
oDrawpage = oDoc.Drawpage
oForm = oDrawpage.Forms.getByName(NameForm)
IF NameSubform <> "" THEN
oSubForm = oForm.getByName(NameSubform)
IF NameSubSubform <> "" THEN
oSubSubForm = oSubForm.getByName(NameSubSubform)
oField = oSubSubForm.getByName(NameIDField)
oFieldList = oSubSubForm.getByName(NameField)
ELSE
oField = oSubForm.getByName(NameIDField)
oFieldList = oSubForm.getByName(NameField)
END IF
ELSE
oField = oForm.getByName(NameIDField)
oFieldList = oForm.getByName(NameField)
END IF
stContent = oFieldList.getCurrentValue()
The displayed content of the combobox is read. Leading and trailing spaces and non-printing
characters are removed if necessary.
StContent = Trim(stContent)
IF stContent <> "" THEN
IF NameTableField2 <> "" THEN
If a second table field exists, the content of the combobox must be split. To determine where the
split is to occur, we use the field separator provided to the function as an argument.
a_stPart = Split(stContent, FieldSeparator, 2)
The last parameter signifies that the maximum number of parts is 2.
Depending on which entry corresponds to field 1 and which to field 2, the content of the combobox
is now allocated to the individual variables. "Position = 2" serves here as a sign that the second
part of the content stands for Field 2.
IF Position = 2 THEN
stContent = Trim(a_stPart(0))
IF UBound(a_stPart()) > 0 THEN
stContentField2 = Trim(a_stPart(1))
ELSE
stContentField2 = ""
END IF
Improving usability 15
stContentField2 = Trim(a_stPart(1))
ELSE
stContentField2 = Trim(a_stPart(0))
IF UBound(a_stPart()) > 0 THEN
stContent = Trim(a_stPart(1))
ELSE
stContent = ""
END IF
stContent = Trim(a_stPart(1))
END IF
END IF
It can happen that with two separable contents, the installed size of the combobox (text length)
does not fit the table fields to be saved. For comboboxes that represent a single field, this is
normally handled by suitably configuring the form control. Here by contrast, we need some way of
catching such errors. The maximum permissible length of the relevant field is checked.
IF (Field1Length > 0 AND Len(stContent) > Field1Length) OR (Field2Length >
0 AND Len(stContentField2) > Field2Length) THEN
If the field length of the first or second part is too big, a default string is stored in one of the
variables. The character CHR(13) is used to put in a line break .
stmsgbox1 = "The field " + NameTableField1 + " must not exceed " +
Field1Length + "characters in length." + CHR(13)
stmsgbox2 = "The field " + NameTableField2 + " must not exceed " +
Field2Length + "characters in length." + CHR(13)
If both field contents are too long, both texts are displayed.
IF (Field1Length > 0 AND Len(stContent) > Field1Length) AND
(Field2Length > 0 AND Len(stContentField2) > Field2Length) THEN
msgbox ("The entered text is too long." + CHR(13) + stmsgbox1 +
stmsgbox2 + "Please shorten it.",64,"Invalid entry")
The display uses the msgbox() function. This expects as its first argument a text string, then
optionally a number (which determines the type of message box displayed), and finally an optional
text string as a title for the window. The window will therefore have the title "Invalid entry" and the
number '64' provides a box containing the Information symbol.
The following code covers any further cases of excessively long text that might arise.
ELSEIF (Field1Length > 0 AND Len(stContent) > Field1Length) THEN
msgbox ("The entered text is too long." + CHR(13) + stmsgbox1 +
"Please shorten it.",64,"Invalid entry")
ELSE
msgbox ("The entered text is too long." + CHR(13) + stmsgbox2 +
"Please shorten it.",64,"Invalid entry")
END IF
ELSE
If there is no excessively long text, the function can proceed. Otherwise it exits here.
First variables are preallocated which can subsequently be altered by the query. The variables
inID1 and inID2 store the content of the primary key fields of the two tables. If a query yields no
results, Basic assigns these integer variable a value of 0. However this value could also indicate a
successful query returning a primary key value of 0; therefore the variable is preset to -1. HSQLDB
cannot set this value for an autovalue field.
Next the database connection is set up, if it does not already exist.
inID1 = -1
inID2 = -1
oDataSource = ThisComponent.Parent.CurrentController
If NOT (oDataSource.isConnected()) Then
oDataSource.connect()
16 Macros
End If
oConnection = oDataSource.ActiveConnection()
oSQL_Statement = oConnection.createStatement()
IF NameTableField2 <> "" AND NOT IsEmpty(stContentField2) AND
NameTable2 <> "" THEN
If a second table field exists, a second dependency must first be declared.
stSql = "SELECT ""ID"" FROM """ + NameTable2 + """ WHERE """ +
NameTableField2 + """='" + stContentField2 + "'"
oQuery_result = oSQL_Statement.executeQuery(stSql)
IF NOT IsNull(oQuery_result) THEN
WHILE oQuery_result.next
inID2 = oQuery_result.getInt(1)
WEND
END IF
IF inID2 = -1 THEN
stSql = "INSERT INTO """ + NameTable2 + """ (""" +
NameTableField2 + """) VALUES ('" + stContentField2 + "') "
oSQL_Statement.executeUpdate(stSql)
stSql = "CALL IDENTITY()"
If the content within the combobox is not present in the corresponding table, it is inserted there.
The primary key value which results is then read. If it is present, the existing primary key is read in
the same way. The function uses the automatically generated primary key fields (IDENTITY).
oQuery_result = oSQL_Statement.executeQuery(stSql)
IF NOT IsNull(oQuery_result) THEN
WHILE oQuery_result.next
inID2 = oQuery_result.getInt(1)
WEND
END IF
END IF
The primary key for the second value is temporarily stored in the variable inID2 and then written
as a foreign key into the table corresponding to the first value. According to whether the record
from the first table was already available, the content is freshly saved (INSERT) or altered
(UPDATE):
IF inID1 = -1 THEN
stSql = "INSERT INTO """ + NameTable1 + """ (""" +
NameTableField1 + """,""" + NameTab12ID + """) VALUES ('" + stContent + "','"
+ inID2 + "') "
oSQL_Statement.executeUpdate(stSql)
And the corresponding ID directly read out:
stSql = "CALL IDENTITY()"
oQuery_result = oSQL_Statement.executeQuery(stSql)
IF NOT IsNull(oQuery_result) THEN
WHILE oQuery_result.next
inID1 = oQuery_result.getInt(1)
WEND
END IF
The primary key for the first table must finally be read again so that it can be transferred to the
form's underlying table.
ELSE
stSql = "UPDATE """ + NameTable1 + """ SET """ + NameTab12ID +
"""='" + inID2 + "' WHERE """ + NameTableField1 + """ = '" + stContent + "'"
oSQL_Statement.executeUpdate(stSql)
END IF
END IF
Improving usability 17
In the case where both the fields underlying the combobox are in the same table (for example
Surname and Firstname in the Name table), a different query is needed:
IF NameTableField2 <> "" AND NameTable2 = "" THEN
stSql = "SELECT ""ID"" FROM """ + NameTable1 + """ WHERE """ +
NameTableField1 + """='" + stContent + "' AND """ + NameTableField2 + """='"
+ stContentField2 + "'"
oQuery_result = oSQL_Statement.executeQuery(stSql)
IF NOT IsNull(oAbfrageergebnis) THEN
WHILE oQuery_result.next
inID1 = oQuery_result.getInt(1)
WEND
END IF
IF inID1 = -1 THEN
... and a second table does not exist:
stSql = "INSERT INTO """ + NameTable1 + """ (""" + NameTableField1 +
""",""" + NameTableField2 + """) VALUES ('" + stContent + "','" +
stContentField2 + "') "
oSQL_Statement.executeUpdate(stSql)
Then the primary key is read again.
stSql = "CALL IDENTITY()"
oquery_result = oSQL_Statement.executeQuery(stSql)
IF NOT IsNull(oQuery_result) THEN
WHILE oQuery_result.next
inID1 = oQuery_result.getInt(1)
WEND
END IF
END IF
END IF
IF NameTableField2 = "" THEN
Now we consider the simplest case: The second table field does not exist and the entry is not yet
present in the table. In other words, a single new value has been entered into the combobox.
stSql = "SELECT ""ID"" FROM """ + NameTable1 + """ WHERE """ +
NameTableField1 + """='" + stContent + "'"
oQuery_result = oSQL_Statement.executeQuery(stSql)
IF NOT IsNull(oQuery_result) THEN
WHILE oQuery_result.next
inID1 = oQuery_result.getInt(1)
WEND
END IF
IF inID1 = -1 THEN
If there is no second field, the content of the box is inserted as a new record.
stSql = "INSERT INTO """ + NameTable1 + """ (""" + NameTableField1 +
""") VALUES ('" + stContent + "') "
oSQL_Statement.executeUpdate(stSql)
… and the resulting ID directly read out.
stSql = "CALL IDENTITY()"
oQuery_result = oSQL_Statement.executeQuery(stSql)
IF NOT ISNULL(oQuery_result) THEN
WHILE oQuery_result.next
inID1 = oQuery_result.getInt(1)
WEND
END IF
END IF
END IF
18 Macros
The value of the primary key field must be determined, so that it can be transferred to the main part
of the form.
Next the primary key value that has resulted from all these loops is transferred to the invisible field
in the main table and the underlying database. The table field linked to the form field is reached by
using 'BoundField'. 'updateInt' places an integer (see under numerical type definitions) in
this field.
oField.BoundField.updateInt(inID)
END IF
ELSE
If no primary key is to be entered, because there was no entry in the combobox or that entry was
deleted, the content of the invisible field must also be deleted. updateNull() is used to fill the
field with the database-specific expression for an empty field, NULL.
oField.BoundField.updateNull()
END IF
END SUB
Improving usability 19
The comment lines show the list of parameters that need to be provided for the subroutine. An
empty parameter is represented by a pair of double quotes. The last three parameters are optional
as they are covered where necessary by default values in the subroutine.
The TextDisplay subroutine is called. The form that contains the fields is Filter > Form >
Address. We are therefore dealing with the subform of a subform.
The first combobox, in which the street is entered, is called comStr, the hidden foreign key field in
the table underlying the form is called numStrID. In this first combobox the field Street is
displayed. The table, which will contain the entries from the combobox, is also called Street.
The town and postcode are entered into the second combobox. This is called comPlcTown. The
hidden foreign key field is called numPlcTownID. This second combobox contains the Postcode
and Town, separated by a space (" "). The first table has the name Postcode, the second table
the name Town. In the first table the foreign key for the second table is called Town_ID. The field
for the second table is the second element in the combobox, that is position 2.
20 Macros
An array is populated; the field name comes first and then the form names, with the main form
preceding the subform.
oDoc = thisComponent
oDrawpage = oDoc.Drawpage
oForm = oDrawpage.Forms.getByName(aForms(1))
IF UBound(aForms()) > 1 THEN
oForm = oForm.getByName(aForms(2))
IF UBound(aForms()) > 2 THEN
oForm = oForm.getByName(aForms(3))
END IF
END IF
oField = oForm.getByName(aForms(0))
oField.BoundField.updateInt(-1)
END SUB
Improving usability 21
IF UBound(aForms2()) = 0 THEN
ThisDatabaseDocument.FormDocuments.getByName( Trim(aForms2(0)) ).close
ELSE
ThisDatabaseDocument.FormDocuments.getByName(
Trim(aForms2(0)) ).getByName( Trim(aForms2(1)) ).close
END IF
END SUB
Form documents that lie in a directory are entered into the Additional Information field as
directory/form. This must be converted to:
...getByName("Directory").getByName("Form").
Sub Toolbar_restore
DIM oFrame AS OBJECT
DIM oLayoutMng AS OBJECT
oFrame = thisComponent.CurrentController.Frame
oLayoutMng = oFrame.LayoutManager
oLayoutMng.visible = true
End Sub
When a toolbar is removed, all bars are affected. However as soon as a form control is clicked, the
menu bar reappears. This is a safety precaution so that the user does not end up in a jam. To
prevent constant toggling, the menu bar reappears.
22 Macros
IF NOT (oDataSource.isConnected()) Then
oDataSource.connect()
End IF
oConnection = oDataSource.ActiveConnection()
Here the database is known so a username and a password are not necessary, as these are
already switched off in the basic HSQLDB configuration for internal version.
For forms outside Base, the connection is made through the first form:
oDataSource = Thiscomponent.Drawpage.Forms(0)
oConnection = oDataSource.activeConnection
Database compaction
This is simply a SQL command (SHUTDOWN COMPACT), which should be carried out now and
again, especially after a lot of data has been deleted. The database stores new data, but still
reserves the space for the deleted data. In cases where the data have been substantially altered,
you therefore need to compact the database.
Once compaction is carried out, the tables are no longer accessible. The file must be reopened.
Therefore this macro closes the form from which it is called. Unfortunately you cannot close the
document itself without causing a recovery when it is opened again. Therefore this function is
commented out.
SUB Database_compaction
DIM stMessage AS STRING
oDataSource = ThisComponent.Parent.CurrentController ' Accessible from
the form
IF NOT (oDataSource.isConnected()) THEN
oDataSource.connect()
END IF
oConnection = oDataSource.ActiveConnection()
oSQL_Statement = oConnection.createStatement()
stSql = "SHUTDOWN COMPACT" ' The database is being compacted and shut down
oSQL_Statement.executeQuery(stSql)
stMessage = "The database is being compacted." + CHR(13) + "The form will
now close."
stMessage = stMessage + CHR(13) + "Following this, the database file
should be closed."
stMessage = stMessage + CHR(13) + "The database can only be accessed after
reopening the database file."
msgbox stMessage
ThisDatabaseDocument.FormDocuments.getByName( "Maintenance" ).close
REM The closing of the database file causes a recovery operation when you
open it again.
' ThisDatabaseDocument.close(True)
END SUB
24 Macros
value of the key. The following subroutine reads the currently highest value of the "ID" field in a
table and sets the next initial key value 1 higher than this maximum.
If the primary key field is not called ID, the macro must be edited accordingly.
SUB Table_index_down(stTable AS STRING)
REM This subroutine sets the automatically incrementing primary key field
mit the preset name of "ID" to the lowest possible value.
DIM inCount AS INTEGER
DIM inSequence_Value AS INTEGER
oDataSource = ThisComponent.Parent.CurrentController ' Accessible through
the form
IF NOT (oDataSource.isConnected()) THEN
oDataSource.connect()
END IF
oConnection = oDataSource.ActiveConnection()
oSQL_Statement = oConnection.createStatement()
stSql = "SELECT MAX(""ID"") FROM """+stTable+"""" ' The highest value in
"ID" is determined
oQuery_result = oSQL_Statement.executeQuery(stSql) ' Query is launched and
the return value stored in the variable oQuery_result
IF NOT ISNULL(oQuery_result) THEN
WHILE oQuery_result.next
inCount = oQuery_result.getInt(1) ' First data field is read
WEND ' next record, in this case none as only one record exists
IF inCount = "" THEN ' If the highest value is not a value, meaning the
table is empty, the highest value is set to -1
inCount = -1
END IF
inSequence_Value = inCount+1 ' The highrst value is increased by 1
REM A new command is prepared for the database. The ID will start
afresh from inCount+1.
REM This statement has no return value, as no record is being read
oSQL_statement = oConnection.createStatement()
oSQL_statement.executeQuery("ALTER TABLE """ + stTable + """ ALTER
COLUMN ""ID"" RESTART WITH " + inSequence_Value + "")
END IF
END SUB
Dialogs
Input errors in fields are often only noticed later. Often it is necessary to modify identical entries in
several records at the same time. It is awkward to have to do this in normal table view, especially
when several records must be edited, as each record requires an individual entry to be made.
Forms can use macros to do this kind of thing, but to do it for several tables, you would need
identically constructed forms. Dialogs can do the job. A dialog can be supplied at the beginning
with the necessary data for appropriate tables and can be called up by several different forms.
Dialogs 25
Dialogs are saved along with the modules for macros. Their creation is similar to that of a form.
Very similar control fields are available. Only the table control of forms is absent as a special entry
possibility.
The appearance of dialog controls is determined by the settings for the graphical user interface.
The dialog shown above serves in the example database to edit tables which are not used directly
as the basis of a form. So, for example, the media type is accessible only through a listbox (in the
macro version it becomes a combobox). In the macro version, the field contents can be expanded
by new content but an alteration of existing content is not possible. In the version without macros,
alterations are carried out using a separate table control.
While alterations in this case are easy to carry out without macros, it is quite difficult to change the
media type of many media at once. Suppose the following types are available: "Book, bound",
"Book, hard-cover", "Paperback" and "Ringfile". Now it turns out, after the database has been in
use for a long time, that more active contemporaries foresaw similar additional media types for
printed works. The task of differentiating them has become excessive. We therefore wish to reduce
them, preferably to a single term. Without macros, the records in the media table would have to be
found (using a filter) and individually altered. If you know SQL, you can do it much better using a
SQL command. You can change all the records in the Media table with a single entry. A second
SQL command then removes the now surplus media types which no longer have any link to the
26 Macros
Media table. Precisely this method is applied using this dialog's Replace With box – only the SQL
command is first adapted to the Media Type table using a macro that can also edit other tables.
Often entries slip into a table which with hindsight can be changed in the form, and so are no
longer needed. It does no harm simply to delete such orphaned entries, but they are quite hard to
find using the graphical user interface. Here again a suitable SQL command is useful, coupled with
a delete instruction. This command for affected tables is included in the dialog under Delete all
superfluous entries.
If the dialog is to be used to carry out several changes, this is indicated by the Edit multiple records
checkbox. Then the dialog will not simply terminate when the OK button is clicked.
The macro code for this dialog can be seen in full in the example database. Only excerpts are
explained below.
SUB Table_purge(oEvent AS OBJECT)
The macro should be launched by entering into the Additional information section for the relevant
buttons:
0: Form, 1: Subform, 2: SubSubform, 3: Combobox or table control, 4: Foreign
key field in a form, empty for a table control, 5: Table name of auxiliary
table, 6: Table field1 of auxiliary table, 7: Table field2 of auxiliary
table, or 8: Table name of auxiliary table for table field2
The entries in this area are listed at the beginning of the macro as comments. The numbers bound
to them are transferred and the relevant entry is read from an array. The macro can edit listboxes,
which have two entries, separated by ">". These two entries can also come from different tables
and be brought together using a query, as for instance in the Postcode table, which has only the
foreign key field Town_ID for the town, requiring the Town table to display the names of towns.
DIM aForeignTable(0, 0 to 1)
DIM aForeignTable2(0, 0 to 1)
Among the variables defined at the beginning are two arrays. While normal arrays can be created
by the Split() command during execution of the subroutine, two-dimensional arrays must be
defined in advance. Two-dimensional arrays are necessary to store several records from one query
when the query itself refers to more than one field. The two arrays declared above must be able to
interpret queries that refer to two table fields. Therefore they are defined for two different contents
by using 0 to 1 for the second dimension.
stTag = oEvent.Source.Model.Tag
aTable() = Split(stTag, ", ")
FOR i = LBound(aTable()) TO UBound(aTable())
aTable(i) = trim(aTable(i))
NEXT
The variables provided are read. The sequence is that set up in the comment above. There is a
maximum of nine entries, and you need to declare if an eighth entry for the table field2 and a
nineth entry for a second table exist.
If values are to be removed from a table, it is first necessary to check that they do not exist as
foreign keys in some other table. In simple table structures a given table will have only one foreign
key connection to another table. However, in the given example database, there is a Town table
which is used for both the place of publication of media and the town for addresses. Thus the
primary key of the Town table is entered twice into different tables. These tables and foreign key
names can naturally also be entered using the Additional Information field. It would be nicer though
if they could be provided universally for all cases. This can be done using the following query.
stSql = "SELECT ""FKTABLE_NAME"", ""FKCOLUMN_NAME"" FROM
""INFORMATION_SCHEMA"".""SYSTEM_CROSSREFERENCE"" WHERE ""PKTABLE_NAME"" = '"
+ aTable(5) + "'"
In the database, the INFORMATION_SCHEMA area contains all information about the tables of the
database, including information about foreign keys. The tables that contain this information can be
Dialogs 27
accessed using "INFORMATION_SCHEMA"."SYSTEM_CROSSREFERENCE". KTABLE_NAME"
gives the table that provides its primary key for the connection. FKTABLE_NAME gives the table
that uses this primary key as a foreign key. Finally FKCOLUMN_NAME gives the name of the
foreign key field.
The table that provides its primary key for use as a foreign key is in the previously created array at
position 6. A the count begins with 0, the value is read from the array using aTable(5).
inCount = 0
stForeignIDTab1Tab2 = "ID"
stForeignIDTab2Tab1 = "ID"
stAuxiltable = aTable(5)
Before the reading of the arrays begins, some default values must be set. These are the index for
the array in which the values from the auxiliary table will be written, the default primary key if we do
not need the foreign key for a second table, and the default auxiliary table, linked to the main table,
for postcode and town, the Postcode table.
When two fields are linked for display in a listbox, they can, as described above, come from two
different tables. For the display of Postcode and town the query is:
SELECT "Postcode"."Postcode" || ' > ' || "Town"."Town" FROM "Postcode", "Town" WHERE
"Postcode"."Town_ID" = "Town"."ID"
The table for the first field (Postcode), is linked to the second table by a foreign key. Only the
information from these two tables and the Postcode and Town fields is passed to the macro. All
primary keys are by default called ID in the example database. The foreign key of Town in
Postcode must therefore be determined using the macro.
In the same way the macro must access each table with which the content of the listbox is
connected by a foreign key.
oQuery_result = oSQL_Statement.executeQuery(stSql)
IF NOT ISNULL(oQuery_result) THEN
WHILE oQuery_result.next
ReDim Preserve aForeignTable(inCount,0 to 1)
The array must be freshly dimensioned each time. In order to preserve the existing contents, they
are backed up using (Preserve).
aForeignTables(inCount,0) = oQuery_result.getString(1)
Reading the first field with the name of the table which contains the foreign key. The result for the
Postcode table is the Address table.
aForeignTables(inCount,1) = oQuery_result.getString(2)
Reading the second field with the name of the foreign key field. The result for the Postcode table is
the field Postcode_ID in the Address table.
In cases where a call to the subroutine includes the name of a second table, the following loop is
run. Only when the name of the second table occurs as the foreign key table for the first table is
the default entry changed. In our case this does not occur, as the Town table has no foreign key
from the Postcode table. The default entry for the auxiliary table therefore remains Postcode; finally
the combination of postcode and town is a basis for the Address table, which contains a foreign
key from the Postcode table.
IF UBound(aTable()) = 8 THEN
IF aTable(8) = aForeignTable(inCount,0) THEN
stForeignIDTab2Tab1 = aForeignTable(inCount,1)
stAuxiltable = aTable(8)
END IF
END IF
inCount = inCount + 1
28 Macros
As further values may need to be read in, the index is incremented to redimension the arrays. Then
the loop ends.
WEND
END IF
If, when the subroutine is called, a second table name exists, the same query is launched for this
table:
IF UBound(aTable()) = 8 THEN
It runs identically except that the loop tests whether perhaps the first table name occurs as a
foreign key table name. That is the case here: the Postcode table contains the foreign key
Town_ID from the Town table. This foreign key is now assigned to the variable
stForeignIDTab1Tab2, so that the relationship between the tables can be defined.
IF aTable(5) = aForeignTable2(inCount,0) THEN
stForeignIDTab1Tab2 = aForeignTable2(inCount,1)
END IF
After a few further settings to ensure a return to the correct form after running the dialog
(determining the line number of the form, so that we can jump back to that line number after a new
read), the loop begins, which recreates the dialog when the first action is completed but the dialog
is required to be kept open for further actions. The setting for repetition takes place using the
corresponding checkbox
DO
Before the dialog is launched, first of all the content of the listboxes is determined. Care must be
taken if the listboxes represent two table fields and perhaps even are related to two different
tables.
IF UBound(aTable()) = 6 THEN
The listbox relates to only one table and one field, as the argument array ends at Tablefield1 of the
auxiliary table.
stSql = "SELECT """ + aTable(6) + """ FROM """ + aTable(5) + """
ORDER BY """ + aTable(6) + """"
ELSEIF UBound(aTable()) = 7 THEN
The listbox relates to two table fields but only one table, as the argument array ends at Tablefield2
of the auxiliary table.
stSql = "SELECT """ + aTable(6) + """||' > '||""" + aTable(7) + """
FROM """ + aTable(5) + """ ORDER BY """ + aTable(6) + """"
ELSE
The listbox is based on two table fields from two tables. This query corresponds to the example
with the postcode and the town.
stSql = "SELECT """ + aTable(5) + """.""" + aTable(6) + """||' >
'||""" + aTable(8) + """.""" + aTable(7) + """ FROM """ + aTable(5) + """,
""" + aTable(8) + """ WHERE """ + aTable(8) + """.""" + stForeignIDTab2Tab1 +
""" = """ + aTable(5) + """.""" + stForeignIDTab1Tab2 + """ ORDER BY """ +
aTable(6) + """"
END IF
Here we have the first evaluation to determine the foreign keys. The variables
stForeignIDTab2Tab1 and stForeignIDTab1Tab2 start with the value ID. For stForeignIDTab1Tab2
evaluation of the previous query yields a different value, namely the value of Town_ID. In this way
the previous query construction yields exactly the content already formulated for postcode and
town – only enhanced by sorting.
Now we must make contact with the listboxes, to supply them with the content returned by the
queries. These listboxes do not yet exist, since the dialog itself has not yet been created. This
dialog is created first in memory, using the following lines, before it is actually drawn on the screen.
Dialogs 29
DialogLibraries.LoadLibrary("Standard")
oDlg = CreateUnoDialog(DialogLibraries.Standard.Dialog_Table_purge)
Next come the settings for the fields of the dialog. Here, for example, is the listbox which is to be
supplied with the results of the above query:
oCtlList1 = oDlg.GetControl("ListBox1")
oCtlList1.addItems(aContent(),0)
Access to the fields of the dialog is accomplished by using GetControl with the appropriate
name. In dialogs it is not possible for two fields to use the same name as this would create
problems when evaluating the dialog.
The listbox is supplied with the contents of the query, which have been stored in the array
aContent() . The listbox contains only the content to be displayed as a field, so only the position 0
is filled.
After all fields with the desired content have been filled, the dialog is launched.
Select Case oDlg.Execute()
Case 1'Case 1 means the "OK" button has been clicked
Case 0'If it was the "Cancel" button
inRepetition = 0
End Select
LOOP WHILE inRepetition = 1
The dialog runs repeatedly as long as the value of "inRepetition" is 1. This is set by the
corresponding checkbox.
Here, in brief, is the content after the "OK" button is clicked:
Case 1
stInhalt1 = oCtlList1.getSelectedItem() 'Read value from Listbox1 ...
REM ... and determine the corresponding ID-value.
The ID value of the first listbox is stored in the variable "inLB1".
stText = oCtlText.Text ' Read the field value.
If the text field is not empty, the entry in the text field is handled. Neither the listbox for a
replacement value nor the checkbox for deleting all orphaned records are considered. This is made
clear by the fact that text entry sets these other fields to be inactive.
IF stText <> "" THEN
If the text field is not empty, the new value is written in place of the old one using the previously
read ID field in the table. There is the possibility of two entries, as is also the case in the listbox.
The separator is >. For two entries in different tables, two UPDATE-commands must be launched,
which are created here simultaneously and forwarded, separated by a semicolon.
ELSEIF oCtlList2.getSelectedItem() <> "" THEN
If the text field is empty and the listbox 2 contains a value, the value from listbox 1 must be
replaced by the value in listbox 2. This means that all records in the tables for which the records in
the listboxes are foreign keys must be checked and, if necessary, written with an altered foreign
key.
stInhalt2 = oCtlList2.getSelectedItem()
REM Read value from listbox.
REM Determine ID for the value of the listbox.
The ID value of the second listbox is stored in the variable inLB2. Here too, things develop
differently depending on whether one or two fields are contained in the listbox, and also on whether
one or two tables are the basis of the listbox content.
The replacement process depends on which table is defined as the table which supplies the
foreign key for the main table. For the obove example, this is the Postcode table, as the
Postcode_ID is the foreign key which is forwarded through Listbox 1 and Listbox 2.
30 Macros
IF stAuxilTable = aTable(5) THEN
FOR i = LBound(aForeignTables()) TO UBound(aForeignTables())
Replacing the old ID value by the new ID value becomes problematic in n:m-relationships, as in
such cases, the same value can be assigned twice. That might be what you want, but it must be
prevented when the foreign key forms part of the primary key. So in the table rel_Media_Author a
medium cannot have the same author twice because the primary key is constructed from
Media_ID and Author_ID. In the query, all key fields are searched which collectively have the
property UNIQUE or were defined as foreign keys with the UNIQUE property using an index.
So if the foreign key has the UNIQUE property and is already represented there with the desired
future inLB2, that key cannot be replaced.
stSql = "SELECT ""COLUMN_NAME"" FROM
""INFORMATION_SCHEMA"".""SYSTEM_INDEXINFO"" WHERE ""TABLE_NAME"" = '" +
aForeignTables(i,0) + "' AND ""NON_UNIQUE"" = False AND ""INDEX_NAME"" =
(SELECT ""INDEX_NAME"" FROM ""INFORMATION_SCHEMA"".""SYSTEM_INDEXINFO"" WHERE
""TABLE_NAME"" = '" + aForeignTables(i,0) + "' AND ""COLUMN_NAME"" = '" +
aForeignTables(i,1) + "')"
' "NON_UNIQUE" = False ' gives the names of columns that are UNIQUE. However not all
column names are needed but only those which form an index with the foreign key field. This is
handled by the Subselect with the same table names (which contain the foreign key) and the
names of the foreign key fields.
If now the foreign key is present in the set, the key value can only be replaced if other fields are
used to define the corresponding index as UNIQUE. You must take care when carrying out
replacements that the uniqueness of the index combination is not compromised.
IF aForeignTables(i,1) = stFieldname THEN
inUnique = 1
ELSE
ReDim Preserve aColumns(inCount)
aColumns(inCount) = oQuery_result.getString(1)
inCount = inCount + 1
END IF
All column names, apart from the known column names for foreign key fields as Index with the
UNIQUE property, are stored in the array. As the column name of the foreign key field also belongs
to the group, it can be used to determine whether uniqueness is to be checked during data
modification.
IF inUnique = 1 THEN
stSql = "UPDATE """ + aForeignTables(i,0) + """ AS ""a"" SET """ +
aForeignTables(i,1) + """='" + inLB2 + "' WHERE """ + aForeignTables(i,1) +
"""='" + inLB1 + "' AND ( SELECT COUNT(*) FROM """ + aForeignTables(i,0) +
""" WHERE """ + aForeignTables(i,1) + """='" + inLB2 + "' )"
IF inCount > 0 THEN
stFieldgroup = Join(aColumns(), """|| ||""")
If there are several fields, apart from the foreign key field, which together form a 'UNIQUE' index,
they are combined here for a SQL grouping. Otherwise only "aColumns(0)" appears as
"stFieldgroup".
stFieldname = ""
FOR ink = LBound(aColumns()) TO UBound(aColumns())
stFieldname = stFieldname + " AND """ + aColumns(ink) + """ =
""a"".""" + aColumns(ink) + """ "
The SQL parts are combined for a correlated subquery.
NEXT
stSql = Left(stSql, Len(stSql) – 1)
Dialogs 31
The previous query ends with a bracket. Now further content is to be added to the subquery, so
this closure must be removed again. After that, the query is expanded with the additional
conditions.
stSql = stSql + stFeldbezeichnung + "GROUP BY (""" + stFeldgruppe + """) ) < 1"
END IF
If the foreign key has no connection with the primary key or with a UNIQUE index, it does not
matter if content is duplicated.
ELSE
stSql = "UPDATE """ + aForeignTables(i,0) + """ SET """ +
aForeignTables(i,1) + """='" + inLB2 + "' WHERE """ + aForeignTables(i,1) +
"""='" + inLB1 + "'"
END IF
oSQL_Statement.executeQuery(stSql)
NEXT
The update is carried out for as long as different connections to other tables occur; that is, as long
as the current table is the source of a foreign key in another table. This is the case twice over for
the Town table: in the Media table and in the Postcode table.
Afterwards the old value can be deleted from listbox 1, as it no longer has any connection to other
tables.
stSql = "DELETE FROM """ + aTable(5) + """ WHERE ""ID""='" + inLB1 + "'"
oSQL_Statement.executeQuery(stSql)
In some cases, the same method must now be carried out for a second table that has supplied
data for the listboxes. In our example, the first table is the Postcode table and the second is the
Town table.
If the text field is empty and listbox 2 also contains nothing, we check if the relevant checkbox
indicates that all surplus entries are to be deleted. This means the entries which are not bound to
other tables by a foreign key.
ELSEIF oCtlCheck1.State = 1 THEN
stCondition = ""
IF stAuxilTable = aTable(5) THEN
FOR i = LBound(aForeignTables()) TO UBound(aForeignTables())
stCondition = stCondition + """ID"" NOT IN (SELECT """ +
aForeignTables(i,1) + """ FROM """ + aForeignTables(i,0) + """) AND "
NEXT
ELSE
FOR i = LBound(aForeignTables2()) TO UBound(aForeignTables2())
stCondition = stCondition + """ID"" NOT IN (SELECT """ +
aForeignTables2(i,1) + """ FROM """ + aForeignTables2(i,0) + """) AND "
NEXT
END IF
The last AND must be removed, since otherwise the delete instruction would end with AND.
stCondition = Left(stCondition, Len(stCondition) - 4) '
stSql = "DELETE FROM """ + stAuxilTable + """ WHERE " + stCondition + ""
oSQL_Statement.executeQuery(stSql)
As the table has already been purged once, the table index can be checked and optionally
corrected downwards. See the subroutine described in one of the previous sections.
Table_index_down(stAuxilTable)
Afterwards, if necessary the listbox in the form from which the Table_purge dialog was called can
be updated. In some cases, the whole form needs to be reread. For this purpose, the current
record is determined at the beginning of the subroutine so that after the form has been refreshed,
the current record can be reinstated.
oDlg.endExecute() 'End dialog ...
32 Macros
oDlg.Dispose() '... and remove from storage
END SUB
Dialogs are terminated with the endExecute() command and completely removed from memory
with Dispose().
Dialogs 33