D365O. Trick to pass a value between Pre and Post event handler using XppPrePostArgs.

Recently we came across a scenario where we needed to check if a field has changed after super() in update method of a table. Back in the days of AX 2012 you could easily compare original field’s value with current using orig() method before super() and call necessary logic after.

public void update()
{
    boolean myFieldHasChanged = this.MyField != this.orig().MyField;

    super();

    if (myFieldHasChanged)
    {
        this.doStuff();
    }
}

Now we want to do the same using extensions. We can create Pre and Post event handlers, but they are static, so we need a way to pass a value between them.
First option is to use static field, like it’s done in RunBase extension example

public static class MyTableEventHandler
{
    private static UnknownNoYes myFieldHasChanged;

    [PreHandlerFor(tableStr(MyTable), tableMethodStr(MyTable, update))]
    public static void MyTable_Pre_update(XppPrePostArgs _args)
    {
        MyTable myTable = _args.getThis() as MyTable;

        if (myTable.MyField != myTable.orig().MyField)
        {
            MyTableEventHandler::myFieldHasChanged = UnknownNoYes::Yes;
        }
        else
        {
            MyTableEventHandler::myFieldHasChanged = UnknownNoYes::No;
        }
    }

    [PostHandlerFor(tableStr(MyTable), tableMethodStr(MyTable, update))]
    public static void MyTable_Post_update(XppPrePostArgs _args)
    {
        MyTable myTable = _args.getThis() as MyTable;

        if (MyTableEventHandler::myFieldHasChanged == UnknownNoYes::Yes)
        {
            myTable.doStuff();
        }

        MyTableEventHandler::myFieldHasChanged = UnknownNoYes::Unknown;
    }
}

Another option is to use XppPrePostArgs as a vehicle for new parameter. XppPrePostArgs has collection of parameters under the hood, so nothing stops us to add one more and framework will take care of passing it between Pre and Post event handler!

XppPrePostArgs_collection.jpg

public static class MyTableEventHandler_XppPrePostArgs
{
    const static str myFieldHasChangedArgName = 'myFieldHasChanged';

    [PreHandlerFor(tableStr(MyTable), tableMethodStr(MyTable, update))]
    public static void MyTable_Pre_update(XppPrePostArgs _args)
    {
        MyTable myTable = _args.getThis() as MyTable;

        boolean myFieldHasChanged = myTable.MyField != myTable.orig().MyField;

        <strong>_args.addArg(MyTableEventHandler_XppPrePostArgs::myFieldHasChangedArgName, myFieldHasChanged);</strong>
    }

    [PostHandlerFor(tableStr(MyTable), tableMethodStr(MyTable, update))]
    public static void MyTable_Post_update(XppPrePostArgs _args)
    {
        MyTable myTable = _args.getThis() as MyTable;

        <strong>boolean myFieldHasChanged = _args.getArg(MyTableEventHandler_XppPrePostArgs::myFieldHasChangedArgName);
</strong>
        if (myFieldHasChanged)
        {
            myTable.doStuff();
        }
    }
}

Using one of these approaches you should remember that static fields apply to the class, not to instances of the class, so do not mix well with concurrency. Trick with XppPrePostArgs tightly depends on current implementation, that could be changed anytime and comes with no warranty.

To overcome this and other limitations of extensions capabilities Microsoft is introducing Method wrapping and chain of command and I’m pretty sure that we’ll see blogs on this from my MVP fellows soon.

Advertisements

AX 7. Link class instances to a form.

In one of my previous blogs we discussed how to override form control methods or form data source field methods without overlaying using registerOverrideMethod(). But what if we need to link class instance to a form? We have two options: use form extension classes with [ExtensionOf] attribute or register class instance on the form using formRun.registerExtensionInstance() method. Main difference between them is that with extension classes you add new methods or state extending form functionality and using registerExtensionInstance() we link class instance with new logic to a from, so we can have one class that could handle multiple forms. First approach is described in this blog post, so today we will focus on the second one.

Under the hood FromRun class has map variable to store registered instances:

Map formExtensionInstance = new Map(Types::string, Types::Class);

And 3 methods to work with it:

