Event handling is a new and welcome feature in AX from version 2012 onwards. One of its biggest gains is that it helps reduce the footprint on standard software (or that provided by an ISV or a partner). The footprint is one of the biggest concerns when minimizing the impact of future updates and upgrades.
The scenario we are describing is where we subscribe to an event (such as a table's delete method) so that it will call a method that handles the event. Although we add this subscription to a standard object in the AOT, the standard object is not modified. AX stores only the subscription definition. It is represented against the standard code, but this is only to help us intuitively create and manage event subscriptions.
The functionality we are going to provide is as follows:
When scheduling an appointment in transport management, the system has to associate the tractor and trailer with our fleet management system, if it exists. No lookup or validation is required because we schedule third-party vehicles, since many deliveries and collections are made by third-party hauliers or subcontractors. This should match the trailer ID or vehicle registration number.
The appointment table already has free text fields for tractors (vehicle) and trailers, so the solution to this is to trap when these fields are modified and relate them to our tables if a match is found.
We will do this by adding fields to the appointment table from our EDTs for the vehicle and trailer (and letting it create the relation), and setting them by hooking into the modifiedField
method in order to trap when the tractor and trailer fields are modified.
This is where events come in. We will add an event to the modifiedField
method to call an event handler than we will create.
The first task is to add our fields, which is something we have done several times by now. However, this is the first time on a standard table. We do this by following these steps:
TMSAppointment
table.ConFMSVehicleTableTruck
, and copy this to the Name property (this saves typing).ConFMSVehicleTableTrailer
table.You may notice that the type of field that AX created was Int64
. This is because the primary key on the tables is a surrogate key, which is based on RecId
.
AX makes performance gains using this type of relation. To make the apparently random number useful to the user, there are ways to hide the number behind more a useful representation of the related record. This is what happened when we added the HCMWorkerRecId
field to the service record.
In this case, we actually don't want to present this data. It is only for us to store a relation if the user enters a vehicle and trailer that we have inside AX.
Now we can write the event handler, which we need to write in a class. The code has to perform a few tasks, so we will do this in a class specific to the event. We will first write the logic to determine whether the vehicle or trailer exists.
The first thing to note is that ConFMSVehicleTableTruck
does not contain a registration number! Since registration is common to trucks and cars, we should add this to the base table, ConFMSVehicleTable
. Create a ConFMSVehRegNo
string EDT of suitable size. Add it to the table as a new field named VehRegNo
. By dragging this field into the Identification
and Overview
field groups, the UI will be adjusted for us.
The class will be specific to linking the appointment to the vehicle. Its entry point will be an event handler that will set the foreign keys created earlier. To create the class, follow these steps:
ConFMSAppointmentVehicleHandler
, and create a standard construct
method.TMSAppointment
(for example, appointment
) and RefFieldId
(for example, fieldId
). Create the parm
methods as you have done previously.public RefRecId getVehicleReferenceForType(str _tmsVehicleId, RefTableId _vehicleTypeTableId) { ConFMSVehicleTable vehicle; ConFMSVehRegNo searchStr = _tmsVehicleId; select firstOnly RecId from vehicle where vehicle.InstanceRelationType == _vehicleTypeTableId && vehicle.VehRegNo == searchStr; return vehicle.RecId; }
This was a simple method, but we have introduced some new concepts. The _tmpVehicleId
parameter is of the str
type. This base type is an unbound string with no specific size. This makes the method more versatile, but also means we can't use it as part of a where
clause of a select
statement. To use it in a where
clause, we should assign it to a variable based on the EDT of the field in question, ConFMSVehRegNo
in this case.
In the select
statement, we are selecting only the RecId
field and the first record found (firstOnly
). This increases performance by transferring less data between database and server tiers.
We are using the InstanceRelationType
field in our where clause, as the registration may not always be globally unique across all vehicle types.
Finally, whenever we write a select statement in AX, we must verify that there is an index covering the fields in the where
clause. Therefore, we need an index for VehRegNo
.
We can now create the class following the patterns used previously:
validate
method:public boolean validate() { if(fieldId == 0) { return checkFailed( strFmt( "Vehicle handler %1 was called incorrectly", classStr(ConFMSAppointmentVehicleHandler))); } return true; }
TractorNumber
and TrailerNumber
fields:private void handleTractorNumber() { RefRecId vehicleTableRef = this.getVehicleReferenceForType( appointment.TractorNumber, tableNum(ConFMSVehicleTableTruck)); // the reference will be 0 if not found. appointment.ConFMSVehicleTableTruck = vehicleTableRef; } private void handleTrailerNumber() { RefRecId vehicleTableRef = this.getVehicleReferenceForType( appointment.TrailerNumber, tableNum(ConFMSVehicleTableTrailer)); // the reference will be 0 if not found, // which is by design. appointment.ConFMSVehicleTableTrailer = vehicleTableRef; }
run
method, which reflects that of the modifiedField
pattern, as follows:public void run() { if(!this.validate()) return; switch(fieldId) { case fieldNum(TMSAppointment, TractorNumber): this.handleTractorNumber(); break; case fieldNum(TMSAppointment, TrailerNumber): this.handleTrailerNumber(); break; } }
The final part of the class is to create the event handler method, which will serve as its primary entry point, even though the way we wrote the method means we can reuse it elsewhere. This method has a special declaration, to which all event handlers must conform, and it is similar to those found in C#. This declaration is as follows:
public static void eventHandlerNameEventHandler(XppPrePostArgs _args)
Unlike C#, we aren't passed the calling object as a parameter. All of the interaction with the caller is done through the XppPrePostArgs
object.
The event can subscribe to a publisher (in our case, the modifiedField
method on the TMSAppointment
table) as a pre or post-event subscription. This means that it can be called before the method is executed or after it completes.
This object has many useful methods. The key methods we will use are listed here:
In order to handle this event, we need to know the current TMSAppointment
record and the field being modified. The current record is taken from getThis
. The field being modified is a parameter of modifiedField
, so we will use getArg
. The event handler method is created as follows:
ConFMSAppointmentVehicleHandler
class and select New | Pre- or post-event hander.tmsAppointmentModifFiedFieldEventHandler
. The EventHandler
suffix is by convention.TMSAppointment
table variable as appointment and assign _args.getThis()
, as shown here:TMSAppointment appointment = _args.getThis();
modifiedField
has one parameter of _fieldId
(we can simply look). We the use the _args.getArg(str _argName)
method:RefFieldId fieldId = _args.getArg('_fieldId'),
TMSAppointment
record in appointment and the field being changed in fieldId
.public static void tmsAppointmentModifiedFieldEventHandler(XppPrePostArgs _args) { TMSAppointment appointment = _args.getThis(); RefFieldId fieldId = _args.getArg('_fieldId'), ConFMSAppointmentVehicleHandler handler; handler = ConFMSAppointmentVehicleHandler::construct(); handler.parmAppointment(appointment); handler.parmFieldId(fieldId); handler.run(); }
This is a simple task. The only decision we have to make is whether it should run before or after the method has executed. This decision varies for each situation, but it should become clear if we imagine the code as running inline. The task of adding the subscription is as follows:
TMSAppointment
table, expand the Methods node.modifiedField
method and choose New Event Handler Subscription.ConFMSVehicleEventHandler
.ConFMSAppointmentVehicleHandler
in Class (copy and paste helps here)tmsAppointmentModifFiedFieldEventHandler
in this case. Select the property and press Enter. This sets AOTLink
to the full path to the method.The best way to test this code is through the debugger. If you want to try this, skip to Chapter 11, Unit and Performance Testing.
When an event occurs, AX builds an XppPrePostArgs
object and passes it to the subscriber, that is, our event handler. The event handler method gets the current appointment record using getThis()
, which is done by reference. It then constructs the class and passes the record to it, again by reference. The fact that the record is passed by reference by the XppPrePostArgs
object means we don't write the change to the database.
Before we finish with this example, let's view the Record Info for the appointment record (right-click on the form and choose Record Info and Show all fields). Not only do our two new fields appear, but AX also displays the reference primary key automatically, as shown in the following screenshot:
We will use this association later in Chapter 6, Extending the Solution.
In the previous section, we were using pre- and post-event subscriptions, which is fine when hooking into standard code. Another method, which is better, is the concept of delegate subscriptions. A delegate is represented in AX as a method with no code. Its sole purpose is to enable other parties to subscribe to it.
We can make a call to our delegate from any point in our code. When the delegate is called, any subscriptions are called as a result. A delegate subscription is preferable to pre- and post-event subscriptions for several reasons:
The same cannot be said for subscribing to a pre- or post-method handler of an arbitrary method. In our example, we will create a delegate that is called when an appointment's truck is changed, which was previously assigned to another vehicle. This is done by the following steps:
ConFMSAppointmentVehicleHandler
class and going to New | Delegate.vehicleChanged
. The naming is important to explain what the event is. See http://msdn.microsoft.com/en-us/library/gg879953.aspx.delegate void vehicleChanged(RefRecId _originalVehicleRef, RefRecId _newVehicleRef) { }
handleTractorNumber
method, add the following lines before the vehicle reference is assigned:private void handleTractorNumber() { RefRecId vehicleTableRef = this.getVehicleReferenceForType( appointment.TractorNumber, tableNum(ConFMSVehicleTableTruck)); // the reference will be 0 if not found. if(appointment.orig().ConFMSVehicleTableTruck != 0 && appointment.orig().ConFMSVehicleTableTruck != vehicleTableRef) { this.vehicleChanged( appointment.orig().ConFMSVehicleTableTruck, vehicleTableRef); } appointment.ConFMSVehicleTableTruck = vehicleTableRef; }
The orig
method allows us access to the appointment record when it was last read from the database. If the user changes the vehicle back to the original, the delegate won't be called. Finally, we create a test event handler. To do this, follow these steps:
ConFMSEventTest
. Then create an event handler method like this: right-click on the class, select New | Pre- or post-event handler, and rename it to vehicleChangedEventHandler
.public static void vehicleChangedEventHandler( RefRecId _origRef, RefRecId _newRef)
ConFMSVehicleTable
instances for origVehicle
and newVehicle
, and select the respective records:ConFMSVehicleTable origVehicle; ConFMSVehicleTable newVehicle; select origVehicle where origVehicle.RecId == origRef; select newVehicle where origVehicle.RecId == newRef;
infoLog
message to the user, using the following code:info(strFmt("Vehicle was changed from %1 (%2) to %3 (%4)", origVehicle.VehicleId, origVehicle.VehRegNo, newVehicle.VehicleId, newVehicle.VehRegNo));
You may notice that the method does not appear in the drop-down list, so you have to manually enter or paste it in Method name field. AX will set the AOTlink property, as shown in the following screenshot:
We should now test this by creating an appointment. Create an appointment where the vehicle will be associated, save the appointment (to reread orig()
), and then change then vehicle to another.
3.145.96.86