AX 7. Display methods and Form Observability.

In AX 2012 display methods are called every time when a form is redrawn, however this behaviour is changed in AX 7 to optimize performance in web client.

Using Form Observability, we can notify form that display method should be updated or recalculated when related variable, data source or control are updated.

There are three different scenarios:

  • Display method depends on a variable.
  • Display method depends on another data source.
  • Display method depends on an event.

Display method depends on a variable.

For example, we have a form level variable “valid, display method that shows a status on the form and button to update the status.

MyForm

[Form]
public class MyForm extends FormRun
{
    [FormObservable]
    boolean valid;

    display public boolean displayIsValid()
    {
        return valid;
    }

    [Control("Button")]
    class Validate
    {
        public void clicked()
        {
            super();
            valid = true;
        }
    }
}

When the form is opened display method will be executed and show default value (false) on the form. When the button is clicked AX does not execute display method so value on the form remains the same.

However variable is decorated with FormObservable attribute and AX will observe it and automatically execute display method when value of the variable is changed.

Form can observe form level variables and class level variables as well. For example, AssetChangeGroup class has couple of variables decorated with FormObservable attribute.

class AssetChangeGroup extends RunBase
{
    [FormObservable]
    AssetGroupId        assetGroupId;

    [FormObservable]
    NoYesId             changeAssetId;

    [FormObservable]
    AssetId             assetId;
}

This class is used in edit methods of AssetChangeGroup form.

[Form]
public class AssetChangeGroup extends FormRun
{
    edit AssetGroupId assetGroupId(boolean set, AssetGroupId _assetGroupId)
    {
        if (set)
        {
            assetChangeGroup.parmAssetGroupId(_assetGroupId);
            element.initAssetId();
            element.enableOrDisableAssetId();
        }

        return assetChangeGroup.parmAssetGroupId();
    }

So any update of class level variables will trigger update of edit methods on the form.

Display method depends on another data source.

In the next example we have a simple header-lines form.

HeaderLine

There is a display method on the header grid that shows total lines quantity.

[Form]
public class MyForm extends FormRun
{
    [DataSource]
    class MyHeader
    {
        public display Qty displayTotal(MyHeader _myHeader)
        {
            MyLine_DS.observe();
            MyLine  myLineLocal;

            select sum(Qty) from myLineLocal
                where myLineLocal.MyHeader == _myHeader.RecId;

            return myLineLocal.Qty;
        }
    }
}

Here MyLine_DS.observe() is used to observe MyLine_DS data source. AX will automatically execute display method if any record in the lines grid is updated, inserted or deleted.

Display method depends on an event.

In some cases, display method does not depend on a forms state or a data source. However, it should be refreshed on a specific event, like a button click or a method call. To handle this, we can use FormObservableLink object.

In the next example we have a form with a display method and a button that invokes a class method. This method will update  data that the display method depends on and invoke  the method on the form to notify the display method that it should be updated.

UpdateLines

Class does records update and calls a method on the form:

class MyClass
{
    public static void main(Args _args)
    {
        MyHeader myHeader = _args.record() as MyHeader;

        if (myHeader)
        {
            MyLine myLine; 

            ttsbegin;
            update_recordset myLine
            setting Qty = 10
                where myLine.MyHeader == myHeader.RecId;
            ttscommit;

            var caller = _args.caller();

            if (caller && Global::formHasMethod(caller, formMethodStr(MyForm, refreshTotals)))
            {
                caller.refreshTotals();
            }
        }
    }
}

On the form linesTotalObservableLink.markChanged() is used to notify the form that it should execute the display method and linesTotalObservableLink.observe() to receive notification.

[Form]
public class MyForm extends FormRun
{
    FormObservableLink linesTotalObservableLink;

    public void refreshTotals()
    {
        linesTotalObservableLink.markChanged();
    }

    public void init()
    {
        super();
        linesTotalObservableLink = new FormObservableLink();
    }

