D365/X++ Chain of Command

Back in 2018 I wrote this article regarding the new capabilities in Chain of Command (CoC) to support Form extensibility which at that point was solely handled by Event Handlers. Fast forward six and a half years and as of December 2024, Chain of Command is the go-to method recommended by Microsoft to extend your application.

Like six years ago, Microsoft supports the implementation of CoC in Classes, Tables, Data Entities, and Forms. However, some things regarding how I work with extensibility on Forms have changed.

In this article I just want to focus on Forms and its set of nested classes, as there is an abundance on all other topics, but mostly I just want to share how I usually work when extending a standard form.


Considerations

Before we dive into some examples of CoC, remember that in order to use CoC you must declare your class as final, and your methods should always contain the next keyword.

The next keyword behaves like a super, and it will define when your extended logic executes. Code written before the next line behaves like a pre-event, your logic executes first, and later on, the logic residing in the standard method gets executed. Code written after the next line behaves like a post-event, your logic executes after the standard code has been executed.

Currently there is no support in the Visual Studio X++ Editor to discover the method candidates that can be wrapped, so you need to refer to the system documentation for each nested concept to identify the proper method to wrap and its exact signature.

Internal or private methods are not wrappable with the exception of private methods containing the decorator [wrappable(true)].


Decorators

These are the decorators we will use when extending a form object:


// Extends a form
[ExtensionOf(formStr(<NameOfFormBeingExtended>))] 

// Extends a form data source
[ExtensionOf(formDataSourceStr(<NameOfStdForm>, <NameOfDS>))] 

// Extends a form data field
[ExtensionOf(formDataFieldStr(<NameOfStdForm>, <NameOfDS>, <NameOfDataField>))] 

// Extends a form control part of the design
[ExtensionOf(formControlStr(<NameOfStdForm>, <NameOfUIControl>))] 

Code language: C# (cs)

Base Form Structure

[Form]
public class CATTestForm extends FormRun
{
    [DataSource]
    class CustGroup
    {
        
        public void initValue()
        {
            super();
             
            custGroup.PaymTermId = 'Net10';
            info(strFmt ("Pre CoC: $1 set to $2", 
                fieldId2PName(tableNum(CustGroup), fieldNum(CustGroup, PaymTermId)),
                custGroup. PaymTermId));
        }
    }
    
    [DataField]
    class CustGroup
    {
        public void modified()
        {
            super() ;

            CustGroup.Name = 'TEST';
            warning(strFmt("Pre CoC: $1 set to $2", 
                fieldId2PName(tableNum (CustGroup), fieldNum(CustGroup, Name)),
                custGroup.Name));
        }
    }
    
    [Control ("Button")]
    class ExecuteButton
    {
        public void clicked()
        {
            super();
        }       
    }
}
Code language: C# (cs)

Looking at the code above, we can be certain that forms in D365 are a set of nested classes. We can see a class for a data source, a class for a data source field, a class for a button control, etc…


Base Form Behaviour

Based on our base form code structure, this is what happens without leveraging CoC.

  • Creating a new record automatically invokes the initValue() method at data source CustGroup, setting the Terms of payment to Net10
  • Entering the Customer group will trigger the modified() method, which will set the Description to TEST
  • When the Execute button is clicked, no action is performed as the clicked() method is just calling super.

Implementation

To use CoC within the classes of the Form, you will need to create a class for every single object you want to extend. This can be a little cumbersome at the beginning, but once you get used to it, the process becomes easier. Likewise, the new development toolkit in Visual Studio 2022 allows you to extend these controls directly from the Application Explorer window.

Let’s take a look at the following three classes that will demonstrate how CoC works on data sources, data source fields, and controls:

  • Form extension class:
/// <summary>
/// Extension class for form <c>CATChainOfCommand</c>
/// </summary>
[ExtensionOf(formStr(CATChainOfCommand))]
final class CATChainOfCommandForm_CAT_Extension
{
    /// <summary>
    /// Executes logic to present an error after a button is pressed
    /// </summary>
    public void execute()
    {
        error("After CoC: Operation executed through CoC");
    }
}
Code language: C# (cs)
  • DataSource extension class:
/// <summary>
/// Extension class for form datasource <c>CATChainOfCommand, CustGroup</c>
/// </summary>
[ExtensionOf(formDataSourceStr(CATChainOfCommand, CustGroup))]
final class CATChainOfCommandForm_CustGroupDS_CAT_Extension
{
    /// <summary>
    /// Initializes the values of a newly created record
    /// </summary>
    public void initValue()
    {
        CustGroup custGroup = this.cursor();

        next initValue();

        custGroup.PaymTermId = 'Net30';

        info(strFmt("Post CoC: %1 set to %2", 
            fieldId2PName (tableNum(CustGroup) , 
            fieldNum(CustGroup, PaymTermId)), custGroup.PaymTermId));
    }
}
Code language: C# (cs)
  • DataSource field extension class:
