Hooking into standard AX logic using event handling

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.

Associating the appointment with a vehicle

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.

Tip

To quickly work out the AOT form (and therefore the table), open the form in the client, right-click on the form, and choose Personalize. On the Information tab, you can click on Edit next to the Form name field.

This is where events come in. We will add an event to the modifiedField method to call an event handler than we will create.

Adding custom fields to the appointment table

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:

  1. Place an AOT window next to your project window.
  2. Locate the TMSAppointment table.
  3. Expand the Relations node.
  4. Right-click on Relations and choose New relation.
  5. We open the property sheet for our new relation, set the Table property to ConFMSVehicleTableTruck, and copy this to the Name property (this saves typing).
  6. Set the cardinality properties as usual for a foreign key.
  7. Right-click on the new relation and select New | ForeignKey | PrimaryKeyBased.

    Note

    More magic happens, as it has created a field to hold the foreign key.

  8. Do the same for the ConFMSVehicleTableTrailer table.
  9. We drag the table onto the Tables node of our project. All objects we modify must be in a project.

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.

Creating a class to handle the modifiedField event

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.

Note

You may decide that the trailer ID field is redundant, given that the vehicle type gives this identifier context. We won't use the trailer ID later in the book.

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:

  1. Create a class as usual, name it ConFMSAppointmentVehicleHandler, and create a standard construct method.
  2. Add class variables for the following types: TMSAppointment (for example, appointment) and RefFieldId (for example, fieldId). Create the parm methods as you have done previously.
  3. Create a method that will get the record ID of the tractor or trailer, as follows:
    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.

Tip

This is a simple solution, and in real life, we will use a more sophisticated search mechanism, since the user isn't selecting a record from a drop-down list.

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:

  1. Create a validate method:
    public boolean validate()
    {
        if(fieldId == 0)
        {
            return checkFailed(
                strFmt(
                  "Vehicle handler %1 was called incorrectly", 
                  classStr(ConFMSAppointmentVehicleHandler)));
        }
        return true;
    }

    Note

    The classStr method simply returns the name of the class as a string. But more importantly, it will cause a compilation error if the class not exist, for example, after refactoring.

  2. Create methods to handle the 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;
    }
  3. Create the 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;
        }
    }

Creating the event handler method

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:

Method

Description

getThis

Returns an instance of the calling object, or publisher. This could be a class instance or a record in a table.

getArg

Allows us to get the value of a named property.

getArgNum

Allows us to get a value of a property by the number from the left.

getReturnValue

Gets the return value of the method (this is effective only when it is a post-event subscription).

setArg

Modifies a named parameter of the publisher (this is effective only when this is a pre-event subscription).

setArgNum

This method is similar to setArg, but referenced by parameter number from the left.

setReturnValue

Modifies the return value of the caller. This is effective for post-event subscription.

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:

  1. Right-click on the ConFMSAppointmentVehicleHandler class and select New | Pre- or post-event hander.
  2. Rename the method to tmsAppointmentModifFiedFieldEventHandler. The EventHandler suffix is by convention.
  3. Declare a TMSAppointment table variable as appointment and assign _args.getThis(), as shown here:
    TMSAppointment appointment = _args.getThis();

    Note

    You may notice that getThis() returns AnyType. This can be cast as any X++ type.

  4. Next, we need to know which field we are handling, since we know that modifiedField has one parameter of _fieldId (we can simply look). We the use the _args.getArg(str _argName) method:
    RefFieldId     fieldId     = _args.getArg('_fieldId'),
  5. We now have the TMSAppointment record in appointment and the field being changed in fieldId.
  6. Now we can declare, construct, and run our handler. The complete method is as follows:
    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();
    }

Adding a subscription to TMSAppointmentTable.modifiedField

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:

  1. On the TMSAppointment table, expand the Methods node.
  2. Right-click on the modifiedField method and choose New Event Handler Subscription.
  3. Open the properties of the new subscription, EventHandler1.
  4. Change Name to ConFMSVehicleEventHandler.
  5. Change CalledWhen to Post.
  6. Enter ConFMSAppointmentVehicleHandler in Class (copy and paste helps here)
  7. Method will default to the first event handler method on the class, tmsAppointmentModifFiedFieldEventHandler in this case. Select the property and press Enter. This sets AOTLink to the full path to the method.

What happens when the code executes

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:

What happens when the code executes

We will use this association later in Chapter 6, Extending the Solution.

Creating a delegate for use by other parties

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:

  • It is easier for a third-party to know what to subscribe to. A delegate method is a clearly defined event.
  • Subscribers to the delegate have the same method signature of the delegate. They are passed the same parameters that were passed to the delegate.
  • The event is called from a specific point in our code.
  • The event is called based on a condition.
  • The clearly defined delegate has a fixed purpose. If we change our code later, any code that relies on it will not be broken as long as the delegate still serves the same purpose.

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:

  1. Create the delegate by right-clicking on the ConFMSAppointmentVehicleHandler class and going to New | Delegate.
  2. Rename the delegate to vehicleChanged. The naming is important to explain what the event is. See http://msdn.microsoft.com/en-us/library/gg879953.aspx.
  3. Add two reference record ID parameters for the original and new vehicle references, as follows:
    delegate void vehicleChanged(RefRecId _originalVehicleRef, 
                                 RefRecId _newVehicleRef)
    {
    }
  4. In the 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:

  1. Create a new class called 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.
  2. We must make the signature of our handler match the delegate, as shown in the following code:
    public static void vehicleChangedEventHandler(
                                        RefRecId _origRef, 
                                        RefRecId _newRef)
  3. Declare two 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;
  4. Then show an 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));
  5. Add an event subscription to the delegate created earlier.

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:

Creating a delegate for use by other parties

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.96.86