    [DataSource]
    class MyHeader
    {
        public display Qty displayTotal(MyHeader _myHeader)
        {
            linesTotalObservableLink.observe();

            MyLine  myLineLocal;

            select sum(Qty) from myLineLocal
                where myLineLocal.MyHeader == _myHeader.RecId;

            return myLineLocal.Qty;
        }
    }
}
Advertisements

Query::insert_recordset() ignores XDS policy.

Recently we did a customization to create custom XDS policy and found out that Query::insert_recordset() ignores it, so all data is selected.

Simple workaround is to rewrite Query::insert_recordset() to while(queryRun.Next()).

However no one really wants to modify bunch of standard code that uses this method so connect issue was created. Feel free to vote for it.

It could be reproduced in AX 2012 and AX 7 as well.

AX 2012. SysEmailDistributor does not send emails to multiply recipients.

We have a couple of modifications that use SysEmailTable::sendMail() to send emails and found that if you pass multiple recipients into _emailAddr parameter AX will send email only to last recipient.

This issue was introduced with SysMailerNet in R3. In previous version we were able to pass multiple email addresses using a semicolon as delimiter. Now each email address should be added using SysMailerNet.tos.add() method.

However SysMailerNet::quickSend() method does not have this issue, so we can use it as an example to fix SysEmailDistributor.

Let’s modify SysEmailDistributor::processEmails() method. In standard code we can see that recipient from outgoingEmailTable is passed to mailer.tos().

//instantiate email
mailer.fromAddress(outgoingEmailTable.Sender,outgoingEmailTable.SenderName);
tos = mailer.tos();
tos.add(outgoingEmailTable.Recipient);

To fix our issue we need to split emails and pass them to tos.add() in a loop.

//multiple recipients fix -->
List emailAddresses;
ListEnumerator enum;
//multiple recipients fix <--

…

//instantiate email
mailer.fromAddress(outgoingEmailTable.Sender,outgoingEmailTable.SenderName);
tos = mailer.tos();
/* Orig -->
tos.add(outgoingEmailTable.Recipient);
Orig <-- */
//multiple recipients fix -->
emailAddresses = SysEmailDistributor::splitEmail(outgoingEmailTable.Recipient);
enum = emailAddresses.getEnumerator();
while (enum.moveNext())
{
    tos.add(enum.current());
}
//multiple recipients fix <--
 

By the way this issue is fixed in AX 7.

Cannot create a record in Dimension history for documents (InventReportDimHistory). Error on Vendor invoice posting.

Usually we can get this error for non PO vendor invoice.

To fix it you need to change one of these two number sequences:

  • Internal invoice
  • Internal credit note

AP_NumberSeq.jpg

Because if they have identical structure they will generate  same numbers and that’s the real cause of this error.

Now let’s look what is happening under the hood.

Unique index of InventReportDimHistory consists of four fields:

  • TransRefId
  • InventTransId
  • TransactionLogType
  • InventDimId

InventTransId is always blank, because non PO invoice lines could have only non-stocked items or procurement categories.

InventDimId could be identical for multiple lines, for example “AllBlank” or any site and warehouse combination.

TransactionLogType is a document type. For invoice it is “PurchInvoice”, for packing slip –  “PurchPackingSlip” and so on.

The last field is TransRefId which is based on InternalInvoiceId field from the Invoice Journal.

InternalInvoiceId is number sequence generated, so it should be unique for each posted invoice.  However, AX could use two different number sequences for this field.

First one is used for invoices and second for credit notes and if this two number sequence have same structure, for example usmf-#######, you could hit this issue when you will create credit note, because at some point AX could generate number for credit note that was already used for invoice.

Please note that other number sequences can be used for some localisations like RU or MY. To get list of all possible number sequence you can look at PurchInvoiceJournalCreate.allocateNumAndVoucher() method.

 

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.

AX 7. How to create new excel template for general journal lines.

Open lines in Excel is a new cool feature in AX 7 that allows you to edit general journal lines using excel.

OpenInExcel

But unfortunately it supports only two account types out of the box: ledger account and customer account. Today we will create a new one for bank.

First of all, we need to create a new Data Entity. There is no difference between Data Entities for account types so I simply copied LedgerJournalLine entity, renamed it and changed AccountType range value to “Bank”.

DataEntity

Public collection name and public entity name properties should be changed, relation entity roles should be updated as well.

Next we need to create a new Excel template file.  I used Excel workbook designer in AX to generate new template based on the new Data Entity. I made my design pretty similar to existing templates for general journal.

Excel

Please note that template has two data entities: one for header and another for lines. As you can see I took the same header entity as other general journal templates has.

This template file should be added to AOT into resources node.

Finally, we need to create a new class. This class should be derived from DocuTemplateRegistrationBase and implement LedgerIJournalExcelTemplate.

 

using Microsoft.Dynamics.Platform.Integration.Office;

/// <summary>
/// The <c>BankJournalExcelTemplate</c> is the supporting class for the Bank Journal Template.
/// </summary>
class BankJournalExcelTemplate extends DocuTemplateRegistrationBase implements LedgerIJournalExcelTemplate
{
    // resource that contains xlsx file.
    private const DocuTemplateName ExcelTemplateName = resourceStr(BankJournalTemplate);
    // lines Data Entity
    private const EntityName LineEntityName = tableStr(BankJournalLineEntity);
    private const FieldName LineEntityJournalNum = fieldStr(BankJournalLineEntity, JournalBatchNumber);
    private const FieldName LineEntityDataAreaId = fieldStr(BankJournalLineEntity, dataAreaId);
    private const FieldName HeaderEntityName = tableStr(LedgerJournalHeaderEntity);
    private const FieldName HeaderEntityJournalNum = fieldStr(LedgerJournalHeaderEntity, JournalBatchNumber);
    // header Data Entity.
    private const FieldName HeaderEntityDataAreaId = fieldStr(LedgerJournalHeaderEntity, dataAreaId);