/// <summary>
/// Registers an extension instance.
/// </summary>
/// <param name="_extensionKey">The key for the extension.</param>
/// <param name="_extensionInstance">The instance for the extension.</param>
public void registerExtensionInstance(str _extensionKey, Object _extensionInstance)
{
    formExtensionInstance.insert(_extensionKey, _extensionInstance);
}
/// <summary>
/// Gets a registered extension instance.
/// </summary>
/// <param name="_extensionKey">The key for the extension.</param>
/// <returns>The instance for the extension if one is found.</returns>
public Object getExtensionInstance(str _extensionKey)
{
    return formExtensionInstance.lookup(_extensionKey);
}
/// <summary>
/// Gets a Boolean value indicating if an extension with the specified key is registered.
/// </summary>
/// <param name="_extensionKey">The key for the extension.</param>
/// <returns>true if an exteinsion with the specified key is registered; otherwise; false.</returns>
public boolean hasExtensionInstance(str _extensionKey)
{
    return formExtensionInstance.exists(_extensionKey);
}

There is a common way in standard code how to build extension classes:

  1. Create new class and add variable to store FormRun object, flag that indicates if instance is initialized and other required variables. Let’s add FormStringControl for example.
public class MySampleExtension
{
    FormRun element;
    boolean initialized;

    FormStringControl myFormStringControl;
}
  1. Create new method to initialize FormRun and register current instance on the form.
public void new(FormRun _formRunInstance)
{
    initialized = false;
    element     = _formRunInstance;

    // register this extension with the main form
    element.registerExtensionInstance(classStr(MySampleExtension), this);
}
  1. Create init() method to initialize all required variables and register override if needed.
private void init()
{
    if (initialized)
    {
        return;
    }

    initialized = true;

    myFormStringControl = element.design().controlName(formControlStr(MySampleForm, MyFormStringControl));

    //register overrides here if needed
}
  1. Subscribe to form pre init() event to register extension instance.
[FormEventHandler(formStr(MySampleForm), FormEventType::Initializing)]
public static void MySampleForm_OnInitializing(xFormRun _sender, FormEventArgs _e)
{
    MySampleExtension extensionInstance = new MySampleExtension(sender);
}
  1. Subscribe to form post init() event to initialize extension instance.
[FormEventHandler(formStr(MySampleForm), FormEventType::Initialized)]
{
    // Initialize the instance of this form extension handler now that the controls exist
    FormRun mySampleForm = _sender as FormRun;
    MySampleExtension extensionInstance  = mySampleForm.getExtensionInstance(classStr(MySampleExtension));

    extensionInstance.init();
}
  1. Use extension class
[FormControlEventHandler(formControlStr(MySampleForm, MySampleButton), FormControlEventType::Clicked)]
public static void mySampleButton_OnClicked(FormControl _sender, FormControlEventArgs _e)
{
    if (_sender && _sender.formRun())
    {
        FormRun formRun = _sender.formRun();

        MySampleExtension extensionInstance = formRun.getExtensionInstance(classStr(MySampleExtension));

        extensionInstance.mySampleButtonClicked();
    }
}

public void mySampleButtonClicked()
{
    myFormStringControl.visible(false);
}

That’s pretty much all of it.

AX 7. Extending forms using Form Parts.

Update 3 brought significant improvement to a form extension experience – ability to add form parts. Why it’s important? Previously we were able to add new data sources, new controls or modify some properties of existing controls. However, we did not have option to override form data source methods, we were able to use events only and it is not enough in some cases. Classic example is adding new InventDim data source on a form, where you need to comment out super() of data source write() method.

Now you can create new form, where you have full control over all the code and use it as a form part on a form extension.

Let’s look how it’s done in a standard application using CaseDetail.Extension form extension as an example.

casedetail-extension

This extension has a couple of new form parts. Each form part is a simple form:

CaseMoreInformation_CustTable.jpg

As you can see, it has one data source and a group with several fields. Here it is used to show fields in a details format, however, you can use grid and action panes if you want. A good example is Addresses and Contact information tabs on customer form that are done with form parts as well.

The most interesting part is how to link new form part with form extension. First, you need to add new “From part” control to a form extension design and specify display menu item name of a form part. After that you will see new “Links” node.  It supports four different types of links:

