AX 7. Breaking dependencies. Part 2.

In the first part we looked how to use SysPluginFactory, today we will talk about event delegates.

Delegates were introduced in AX 2012 but were rare used by standard code and partners.

In AX 7 delegates obtained second life.  They are widely used by MS to create customization points for customers and partners. As we can see number of them is constantly growing with each update release.

Another use of delegates is a situation when you want to access a code from another model without referencing it, for example accessing the code in a higher model from a lower model.

Create new delegate:

delegate void validateAmountDelegate(Amount _amount, EventHandlerResult _res)
{
}

Delegate does not have return value and method body.

Subscribe to:

[SubscribesTo(classStr(MyClass), delegatestr(MyClass, validateAmountDelegate))]
public static void validateAmount(Amount _amount, EventHandlerResult _res)
{
    if (amount > 0)
    {
        _res.result(true);
    }
    else
    {
        _res.result(false);
    }
}

Method signature should be the same for subscriber and delegate.

Invoke delegate:

EventHandlerResult result = new EventHandlerResult();

this.validateAmountDelegate(amount, result);

boolean res = result.result();

 

To read about usage scenarios and find out more details please refer to AX wiki
https://ax.help.dynamics.com/en/wiki/delegates-for-migration/

Advertisements

AX 7. How to add financial dimensions as separate columns to Data Entity.

Everyone who is working with financial dimensions in AX would like to see them in separate columns in Excel and today we will look how to achieve this for all Data Entities.

1

It’s also awesome because we have wizard that will do all the job for us!

In the Visual Studio under Dynamics AX > Addins select “Add financial dimensions for OData…”

2

Now we need to enter dimensions we want to expose. As you can see from a help text this dimensions should be setup in AX on Data Entities tab under General ledger > Chart of accounts > Dimensions > Financial dimension configuration for integrating applications.

3

Also we need to specify a model for new objects. Please note that the model should have reference to the Dimensions model, however you can add reference afterwards.

After clicking “Apply”, VS will show new project creation dialog. New project will be created. It will contain two Data Entity Extensions:

4

Please note that you cannot rename them because internal code works only with entity extensions that have name = Data Entity Name + “DimensionIntegration”.

Both entities has identical changes, the only difference is that one show default dimensions and another one ledger dimensions.

Let’s looks at DimensionSetEntity extension:

5

It has 3 new fields, one for each financial dimensions we specified in the wizard.

This fields are computed columns, each of them use same data method “getValueColumnForDimensionAttributeSql”.

6

It is quite interesting and has some smarties inside but hardcoded values as well.

That all we need to do. After compilation and synchronization we can open Data Entity in Excel and will see new fields in data connector designer.

7

How does it work? That the most exciting part. All the magic is inside DimensionAttributeValueSet table. If we look at it in AOT – nothing is really changed since AX 2012, but if we go to SQL management studio we will see bunch of new columns:

8

For each financial dimensions we have 2 columns: one contains value and another RecId.

Each computed column in Dimensions entity select value from this table.

So, if you are doing BI with 3rd party tools, now you can use  DimensionAttributeValueSet and DimensionAttributeValueCombination tables to query all financial dimensions directly.

AX 7. How to override form control methods using extensions.

In one of the previous posts we discussed how to override methods on form data source fields (AX 7. How to override form data source field methods without overlaying.). Today we will talk about form controls.

In AX 7 we have a bunch of events to subscribe on a form control level:

FormControlEvents

Let’s change a lookup, because it is a quite common customization.

/// <summary>
/// Adds a lookup to the <c>CustAccount</c> control on <c>SalesTable</c> form.
/// </summary>
/// <param name="_sender">The source of the event.</param>
/// <param name="_e">Arguments of the OnLookup event.</param>
[FormControlEventHandler(formControlStr(SalesTable, CustAccount), FormControlEventType::Lookup)]
public static void SalesTable_CustAccount_OnLookup(FormControl _sender, FormControlEventArgs _e)
{
    SysTableLookup sysTableLookup = SysTableLookup::newParameters(tablenum(CustTable), _sender);
    Query query = new Query();

    sysTableLookup.addLookupfield(fieldnum(CustTable, AccountNum), true);
    sysTableLookup.addLookupfield(fieldnum(CustTable, Party));
    sysTableLookup.addLookupMethod(tableMethodStr(CustTable, nameAlias));
    sysTableLookup.addLookupfield(fieldnum(CustTable, OurAccountNum));

    QueryBuildDataSource qbds = query.addDataSource(tablenum(CustTable));
    sysTableLookup.parmQuery(query);

    sysTableLookup.performFormLookup();

    FormControlCancelableSuperEventArgs ce = _e as FormControlCancelableSuperEventArgs;

    //cancel super() to prevent error.
    ce.CancelSuperCall();
}