    public void registerTemplates()
    {
        this.addTemplate(OfficeAppApplicationType::Excel,
                         ExcelTemplateName,
                         ExcelTemplateName,
                         "@GeneralLedger:LedgerJournalLineEntryTemplateDescription",
                         "@MyLabels:BankLedgerDailyJournalTemplateName",
                         NoYes::No,
                         NoYes::No);
    }

    public boolean isJournalTypeSupported(LedgerJournalType _ledgerJournalType)
    {
	// only daily journals are supported in this template.
        return _ledgerJournalType == LedgerJournalType::Daily;
    }

    public DocuTemplateName documentTemplateName()
    {
        return ExcelTemplateName;
    }

    public Set supportedAccountTypes()
    {
	// Set with supported account types.
        Set accountSetTypes = new Set(Types::Integer);

        accountSetTypes.add(LedgerJournalACType::Bank);
        accountSetTypes.add(LedgerJournalACType::Ledger);

        return accountSetTypes;
    }

    public Set supportedOffsetAccountTypes()
    {
        // Set with supported offset account types.
        Set offsetAccountTypeSet = new Set(Types::Integer);

        offsetAccountTypeSet.add(LedgerJournalACType::Ledger);

        return offsetAccountTypeSet;
    }

    public boolean validateJournalForTemplate(LedgerJournalTable _ledgerJournalTable)
    {
        return LedgerJournalExcelTemplate::validateJournalForTemplate(_ledgerJournalTable, this);
    }

    public EntityName headerEntityName()
    {
        return HeaderEntityName;
    }

    public EntityName lineEntityName()
    {
        return LineEntityName;
    }

    public FieldName headerJournalBatchNumberFieldName()
    {
        return HeaderEntityJournalNum;
    }

    public FieldName headerDataAreaFieldName()
    {
        return HeaderEntityDataAreaId;
    }

    public FieldName lineJournalBatchNumberFieldName()
    {
        return LineEntityJournalNum;
    }

    public FieldName lineDataAreaFieldName()
    {
        return LineEntityDataAreaId;
    }

    public FilterCollectionNode appendHeaderEntityFilters(FilterCollectionNode _headerFilter, ExportToExcelFilterTreeBuilder _headerFilterBuilder)
    {
        return _headerFilter;
    }

    public FilterCollectionNode appendLineEntityFilters(FilterCollectionNode _lineFilter, ExportToExcelFilterTreeBuilder _lineFilterBuilder)
    {
	// creates excel filter to show only lines that have Bank account type
        // and Ledger offset account type
        FilterCollectionNode lineFilter = _lineFilterBuilder.and(
            _lineFilterBuilder.areEqual(fieldStr(CustInvoiceJournalLineEntity, AccountType), LedgerJournalACType::Bank),
            _lineFilterBuilder.areEqual(fieldStr(CustInvoiceJournalLineEntity, OffsetAccountType), LedgerJournalACType::Ledger));

        return _lineFilterBuilder.and(_lineFilter, lineFilter);
    }
}

As you can see, code is pretty straightforward and there is only one interesting method appendLineEntityFilters(). It uses FilterCollectionNode to create filters for template, so this template will show only journal lines that has account type bank and offset account type ledger.

When development is done, we need to reload system templates. Go to Common -> Office integration -> Document templates and click “Reload system templates”.

ReloadSystemTemplates.jpg

After that, our new template should appear under Open lines in Excel button.

OpenLinesInExsel.jpg