Field relation link Link the form data source with the form part data source by the field.
Field value link Link the form data source with the form part data source by specifying fixed value for the form part data source field.
Table relation link Link the form data source with the form part data source using relation on the form data source table to the form part data source table.
Target table relation link Link the form data source with the form part data source using relation on the form part data source table to the form data source table.

Choose link type that suits, set properties and you are good to go!

AX 7. URLUtility extension to create deep links.

In AX 7 we can use URL to open any form and it’s quite handy. Out of the box there is a feature called “Get link” that helps us to generate and share links. We can find it under Options -> Share -> Get Link.

GetLink.png

However, it has a limitation – you cannot generate link to open a specific record (so called “deep links”).

We can generate deep links from code by using URLGenerator class (there is a wiki article with explanations ), but it requires quite a bit of code to write. So I did small extension to URLUtility class to simplify this process.

It adds two new methods: generateRecordUrlFromDataSource and generateRecordUrl.

  • generateRecordUrlFromDataSource accepts form data source and generates a link to the record based on current record primary index fields.
  • generateRecordUrl accepts all parameters that are required to open a form, like menu item name and type, form data source name and map with key field’s IDs and values.
using Microsoft.Dynamics.AX.Framework.Utilities;
using Microsoft.Dynamics.@Client.ServerForm.Contexts;

/// <summary>
/// The class <c>URLUtility_Extension</c> contains extension methods for the <c>URLUtility</c> class.
/// </summary>
[ExtensionOf(classStr(URLUtility))]
public static class URLUtility_Extension
{
    public static str generateRecordUrl(str _menuItemName, MenuItemType _menuItemType, DataSourceName _dataSourceName, Map _indexFieldValuesMap, DataAreaId _dataAreaId = curExt())
    {
        System.Uri host                     = SessionContext::Get_Current().Get_RequestUrl();
        UrlHelper.UrlGenerator generator    = new UrlHelper.UrlGenerator();
        generator.MenuItemName              = _menuItemName;
        generator.MenuItemType              = _menuItemType;
        generator.HostUrl                   = host.GetLeftPart(System.UriPartial::Path);
        generator.Company                   = _dataAreaId;
        generator.EncryptRequestQuery       = true;

        if (_dataSourceName && _indexFieldValuesMap)
        {
            MapEnumerator mapEnumerator = _indexFieldValuesMap.getEnumerator();

            var requestQueryParameterCollection = generator.RequestQueryParameterCollection;

            while (mapEnumerator.moveNext())
            {
                requestQueryParameterCollection.UpdateOrAddEntry(_dataSourceName, mapEnumerator.currentKey(), mapEnumerator.currentValue());
            }
        }

        return generator.GenerateFullUrl().AbsoluteUri;
    }

    public static str generateRecordUrlFromDataSource(FormDataSource _formDataSource)
    {
        FormRun         formRun         = _formDataSource.formRun();
        str             menuItemName    = formRun.args().menuItemName();
        MenuItemType    menuItemType    = formRun.args().menuItemType();
        DataSourceName  dataSourceName  = _formDataSource.name();

        TableId   tableId   = _formDataSource.table();
        DictTable dictTable = new DictTable(tableId);
        DictIndex dictIndex = new DictIndex(tableId, dictTable.primaryIndex());

        int     fieldCount          = dictIndex.numberOfFields();
        Map     indexFieldValuesMap = new Map(Types::String, Types::String);
        Common  record              = _formDataSource.cursor();
        FieldId primaryKeyFieldId;

        for (int fieldIndex = 1; fieldIndex <= fieldCount; fieldIndex++)
        {
            primaryKeyFieldId = dictIndex.field(fieldIndex);

            indexFieldValuesMap.insert(fieldId2Name(tableId, primaryKeyFieldId), any2Str(record.(primaryKeyFieldId)));
        }

        return URLUtility::generateRecordUrl(menuItemName, menuItemType, dataSourceName, indexFieldValuesMap, record.DataAreaId);
    }
}

To test this class, you can use simple job:

class RunnableClass1
{
    public static void main(Args _args)
    {
        Map indexFieldValuesMap = new Map(Types::String, Types::String);
        indexFieldValuesMap.insert(fieldStr(VendTable, AccountNum), '1002');

        Box::info(URLUtility::generateRecordUrl(menuItemDisplayStr(VendTable), MenuItemType::Display, identifierStr(VendTable), indexFieldValuesMap, 'usmf'));
    }
}