/// <summary>
/// Extension class for form datasource field <c>CATChainOfCommand, CustGroup, CustGroup</c>
/// </summary>
[ExtensionOf(formDataFieldStr(CATChainOfCommand, CustGroup, CustGroup))]
final class CATChainOfCommandForm_CustGroupDS_CustGroup_CAT_Extension
{
    /// <summary>
    /// Performs operations when the data field is modified
    /// </summary>
    public void modified()
    {
        CustGroup       custGroup;
        FormDataSource  ds;
        FormDataObject  df = any2Object(this) as FormDataObject;

        next modified () ;

        ds = df.datasource();
        custGroup = ds.cursor ();
        custGroup.Name = strFmt("%1 with added description after CoC", CustGroup.Name);
    }
}

Code language: C# (cs)
  • Button control extension class:
/// <summary>
/// Extension class for form control <c>CATChainOfCommand, ExecuteButton</c>
/// </summary>
[ExtensionOf(formControlStr(CATChainOfCommand, ExecuteButton))]
final class CATChainOfCommandForm_ExecuteButtonControl_CAT_Extension
{
    /// <summary>
    /// Performs operations when a button is clicked
    /// </summary>
    public void clicked()
    {
        const str custGroup10 = '10';
        const str custGroup20 = '20';

        CustGroup           custGroup;
        FormButtonControl   bc = any2Object(this) as FormButtonControl;
        FormDataSource      ds = bc.formRun().dataSource(tableStr(CustGroup)) ;

        next clicked();

        custGroup = ds.cursor();

        if (custGroup.CustGroup == custGroup10 || custGroup.CustGroup == custGroup20)
        {
            element.execute() ;
        }
    }
}

Code language: C# (cs)

Results

Once we test the form following the same steps as before, this is what we get as a result after Chain of Command gets implemented:

  • Create a new record invokes the initValue() method at data source CustGroup, setting the Terms of Payment to Net10. However, we are wrapping this method using CoC, which will behave as a post-event. Therefore, the extended method will set Net30 as the new Term of payment.
  • Entering the Customer group will trigger the modified() method, which will set the Description to TEST. However, the data source field method is wrapped using CoC with a post-event. This will result in a modification of the Description, which will be the assignment before the next() call, plus the text added by the strFmt() function, resulting in a new value equal to TEST with added description after CoC.
  • When the Execute button is clicked a post-event is triggered, if the customer group is equal to 10 or 20, then the logic will execute. In the case of this button, it will invoke a method called execute() that is located in the form extension class. This method will print a message saying “After CoC: Operation executed through CoC”.

Things I’ve Learned These Years

After working so many years with Chain of Command I started to switch things a bit, initially in every extension I would try to put my logic within the extended class, but when you have a form with multiple data sources and you want to perform conditional logic based on multiple tables, it becomes complicated due to the number of objects you might need to declare to access those objects within a data source field extension or a control extension.

Take as an example the ExecuteButton extension class on our sample, I did it this way (almost similar to my original 2018 article) to explain my point. Today I will do this differently. First, I will extract all the code inside the clicked() method extension and leave the line element.execute().

/// <summary>
/// Extension class for form control <c>CATChainOfCommand, ExecuteButton</c>
/// </summary>
[ExtensionOf(formControlStr(CATChainOfCommand, ExecuteButton))]
final class CATChainOfCommandForm_ExecuteButtonControl_CAT_Extension
{
    /// <summary>
    /// Performs operations when a button is clicked
    /// </summary>
    public void clicked()
    {
        next clicked();

        element.execute() ;
    }
}

Code language: C# (cs)

All the code from the clicked() method is now part of the execute() method of my form extension class. This is easier as I have direct access to all my data sources without the need to poke around elements until I can get them all. In this case, because my element is the FormRun itself, I have direct access to my data source table record CustGroup which makes things easier when trying to evaluate if a customer group is equal to one of my predefined groups (10 or 20). Finally, once everything is cleaned up this is how it looks.

/// <summary>
/// Extension class for form <c>CATChainOfCommand</c>
/// </summary>
[ExtensionOf(formStr(CATChainOfCommand))]
final class CATChainOfCommandForm_CAT_Extension
{
    public const str custGroup10 = '10';
    public const str custGroup20 = '20';
    
    /// <summary>
    /// Executes logic to present an error after a button is pressed
    /// </summary>
    public void execute()
    {
        if (custGroup.CustGroup == custGroup10 || custGroup.CustGroup == custGroup20)
        {
            error("After CoC: Operation executed through CoC");
        }        
    }
}
Code language: C# (cs)

Thanks for checking up and happy coding 🙂

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *