If GUI actions are similar to procedures, GUI triggers are similar to event handlers. EDWConsole will call all registered GUI triggers every time a given event (like the user changing a field's value, or trying to confirm the changes to a record) happens. GUI triggers work in the context of a TEDWGUIForm. A trigger is an instance of TEDWGUITrigger, a strong entity in the GUICatalog.
GUI triggers are used in data-entry forms to:
Apply default values (both constant/fixed values and dynamic/expression-based values).
Generate record IDs.
Enforce business rules of various kinds. Examples of business rules:
Whenever field X = A, then field Y becomes required, otherwise field Y is not required.
Whenever field X changes, field Y changes as well according to a function of X (e.g. a table lookup). Optionally, this happens only if field Y is empty at the time field X changes.
Field DateTo, if specified, should be higher than or equal to field DateFrom. This check is performed when the record is posted.
SomeNumber should be negative or between 100 and 1000. This check is performed whenever SomeNumber is modified, and again when the record is posted.
Similarly to what happens with GUI actions, trigger types are descendants of TEDWGUITriggerImplementation. There can be many types of GUI triggers. Each of them does a specific thing, but one particularly useful and general trigger implementation is the script-based trigger, which executes a user-provided script. The script is granted access to an environment that allows it to make changes to the appearance and behaviour of the form that fired the trigger.
A GUI trigger implementation object is an instance of a predefined or user-defined trigger implementation class, registered in a global registry and retrieved by its Id at run time. Both triggers and trigger implementation objects have parameters to customize their behaviour. Parameters may be set at definition time (when the triggers are stored in the GUICatalog) or at link time (dynamica parameters).
Trigger objects are stored as first class objects in the GUICatalog. Each trigger refers to a trigger implementation class through an ImplementationId attribute. A trigger, when executed, manufactures an implementation object through a factory, based on the Id, and executes it, passing in any needed parameters. Trigger implementation classes are registered in a global registry. There are a number of pre-defined trigger implementation classes in the EDW GUI framework, and more can be added and registered by the developer.
Triggers are linked to forms through an ordered list of trigger link objects (instances of the embedded class TEDWGUITriggerLink) that is part of TEDWGUIForm. This ordered list is an instance of TEDWGUITriggerLinkCollection. A trigger link object holds the following information:
A reference to a trigger object.
A set of named and typed trigger parameters. Parameters give the trigger object a context; it is hard to imagine parameterless triggers, yet parameters are optional because they might be defaulted. Parameters might also be set in triggers themselves, in case they are widely used.
A set of trigger firing event names (AfterInsert, AfterEdit, BeforePost, BeforeFieldChange, AfterFieldChange, etc.). Each trigger firing event name also has a string parameter, which is used by field-related events to store the field names (with * meaning all fields) and may be used for other purposes as well.
EDWConsole executes the relevant triggers, in the specified order, every time a particular event happens. The following table features a list of currently defined events:
Table 5.1. Available trigger firing events
| Event | Scope | Notes |
|---|---|---|
| BeforeFieldChange | Field | Fired when a user is attempting to modify a field's value. You can use it to prevent the modification by raising an exception. When the event is fired, the Field dynamic parameter already holds the new value. Unfortunately the old value is not currently available. Please note that this event is also fired when modifying fields non-interactively, for example by another trigger. |
| AfterFieldChange | Field | Fired after a field's value has been successfully modified. Use it to fire side effects, like lookups. When the event is fired, the Field dynamic parameter holds the new value. Unfortunately the old value is not currently available. Please note that this event is also fired when modifying fields non-interactively, for example by another trigger. |
| BeforeEdit | DataSet | Fired right after opening an edit form. The DataSet is not in edit mode yet, so you cannot make changes to it in triggers attached to this event. Use it to prevent editing certain records by raising exceptions. |
| AfterEdit | DataSet/Field | Fired right after opening an edit form. The DataSet is in edit mode, so you can make changes to it in triggers attached to this event. Typically used to set field attributes like Required or ReadOnly. This event is fired first for the dataset, and then for each field. You can hook the most convenient depending on your needs. |
| BeforeInsert | DataSet | Fired right after opening an edit form to create a new record. The DataSet is not in edit mode yet, so you cannot make changes to it in triggers attached to this event. You can use it to prevent creating records, by raising exceptions, under particular circumstances. |
| AfterInsert | DataSet/Field | Fired right after opening an edit form to create a new record. The DataSet is in insert mode, so you can make changes to it in triggers attached to this event. Typically used to set defaults and record IDs. This event is fired first for the dataset, and then for each field. You can hook the most convenient depending on your needs. |
| BeforeDelete | DataSet | Fired when a request to delete a record (for example when the user clicks the relevant tool bar button) is received. Use this event to prevent deleting records, by raising an exception, depending on particular circumstances. |
| AfterDelete | DataSet | Fired after a record has been deleted in a GUI form. Use it to fire side effects. Unfortunately, the data of the deleted record is not currently available to triggers attached to this event. |
| BeforePost | DataSet | Fired just before posting an edited or newly created record. Last chance to prevent the post by raising an exception. The DataSet contains the data of the record about to be posted. |
| AfterPost | DataSet | Fired after posting an edited or newly created record. You can hook this event to trigger side effects. The DataSet contains the data of the record about to be posted. |
Each trigger returns a Boolean value. If a trigger returns False then the execution sequence is interrupted without errors. This allows to write conditional trigger sequences. A trigger may also raise an exception to interrupt the sequence with an error.
The following sections describe the currently available trigger implementations, which are ready to use in your models: just set the ImplementationId and any required parameters. Remember that all triggers have a few dynamic parameters which are set by the system before calling them. These parameters are:
Table 5.2. Common GUI trigger parameters
| Parameter name | Data type | Contents |
|---|---|---|
| DataSet | object (TDataSet) | A reference to the dataset firing the trigger. It is set for both dataset-level and field-level GUI triggers. You can use it to set dataset-level properties (with caution), or to access other fields. |
| Field | object (TField) | A reference to the field for which the field-level trigger is fired. It is nil for dataset-level GUItriggers. Typically, you'll use this reference to get at the field attributes, like ReadOnly or Required (similarly to doing it with events in Delphi code). |
| DBConnection | object (IEDWDBConnection) | A reference to the database connection (which is usually, but not necessarily, the environment's connection). |
This trigger, which is often linked to the BeforePost or AfterInsert events, fetches a new Id from a sequence generator and assigns it to the target field. It uses the GenId GUI trigger implementation Id. This only works with databases that support sequence generators, such as Oracle, InterBase or Firebird.
Technically, this trigger works only with database connection that implement the IEDWDBConnection.FetchSequenceGeneratorValue in a meaningful way.
Parameters:
DestinationFieldName: string (Required): Indicates the name of the field that will host the generated value.
SequenceName: string (Required): Name of the sequence generator to use.
OnlyIfEmpty: Boolean (Optional, default False): True means that the value is written to the destination field only if it is empty (null or 0). False means that the value is written always, potentially overwriting an existing value.
Here is a model snippet that defines a trigger with a pre-loaded sequence name (G_GLOBAL) and destination field name (ID). Assuming many tables in the model have similar structure, then we'll define a parameterless trigger link for each table, all pointing to this trigger. The EDW demo does exactly this.
<TEDWGUITrigger>
<Id>EDWDemoGenIdTrigger</Id>
<ImplementationId>GenId</ImplementationId>
<Params>
<TEDWGUIParams>
<Params>
<TEDWGUIParam>
<Name>DestinationFieldName</Name>
<Value>ID</Value>
<DataTypeAsString>String</DataTypeAsString>
</TEDWGUIParam>
<TEDWGUIParam>
<Name>SequenceName</Name>
<Value>G_GLOBAL</Value>
<DataTypeAsString>String</DataTypeAsString>
</TEDWGUIParam>
<TEDWGUIParam>
<Name>OnlyIfEmpty</Name>
<Value>True</Value>
<DataTypeAsString>String</DataTypeAsString>
</TEDWGUIParam>
</Params>
</TEDWGUIParams>
</Params>
</TEDWGUITrigger>Here is the snippet that links the above GUI trigger to a particular GUI form:
<TEDWGUIFormElement>
...
<MasterForm>
<TEDWGUIForm>
...
<TriggerLinkCollection>
<TEDWGUITriggerLinkCollection>
<Items>
<TEDWGUITriggerLink>
<Trigger>
<TEDWGUITrigger>
<Id>EDWDemoGenIdTrigger</Id>
</TEDWGUITrigger>
</Trigger>
<TriggerFiringEvents>
<TEDWGUITriggerFiringEvent>
<EventName>BeforePost</EventName>
</TEDWGUITriggerFiringEvent>
</TriggerFiringEvents>
</TEDWGUITriggerLink>
</Items>
</TEDWGUITriggerLinkCollection>
</TriggerLinkCollection>
</TEDWGUIForm>
</MasterForm>
</TEDWGUIFormElement>The combination of the settings above will generate a new Id every time a new record is created in that particular GUI form.
This one, which uses the Lookup GUI trigger implementation Id, allows you to set the value(s) of one or more target fields according to a function of the value of one or more source field(s). The function in this particular case is a table lookup that uses the source field values as a key and returns the result values which are used to set the target fields' values. The lookup query uses the DBConnection dynamic parameter to access the database.
It is actually easier said than done, as you'll surely know what a table lookup works already. Thus, let's get to the parameters:
LookupResultFieldNames: string (Required): Comma-separated list of fields to read from the lookup table.
ResultFieldNames: string (Required): Comma-separated list of fields to write in the current dataset (the DataSet dynamic parameter). The list should have the same number of elements as LookupResultFieldNames, as fields are matched between the two lists by position.
LookupKeyFieldNames: string (Required): Comma-separated list of key fields in the lookup table.
KeyFieldNames: string (Required): Comma-separated list of key fields in the current dataset (the DataSet dynamic parameter).
LookupTableName: string (Required): Lookup table name. May hold a "derived table" expression (a select statement) in case the lookup table needs to be filtered or joined.
OnlyIfEmpty: Boolean (Optional, default False): True means that each result value is written to the relevant field only if it is empty. False means the values are written always, potentially overwriting existing values.
These parameters define a specific lookup. If you have the same lookup in more places, then you'll want to define a trigger with this implementation Id and pre-loaded parameters, and then use parameterless trigger links in each place in which you need the trigger. Otherwise, for one-time triggers, you'll want to set all parameters in the trigger link. Here is an example of the latter case:
<TEDWGUITriggerLink>
<Trigger>
<TEDWGUITrigger>
<Id>StandardLookupGUITrigger</Id>
</TEDWGUITrigger>
</Trigger>
<TriggerParams>
<TEDWGUIParams>
<Params>
<TEDWGUIParam>
<Name>LookupResultFieldNames</Name>
<Value>ID_VOCE_DEFAULT</Value>
<DataTypeAsString>String</DataTypeAsString>
</TEDWGUIParam>
<TEDWGUIParam>
<Name>ResultFieldNames</Name>
<Value>ID_VOCE</Value>
<DataTypeAsString>String</DataTypeAsString>
</TEDWGUIParam>
<TEDWGUIParam>
<Name>LookupKeyFieldNames</Name>
<Value>ID</Value>
<DataTypeAsString>String</DataTypeAsString>
</TEDWGUIParam>
<TEDWGUIParam>
<Name>KeyFieldNames</Name>
<Value>ID_PDC</Value>
<DataTypeAsString>String</DataTypeAsString>
</TEDWGUIParam>
<TEDWGUIParam>
<Name>LookupTableName</Name>
<Value>PDC</Value>
<DataTypeAsString>String</DataTypeAsString>
</TEDWGUIParam>
<TEDWGUIParam>
<Name>OnlyIfEmpty</Name>
<Value>True</Value>
<DataTypeAsString>Boolean</DataTypeAsString>
</TEDWGUIParam>
</Params>
</TEDWGUIParams>
</TriggerParams>
<TriggerFiringEvents>
<TEDWGUITriggerFiringEvent>
<EventName>AfterFieldChange</EventName>
<EventParams>ID_PDC</EventParams>
</TEDWGUITriggerFiringEvent>
</TriggerFiringEvents>
</TEDWGUITriggerLink>The settings above will cause the execution of a SQL query similar to "select ID_VOCE from PDC where ID = :ID_PDC" whenever the ID_PDC field is changed. The result value is then written to the ID_VOCE_DEFAULT field.
This trigger is similar in its working to the lookup trigger. Its implementation Id is SingletonSelect, and it executes a given SQL select statements, then copies all fields of the first record (which should be the only record - that's the meaning of singleton) of the result set to the equally named fields in the DataSet. You can use this trigger to implement custom lookups or custom Id-generation strategies. Parameters:
CommandText: string (Required): The singleton select statement; use column aliases to match field names to those of DataSet. The statement is executed in the context of DBConnection.
OnlyIfEmpty: Boolean (Optional, default False): True means that each result value is written to the relevant field only if it is empty. False means the values are written always, potentially overwriting existing values.
Here is an example that shows how to use this kind of trigger to implement a custom Id-generation strategy which is usable with small tables and low concurrency: each new record created in table EXAMPLE gets as ID the value of max(ID) +1:
<TEDWGUITriggerLink>
<Trigger>
<TEDWGUITrigger>
<Id>StandardSingletonSelectGUITrigger</Id>
</TEDWGUITrigger>
</Trigger>
<TriggerParams>
<TEDWGUIParams>
<Params>
<TEDWGUIParam>
<Name>CommandText</Name>
<Value>select max(ID) + 1 ID from EXAMPLE</Value>
<DataTypeAsString>String</DataTypeAsString>
</TEDWGUIParam>
<TEDWGUIParam>
<Name>OnlyIfEmpty</Name>
<Value>True</Value>
<DataTypeAsString>Boolean</DataTypeAsString>
</TEDWGUIParam>
</Params>
</TEDWGUIParams>
</TriggerParams>
<TriggerFiringEvents>
<TEDWGUITriggerFiringEvent>
<EventName>AfterInsert</EventName>
</TEDWGUITriggerFiringEvent>
</TriggerFiringEvents>
</TEDWGUITriggerLink>Similar triggers may be created to implement other strategies, like using a separate counter table, or the high-low technique.
This trigger (that uses the Constant implementation Id) is very simple: if will just assign whatever value is provided in its Value parameter to the target field. You can hook it to the AfterInsert event to set constant defaults, or to the BeforePost event to fill in values that the used left blank. You are quired to use it as a field-level trigger. Parameters:
Value: <any data type> (Required): The constant value to which the target field is set. The data type should be compatible with the field's data type.
OnlyIfEmpty: Boolean (Optional, default False): True means that the constant value is written to the field only if it is empty. False means the value is written always, potentially overwriting an existing value.
The following is an example that sets a default value of False for a Boolean field named IS_ACTIVE:
<TEDWGUITriggerLink>
<Trigger>
<TEDWGUITrigger>
<Id>StandardConstantGUITrigger</Id>
</TEDWGUITrigger>
</Trigger>
<TriggerParams>
<TEDWGUIParams>
<Params>
<TEDWGUIParam>
<Name>Value</Name>
<Value>False</Value>
<DataTypeAsString>Boolean</DataTypeAsString>
</TEDWGUIParam>
</Params>
</TEDWGUIParams>
</TriggerParams>
<TriggerFiringEvents>
<TEDWGUITriggerFiringEvent>
<EventName>AfterInsert</EventName>
<EventParams>IS_ACTIVE</EventParams>
</TEDWGUITriggerFiringEvent>
</TriggerFiringEvents>
</TEDWGUITriggerLink>As you can see, the above is a rather verbose way of defining a default value for a field, but it is just an example. A more conventional and compact way to do that would be to just set the DefaultValue attribute of the IS_ACTIVE GUI field.
This trigger (implementation Id: PascalScript) allows you to apply custom logic. Basically, you provide a piece of Pascal script code that will be executed when the trigger is fired. Parameters:
Source: string (Required): Trigger source, with or without the function prototype.
Simple scripts do not need to specify the function prototype in the source, which will be added automatically if missing. For example, this source:
FIELD_A.AsString := UpperCase(FIELD_B.AsString);
Will be turned to:
function TriggerMain: Boolean; begin FIELD_A.AsString := UpperCase(FIELD_B.AsString); Result := True; end;
before execution.
Complex, multi-function scripts, on the other hand, should enclose the main code in a function called TriggerMain with a Boolean result, and set the Result to True to allow the sequence of triggers to continue, or False to stop it.
As you can see from the example above, a script trigger is able to access all fields in the dataset that fired the trigger as pseudo-variables named after the field names. In addition, script triggers have access to an Environment object that features the DataSet and Field properties coming from the trigger's dynamic parameters, plus Log and RaiseError methods akin to those of EDW's global Environment object. The interface of the script trigger's environment object, of type TEDWGUITriggerEnvironment, will be extended in the future. Please see the EDW reference for more details.
Some other example of script triggers. Here they go.
// A script-based replacement for the Constant GUI trigger.
// Use it in AfterInsert.
SomeField.AsString := 'SomeValue';
// Timestamping. Use it in AfterInsert.
CreatedOn.AsDateTime := Now();
LastChangedOn.AsDateTime := Now();
// Timestamping. Use it in AfterInsert and BeforePost.
LastChangedOn.AsDateTime := Now();
// Conditionally required fields. Use it in AfterInsert,
// AfterEdit and AfterFieldChange(X).
Y.Required := X.AsInteger = 10;
// Calculated stored fields (or side effects).
// Use it in AfterFieldChange(USDAmount).
{$include ConvUtils_inc.pas }
function TriggerMain: Boolean;
begin
EuroAmount.AsCurrency := USDToEuro(USDAmount.AsCurrency);
Result := True;
end;
// Enforce dataset-level constraints. Use it in BeforePost.
if not DateTo.IsNull and (DateTo.AsDate < DateFrom.AsDate) then
Environment.RaiseError(DateTo.DisplayLabel
+ ' is inconsistent with ' + DateFrom.DisplayLabel);