It’s an initial version, so feel free to post comments in case of any bugs!

AX 7. Trick to override method in derived class without overlaying.

Scenario:

The VendDocumentLineType class has several subclasses that handle specific types of vendor document lines. We want to change a behaviour for invoice lines and implement new logic for Purchase Unit field defaulting.

Solution using overlaying:

Overlay VendDocumentLineType_Invoice class and override determineDefaultPurchUnit method.

Solution without overlaying:

venddocumentlinetypehierarchy

1) Create class derived from VendDocumentLineType_Invoice:

[Form]
/// <summary>
/// The <c>VendDocumentLineType_Invoice_Sample</c> class is used for validation and applying default values to invoice lines.
/// </summary>
class VendDocumentLineType_Sample extends VendDocumentLineType_Invoice
{
}

2) Override determineDefaultPurchUnit method:

protected PurchUnit determineDefaultPurchUnit()
{
    // implement new behaviour
    return VendParameters::find().DefaultPurchUnit_Sample;
}

3) Create post event handler for VendDocumentLineType constructor to replace VendDocumentLineType_Invoice with new implementation:

/// <summary>
/// Handles events raised by <c>VendDocumentLineType</c> class.
/// </summary>

public static class VendDocumentLineTypeEventHandler_Sample
{
    /// <summary>
    /// Post event handler for VendDocumentLineType.createFromTable(...)
    /// </summary>
    /// <param name="_args"></param>
    [PostHandlerFor(classStr(VendDocumentLineType), staticMethodStr(VendDocumentLineType, createFromTable))]
    public static void VendDocumentLineType_Post_createFromTable(XppPrePostArgs _args)
    {
        // get params from args
        VendDocumentLineMap vendDocumentLineMap = _args.getArg(identifierStr(_vendDocumentLineMap));
        PurchLine           purchLine           = _args.getArg(identifierStr(_purchLine));
        PurchParmUpdate     purchParmUpdate     = _args.getArg(identifierStr(_purchParmUpdate));

        // construct and init new object
        VendDocumentLineType strategy = VendDocumentLineType_Invoice_Sample::createFromTable(vendDocumentLineMap, purchLine, purchParmUpdate);

        // replace return value only if new strategy is created; otherwise use standard
        if (strategy)
        {
           // replace return value with new implementation
            _args.setReturnValue(strategy);
        }
    }

}

4) Implement methods that will instantiate and initialize VendDocumentLineType_Invoice_Sample class:

/// <summary>
/// Constructs a new instance of a <c>VendDocumentLineType_Invoice_Sample</c> class derivative.
/// </summary>
/// <param name="_vendDocumentLineMap">
/// A <c>VendDocumentLineMap</c> record.
/// </param>
/// <param name="_purchLine">
/// A <c>PurchLine</c> table record that is used when you apply the default values; optional.
/// </param>
/// <param name="_purchParmUpdate">
/// A <c>PurchParmUpdate</c> table record that is used when you apply the default values; optional.
/// </param>
/// <returns>
/// A <c>VendDocumentLineType_Invoice_Sample</c> class.
/// </returns>
public static VendDocumentLineType_Invoice_Sample createFromTable(VendDocumentLineMap _vendDocumentLineMap, PurchLine _purchLine = null, PurchParmUpdate _purchParmUpdate = null)
{
    VendDocumentLineType_Invoice_Sample strategy;

    // VendDocumentLineType_Invoice is created only for this 2 types
    if (_vendDocumentLineMap.Ordering == DocumentStatus::Invoice ||
        _vendDocumentLineMap.Ordering == DocumentStatus::ApproveJournal)
    {
        strategy = new VendDocumentLineType_Invoice_Sample();
        strategy.init(_vendDocumentLineMap, _purchLine, _purchParmUpdate);
    }

    return strategy;
}

public void init(VendDocumentLineMap _vendDocumentLineMap, PurchLine _purchLine, PurchParmUpdate _purchParmUpdate)
{
    // code copied from VendDocumentLineType::createFromTable to init new instance
    this.vendDocumentLineMap(_vendDocumentLineMap);
    this.purchLine(_purchLine);
    this.physicalStrategy(VendDocumentLineTypePhysical::createFromTable(this, _vendDocumentLineMap));
    this.purchParmUpdate(_purchParmUpdate);
}