Please note that execution of original lookup should be cancelled by FormControlCancelableSuperEventArgs.CancelSuperCall() otherwise AX will throw an error “More than one form was opened at once for the lookup control.”

AX 7. QueryRangeFunctionAttribute as a replacement for SysQueryRangeUtil.

All of you are familiar with SysQueryRangeUtil class. It is really helpful when you need to use custom query ranges with AOT queries or advanced form filters (see TechNet for examples). However it is a part of Application Platform model now and you won’t be able to add new methods there soon, because this model will be locked for overlaying.

New QueryRangeFunctionAttribute is introduced to replace it. Now we can mark any public static method with this attribute and it becomes available everywhere you used to use SysQueryRangeUtil class. Let’s have a look at code example and its usage:

/// <summary>
/// Retrieves a list of field IDs that are valid inventory dimensions parameters.
/// </summary>
/// <returns>String representing a list of field IDs.</returns>
[QueryRangeFunctionAttribute()]
public static str includedFieldIds()
{
    Set             validFieldIds   = InventInventoryDimensionsParametersEntityHelper::getValidInventoryDimensionsParametersFieldIds();
    SetEnumerator   enumerator      = validFieldIds.getEnumerator();
    str             value;

    while (enumerator.moveNext())
    {
        value = queryRangeConcat(value, enumerator.current());
    }

    return value;
}

Untitled

As you can see, syntax is slightly changed, previously you should specify only method’s name and now you need to add class name as well, because any class can have such method.

AX 7. Breaking dependencies. Part 1.

Model dependencies is a new term in AX dev world. In previous versions we did not have to care about it because AX had one big namespace and you were able to call any x++ code without referencing it.

