The form is based on the FormRun class. We can see this by opening (double-clicking) classDeclaration for the ConVMSParameters form:
public class ConVMSParameters extends FormRun
This differs from AX 2012, where the declaration was public class FormRun extends ObjectRun, which was probably a little more honest as the form is not a type; this is why it can have the same name as a table.
Once the system has built the form design using SysSetupFormRun, it will perform the initialization tasks and run the form. The key methods in this are Init and Run. These can be overridden on the form in order to perform additional initialization tasks.
One of FormRun's Init method's key tasks is to construct the form's data sources; these aren't table references but FormDataSource objects constructed from the tables listed under the Data Sources node.
In the case of the ConVMSParameters form, the system creates the following objects from the ConVMSParameters data source for us:
- ConVMSParameters as ConVMSParameters (table)
- ConVMSParameters_DS as FormDataSource
- ConVMSParameters_Q as Query
- ConVMSParameters_QR as QueryRun
We aren't normally concerned with the Query and QueryRun objects as we can access them through the FormDataSource object anyway.
The data sources are declared as global variables to the form and provide a layer of functionality between the form and the table. This allows us to control the interaction between the bound form controls and the table.
Let's override the init method on the data source, and then override the modifiedField event (which is triggered from a form's data source events); the code editor will present the change as follows:
[Form]
public class ConVMSParameters extends FormRun
{
public void init()
{
ConVMSParameters::Find();
super();
}
[DataSource]
class ConVMSParameters
{
public void init()
{
super();
}
[DataField]
class DefaultVehicleGroupId
{
public void modified()
{
super();
}
}
}
}
It adds the data source as a class within the form's class declaration, and then adds the field as a class within the data source class. This is the only place in SCM where this occurs. If the data source class were a class, it would have to extend FormDataSource. The Form, DataSource, and DataField attributes are a clue as to what's going on here. As all executable code compiles to .NET types, the compiler uses these attributes in order to create the actual types. The structure is written as such for our convenience in order to present the code.
Let's take the modifiedField method. This is an event that occurs after the validateField event returns true. The call to super() calls the table's modifiedField method. We may wonder why the call to super() has no parameter. This happens behind the scenes, and it is useful that this is handled for us.
This pattern is followed for the following methods:
DataSource method | Calls table method |
validateWrite | validateWrite |
write | write (in turn, insert or update) |
initValue | initValue |
validateDelete | validateDelete |
delete | delete |
DataField.validateField | validateField(FieldId) |
DataField.modifiedField | modifiedField(FieldId) |
The table's initValue, validateField, modifiedField, validateWrite, and validateDelete methods are only called from form events; the write method does not call validateWrite.
From this, we have a choice as to where we place our code, and this decision is very important. The rule to follow here is to make changes as high as possible: table, data source, and form control.
It is important that the code on a form is only written to control the user interface. It should not contain validation or business logic.
We can go further with this and write form interaction classes that allow user interface control logic that can be shared across forms; for instance, controlling which buttons are available to a list page and the associated detail form.