Run interface implementations parameterized in a lookup

In this blog post, I’m going to show you a pattern that allows users to parameterize an Interface and run it based on that parameterization.

This isn’t a tutorial or guide per se, but rather an experimental solution I developed while trying to implement a similar pattern to the one already used by Microsoft in the Spanish Immediate VAT Supply (SII), as well as in other countries like India, Brazil, Italy, etc. If you think something could be done better, please leave a comment.

Microsoft Implementation

This pattern could be used, for example, if different logic is needed for each country’s tax reports, or when confirming a sales order where each client requires different validations. The advantage of this approach is that a developer, who may not even be from our company, can extend this process for a new variation by simply implementing our interface without needing to change or even understand the base code. Then, the user selects which of those implementations they need.

Let’s start with a basic pattern: a table, its form, a menu item pointing to the form, and a button within the form that executes the ‘main’ method of a class, displaying an info message.

Our table:

The form:

The action menu:

And the class:

Now we can start creating the interface, which will have two methods. The methods in the interface must be empty. Any class implementing our interface must include at least these two methods. In C#, interfaces always start with an ‘I’, so in X++, I name them using the pattern ‘prefix_IName’. In this case, it’s ‘TST_IInterface’.

interface TST_IInterface
{  
    str name()
    {

    }
    
    str infoMessafe(str _str)
    {

    }
}

Let’s implement our interface by creating a new class. This is the part, and the only part, of our development that could be implemented by third parties. We cannot control their code, and they should not change ours.

class TST_ImplementationA implements TST_IInterface
{
    str name()
    {
        return "Implementation A";
    }

    str infoMessafe(str _str)
    {
        return strFmt("%1, this is the message of implementation A", _str);
    }

}

On our form, in the ‘ClassName’ field, we need a lookup that shows all implementations of our interface. This includes the ones we have now and any that might be added later.

We start by making a temp table. In this table, each record represents one implementation. The table has two fields: the first field is the class name, which is the key; the second field is the name, which comes from the ‘name()’ method in our interface.

Let’s fill this table when the form’s datasource is initialized. To do this, we’ll override the init method:

[Form]
public class TST_Table002 extends FormRun
{
    TST_InterfaceTmpTable tmpTable;
  
    [DataSource]
    class TST_Table002
    {
        /// <summary>
        ///
        /// </summary>
        public void init()
        {
            super();

            Dictionary dictionary = new Dictionary();
            ClassId interfaceClassId = classNum(TST_IInterface);

            //Loops all classes in the system
            for (int i = 1; i <= dictionary.classCnt(); i++)
            {
                //If this class we are looping implements TST_IInterface...
                SysDictClass dictClass = new SysDictClass(dictionary.classCnt2Id(i));
                if (EMActionClass::implementsClass(dictClass, interfaceClassId))
                {                    
                    ClassId      classId;
                    Object       obj;
                    SysDictClass sysDictClass;

                    // Create a new object from a class named according
                    //to the 'ClassName' string field in our record.
                    classId = className2Id(dictClass.name());
                    obj = classFactory.createClass(classId);
                    sysDictClass = new SysDictClass(classId);

                    //Let's fill the tmp table
                    tmpTable.clear();
                    tmpTable.ClassName              = dictClass.name();
                    // The first parameter specifies the method name.
                    // The second parameter is a new object created from the implementation class.
                    tmpTable.Name                   = sysDictClass.callObject("name", obj);                   
                    tmpTable.insert();
                }

            }
        }
    }
}

Now, let’s convert our ‘className’ field into a lookup. For more information about lookups, I recommend checking out this guide by Peter Ramer.

[DataField]
        class ClassName 
        {            
            public void lookup(FormControl _formControl, str _filterStr)
            {                
                Query query = new Query();
                QueryBuildDataSource queryBuildDataSource;
                QueryBuildRange queryBuildRange;
    
                SysTableLookup sysTableLookup = SysTableLookup::newParameters(tableNum(TST_InterfaceTmpTable), _formControl);
    
                sysTableLookup.addLookupField(fieldNum(TST_InterfaceTmpTable, ClassName), true);
                sysTableLookup.addLookupField(fieldNum(TST_InterfaceTmpTable, Name));                
    
                queryBuildDataSource = query.addDataSource(tableNum(TST_InterfaceTmpTable));

                sysTableLookup.parmQuery(query);
                sysTableLookup.parmTmpBuffer(tmpTable);
                sysTableLookup.performFormLookup();
            }

        }

If we compile now, we should be able to select all implementations of our interface.

Now, let’s run the infoMessage of that class when the user clicks the button.

internal final class TST_RunObj
{    
    public static void main(Args _args)
    {
        ClassId      classId;
        Object       obj;
        SysDictClass sysDictClass;
        TST_table002 tst_table002;

        //Get the selected record
        select firstonly * from tst_table002 where tst_table002.RecId == _args.record().RecId;

        // Create a new object from a class named according 
        //to the 'ClassName' string field in our record.
        classId = className2Id(tst_table002.ClassName);
        obj = classFactory.createClass(classId);
        sysDictClass = new SysDictClass(classId);

        // The first parameter specifies the method name.
        // The second parameter is a new object created from the implementation class.
        // The third parameter corresponds to the first parameter of the method.
        // If the method has additional parameters, they should be appended at the end.
        Info(sysDictClass.callObject("infoMessafe", obj, "Hi"));
    }
}

Finally, let’s check if it works! 🔍


Posted

in

,

by

Tags:

Comments

Leave a Reply

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