In AX 7 all the code is divided into models. Each model compiles into its own assembly (see AX wiki http://ax.help.dynamics.com/en/wiki/understanding-the-model-split/ ).

Now if your class belongs to a model A and you want to use a code from a model B you have to add reference to it. AX 7 supports only one way references, so if the model A refers to the model B then the model B cannot refer to the model A. It is also known as circular dependencies.

This problem can be overcome by using interface and events.

Today we will talk about interface approach. To help us with this Microsoft created SysPluginFactory class.

Let’s look at the GeneralLedgerIExtension interface and its implementation as an example.

Untitled

As you can see GeneralLedgerIExtension interface and GeneralJournalEntry table belongs to GeneralLedger model and GeneralLedgerExtension class belong to Application Suite. At the same time Application Suite has a reference to GeneralLedger thus GeneralJournalEntry table cannot use GeneralLedgerExtension directly.

To handle this SysPluginFactory class comes into play. We will walk through the code below line by line. It consists of three main parts:

1. Interface marked with [ExportInterfaceAttribute()] attribute.

using System.ComponentModel.Composition;

[ExportInterfaceAttribute()]
public interface GeneralLedgerIExtension
{
}

2. Class that implements interface. It is marked with [ExportMetadataAttribute()] and [ExportAttribute()] attributes. Where:

  •  ExportAttribute accepts interface name.
  •  ExportMetadataAttribute accepts key and value. Key should be the same for all implementations and value should be different.
using System.ComponentModel.Composition;

[ExportMetadataAttribute(&quot;GeneralLedger&quot;, &quot;GeneralLedger&quot;),ExportAttribute(&quot;Dynamics.AX.Application.GeneralLedgerIExtension&quot;)]
public class GeneralLedgerExtension implements GeneralLedgerIExtension
{
}

3. Code that instantiate the class:

//interface type variable
GeneralLedgerIExtension generalLedgerExtension = null;

//instance of SysPluginMetadataCollection that holds key-value pair.
SysPluginMetadataCollection metadataCollection = new SysPluginMetadataCollection();
metadataCollection.SetManagedValue(&quot;GeneralLedger&quot;, &quot;GeneralLedger&quot;);

// instantiates the class using SysPluginFactory. It will find interface
// implementation marked with key-value specified in the metadataCollection.
generalLedgerExtension = SysPluginFactory::Instance(&quot;Dynamics.AX.Application&quot;, classStr(GeneralLedgerIExtension), metadataCollection);

 

We can notice that hardcoded string is used as a value in SetManagedValue() method to select interface implementation, however you can use enum or field value from a parameter table. Using parameter table may be useful in scenarios where a user will switch between implementation, for example choosing a price engine.
You can use this code as a starting point to create your own plugins.
Also SysPluginFactory can be used instead of construct pattern. Is has a couple of benefits:

  1.  Base class does not know about derived classes.
  2.  You don’t need to change code of base class to add new derived class.

AX 7. Process the data after Data Entity import.

It’s a common requirement to perform post actions on records that are imported by Data Entity, for example to invoice created sales order or to post created journal.

To do this we need to add a new method postTargetProcess() to our Data Entity. This method is automatically called by the DMFEntityWriter class in the end of processRecords() method when all records are transferred from staging to target. It is not available from override method drop down on Data Entity because it is static and is called via reflection.

/// <summary
/// Executes the logic once after processing the target data.
/// </summary>
/// <param name= “_dmfDefinitionGroupExecution">
/// The definition group that should be processed.
/// </param>
public static void postTargetProcess(DMFDefinitionGroupExecution _dmfDefinitionGroupExecution)
{
    //check if import job is finished
    if (_dmfDefinitionGroupExecution.StagingStatus == DMFBatchJobStatus::Finished)
    {
        MyStaging myStaging;

        //select all staging records that were processed by current execution job without errors.
        while select myStaging
            where myStaging.DefinitionGroup == _dmfDefinitionGroupExecution.DefinitionGroup
               && myStaging.ExecutionId     == _dmfDefinitionGroupExecution.ExecutionId
               && myStaging.TransferStatus  == DMFTransferStatus::Completed
        {
            //here you can find newly created records and update\post them.
        }
    }
}

Please note that it can be done only in data management scenarios but not via OData because OData updates\inserts records row by row and there is no post event\method to use.

AX 7. How to override form data source field methods without overlaying.

It is a common customization task to override standard form data source field methods like jumpRef(), modified() or  validate().

Recommended approach in AX 7 is to avoid overlaying and use extensions as much as possible. To achieve this new events were introduced. However on form data source field level we have only 3 available events:

SalesTable

We can try to use form controls instead of form data source fields but this approach has several cons:

  • User can add new control using form personalization and this control won’t support overridden logic. It could be critical if you are restricting field lookup values or adding validations.
  • One form could have several controls that refers to one data source field so you have to duplicate your code.
  • Number of delegates are limited as well. Microsoft will add more later 🙂

For example we want to add custom jumpRef() and validate() methods to itemId field on sales order form. To achieve this we have to either overlay it or use next approach.

First of all we will create class to handle method overrides.

/// <summary>
/// Handles events raised by <c>SalesTable</c> form.
/// </summary>
public class SalesTableEventHandler
{
    /// <summary>
    /// Post event handler for <c>SalesTable</c> <c>SalesLine</c> Initialized event.
    /// </summary>
    /// <param name=“_sender”></param>
    /// <param name=“_e”></param>
    [FormDataSourceEventHandler(formDataSourceStr(SalesTable, SalesLine), FormDataSourceEventType::Initialized)]
    public static void SalesLine_OnInitialized(FormDataSource _sender, FormDataSourceEventArgs _e)
    {
        var overrides = SalesTableFormExtensionOverrides::construct();

        _sender.object(fieldNum(SalesLine, ItemId)).registerOverrideMethod(methodStr(FormDataObject, jumpRef),
            methodStr(SalesTableFormExtensionOverrides, itemId_OnJumpRef), overrides);

        _sender.object(fieldNum(SalesLine, ItemId)).registerOverrideMethod(methodStr(FormDataObject, validate),
            methodStr(SalesTableFormExtensionOverrides, itemId_OnValidate), overrides);
    }
}

Then we will create new event handler class and subscribe to OnInitialized event of SalesLine data source.

/// <summary>
/// Contains methods which are used to override <c>SalesLine</c> data source field methods.
/// </summary>
public class SalesTableFormExtensionOverrides
{
    protected void new()
    {
    }

    /// <summary>
    /// Constructs a new instance of <c>SalesTableFormExtensionOverrides</c> class.
    /// </summary>
    /// <returns>
    /// A <c>SalesTableFormExtensionOverrides</c> class.
    /// </returns>
    public static SalesTableFormExtensionOverrides construct()
    {
        return new SalesTableFormExtensionOverrides();
    }

    /// <summary>
    /// Provides the open main table functionality for an item.
    /// </summary>
    /// <param name ="_targetField"> The <c>FormDataObject</c> where the jumpRef is triggered.</param>
    public void itemId_OnJumpRef(FormDataObject _targetField)
    {
        InventTable::jumpRefItemId(_targetField.getValue(), OpenMode::Edit);
    }

    /// <summary>
    /// Checks whether <c>ItemId</c> is valid.
    /// </summary>
    /// <param name = "_targetField"> The <c>FormDataObject</c> where the Validate is triggered.</param>
    public boolean itemId_OnValidate(FormDataObject _targetField)
    {
        //emulate super() call. Comment out to skip.
        boolean ret = _targetField.validate();

        if (ret)
        {
            //custom validation here.
        }

        return ret;
    }
}

That’s all. Using this approach you can override any data source field method.