Method as a parameter (in D365FO)

We usually have methods where the logic is written in code and it just receives some data in parameters. But sometimes it’s benefitial to allow the caller code to define a piece of logic by itself.

For example, I can write code to filter a collection and let consumers decide what condition should be used for the filter. This kind of things is heavily used in LINQ in .NET.

Or I may have a piece of code to select records from database and perform some actions on them, and while the selection logic is the same, the actions that callers may want to perform are unknown to me. Therefore I could create a method that fetches the data and then takes the actual logic as a parameter.

Let me demonstrate it on a simple example. I’ll have a collection of data (a Set object) and I want an easy way to run an action on each of the elements.

Therefore the iteration of elements will be done by my code, but the actual action will be provided as a method parameter.

To make the method easily discoverable, I’ll add it to the Set class as an extension:

[ExtensionOf(classStr(Set))]
public final class My_Set_Extension
{
    public void forEach(System.Delegate _action)
    {
        SetEnumerator enumerator = this.getEnumerator();
        while (enumerator.moveNext())
        {
            System.Object[] parameters = new System.Object[1]();
            parameters.SetValue(enumerator.current(), 0);
            _action.DynamicInvoke(parameters);
        }
    }
}

I’m using a .NET class called System.Delegate as the parameter, because X++ as such doesn’t have any appropriate type. It’s the beauty of the interoperability of X++ with .NET – we can benefit from the whole .NET platform.

The actual iteration of the collection is straightforward. To execute the provided method, we use DynamicInvoke(), which expects an object array with method parameters. We have a single parameter (the element of the collection), therefore we create an array of size 1, put the element there and pass the array to DynamicInvoke().

Before we call our forEach() method, let’s create a method preparing some test data for us:

Set getData()
{
    Set set = new Set(Types::String);
    set.add('Primus');
    set.add('Secundus');
    set.add('Tertius');
    return set;
}

And this will be a method that we want to call for each element:

void reverseAndShow(str _s)
{
    info(strReverse(_s));
}

Because we’ll iterate a collection of strings, the parameter type is str.

Now we need to define a delegate, i.e. the type for our method. The declaration of a delegate in X++ looks like a method declaration, but it’s a very different thing under the hood. We’ll basically get a variable, a type of which is a class (generated under the hood, inheriting from System.Delegate) that can hold references to methods with a particular list of parameters and a return type.

Our method accepts a string and returns void, therefore we need a delegate with the same profile.

delegate void actionDelegate(str _s)
{
}

Now it’s time to make the call:

void run()
{
    this.actionDelegate += eventhandler(this.reverseAndShow);
 
    Set set = this.getData();
    set.forEach(this.actionDelegate);
}

We use the newly created delegate and add the reverseAndShow() method to it by += eventhandler(…).

Then we get the Set with data, call its forEach() method and pass the delegate to it. If you remember, we declared the parameter of forEach() as System.Delegate, which is the parent of what we get from this.actionDelegate.

If you run it, you’ll see that it indeed iterates the Set and calls reverseAndShow() for each element.

A nice feature of delegates is that they can hold references to several methods at once, and invoking the delegate executes all the methods. Let’s add one more method to our solution:

void toUpperAndShow(str _s)
{
    info(strUpr(_s));
}

Then we’ll simply add one more delegate subscription to run() method – no other change is needed.

void run()
{
    this.actionDelegate += eventhandler(this.reverseAndShow);
    this.actionDelegate += eventhandler(this.toUpperAndShow);
 
    Set set = this.getData();
    set.forEach(this.actionDelegate);
}

Now both methods are executed for every element of the collection.

For your convinience, here is a complete class that you can copy and run.

internal final class DelegateDemo
{
    public static void main(Args _args)
    {
        new DelegateDemo().run();
    }
 
    void run()
    {
        this.actionDelegate += eventhandler(this.reverseAndShow);
        this.actionDelegate += eventhandler(this.toUpperAndShow);
 
        Set set = this.getData();
        set.forEach(this.actionDelegate);
    }
 
    Set getData()
    {
        Set set = new Set(Types::String);
        set.add('Primus');
        set.add('Secundus');
        set.add('Tertius');
        return set;
    }
 
    delegate void actionDelegate(str _s)
    {
    }
 
    void reverseAndShow(str _s)
    {
        info(strReverse(_s));
    }
 
    void toUpperAndShow(str _s)
    {
        info(strUpr(_s));
    }
}

Note that DynamicInvoice() is slower than direct method calls. It’s typically not a problem, but you need to keep it in mind if you’re writing performance-critical code that makes a lot of such calls.

One Comment

Leave a Reply

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