You can apply this approach to any class hierarchy in AX that has construct method (like SalesLineType, PurchLineType, etc.) when you need to override methods in derived class to implement new behaviour.

AX 7. Get information about table extension using Microsoft.Dynamics.Ax.Xpp.MetadataSupport

In AX 2012 we used DictTable class to get information about tables, table fields, indexes and field groups. However we cannot retrieve any information about table extensions using this class, so Microsoft.Dynamics.Ax.Xpp.MetadataSupport was introduced in AX 7.

Let’s look at simple job that will return all table extensions and all fields from table extensions for CompanyInfo table.

public static void main(Args _args)
{
    var axTableExtensions = Microsoft.Dynamics.Ax.Xpp.MetadataSupport::GetTableExtensionsForTable(tableStr(CompanyInfo));
    System.Collections.IEnumerator axTableExtensionsEnumerator = axTableExtensions.GetEnumerator();

    while (axTableExtensionsEnumerator.moveNext())
    {
        Microsoft.Dynamics.AX.Metadata.MetaModel.AxTableExtension axTableExtension = axTableExtensionsEnumerator.get_Current();

        info(strFmt("Extension name %1", axTableExtension.Name));

        var axTableExtensionFields = axTableExtension.Fields;

        System.Collections.IEnumerator axTableExtensionFieldsEnumerator = axTableExtensionFields.GetEnumerator();

        while (axTableExtensionFieldsEnumerator.moveNext())
        {
            Microsoft.Dynamics.AX.Metadata.MetaModel.AxTableField axTableExtensionField = axTableExtensionFieldsEnumerator.get_Current();

            info(strFmt("Field name %1", axTableExtensionField.Name));
            info(strFmt("Mandatory %1", axTableExtensionField.Mandatory));
            info(strFmt("Allow edit %1", axTableExtensionField.AllowEdit));
        }
    }
}

AX 7. Accessing private\protected class methods and members from extension code.

All class member variables are protected by default in AX 7, so it is impossible to access them from extensions. It becomes a real problem when you try to extend classes like SalesLineType.

For example, we want to add custom logic on sales line insert event. Call to super() is commented out so we cannot create pre or post event handlers. We may try to create event handlers for SalesLineType.insert() mehtod, but we won’t get access to salesLine buffer because this variable is protected.

There are two options: use overlaying or use reflection.

Overlaying is preferable approach, but today we will talk about reflection to explain this option.

Reflection is usually used for unit testing in case you need to cover protected or private code and it is hard to call this code using public API.

It has bunch of disadvantages:

  • It breaches entire basis of OO programming.
  • Slow performance.
  • Possible issues with future updates. Private methods could be changed at any time.

However, once you may get into situation where it could be the only option so it’s better to know about this possibility.

Let’s try to change some fields on sales line insert using reflection.

Create new event handler class for SalesLineType and subscribe to post insert:

using System.Reflection;

///
<summary>
/// Handles events raised by <c>SalesLineTypeEventHandler</c> class.
/// </summary>

public class SalesLineTypeEventHandler
{
    [PostHandlerFor(classStr(SalesLineType), methodStr(SalesLineType, insert))]
    public static void SalesLineType_Post_insert(XppPrePostArgs _args)
    {
        SalesLineType salesLineType = _args.getThis();

        var bindFlags = BindingFlags::Instance | BindingFlags::NonPublic;

        var field = salesLineType.GetType().GetField("salesLine", bindFlags);

        SalesLine salesLine = field.GetValue(salesLineType);

        if (salesLine)
        {
            salesLine.MyNewField = 42;
            salesLine.doUpdate();
        }
    }
}

Also we can call private or protected method:

var bindFlags = BindingFlags::Instance | BindingFlags::NonPublic;

var methodInfo = salesLineType.GetType().GetMethod(methodStr(SalesLineType, checkQuantityUpdate), bindFlags);

if (methodInfo)
{
    methodInfo.Invoke(salesLineType,  new System.Object[0]());
}

You can read more about reflection on msdn

Thanks to Simon Buxton for rising this on yammer.