Advanced cross-reference search.

Cross-references search

Cross-reference is one of the best tools we have in AX that is used by developers daily, however, not everyone is using it for 100%.

Recently, I was asked how to find cross-references for a kernel method that is not defined on a table, because you cannot right click on it 😊   And that’s a good question, often we want to find if there is a call to doUpdate or doInsert somewhere.

In AX 2012 go to Tools -> Cross-reference -> Names and filter by table or method name and then click Used by.

SalesLine.DoUpdate_Names

SalesLine.DoUpdate_UsedBy

Using this form, we can find usage of CRL types as well, for example, Microsoft.Dynamics.IntegrationFramework.  It is defined in AOT under references node, however, from there you cannot find any references.  Names would show you everything!

CLRTypeUsage

What about D365FOE?

As we know, it’s a bit different here. Cross-references data is moved to DYNAMICSXREFDB database.

DYNAMICSXREFDB

And it makes perfect sense because you need to pay for each GB of DB space in Production.

Using next SQL statement, we can find usage of any object. For example, Microsoft.Dynamics.IntegrationFramework :

SELECT
	sourceName.[Path] as sourcePath,
	targetName.[Path] as targetPath,
	[Kind],
	[Line],
	[Column],
	sourceModule.[Module] as sourceModule,
	targetModule.[Module] as targetModule
	FROM dbo.[References]
	INNER JOIN dbo.[Names] sourceName
		ON dbo.[References].[SourceId] = sourceName.[Id]
	INNER JOIN dbo.[Modules] sourceModule
		ON sourceName.[ModuleId] = sourceModule.[Id]
	INNER JOIN dbo.[Names] targetName
		ON dbo.[References].[TargetId] = targetName.[Id]
	INNER JOIN dbo.[Modules] targetModule
		ON targetName.[ModuleId] = targetModule.[Id]
	WHERE targetName.[Path] like '%Microsoft.Dynamics.IntegrationFramework%'

CLRTypeUsage_D365FOE

Where Kind is:

    /// <summary>
    /// Types of Cross References
    /// </summary>
    public enum CrossReferenceKind
    {
        /// <summary>
        /// Type not specified. Used for queries
        /// </summary>
        Any = 0,

        /// <summary>
        /// Indicates that the reference is a Method Call
        /// </summary>
        MethodCall = 1,

        /// <summary>
        /// Type reference
        /// Indicated that the type is used (variable and field declaration, attributes, function return type, etc)
        /// </summary>
        TypeReference = 2,

        /// <summary>
        /// Interface implementation
        /// Indicates that the source entity is implementing this interface
        /// </summary>
        InterfaceImplementation = 3,

        /// <summary>
        /// Class Extended
        /// Indicates that the source entity is extending this class or interface
        /// </summary>
        ClassExtended = 4,

        /// <summary>
        /// Test Call
        /// Indicates that the source entity (test) directly or indirectly calls an application method.
        /// </summary>
        TestCall = 5,

        /// <summary>
        /// Property
        /// Indicates that the source entity has a certain property.
        /// </summary>
        Property = 6,

        /// <summary>
        /// Attribute reference
        /// Indicated that an Attribute is used
        /// </summary>
        Attribute = 7,

        /// <summary>
        /// Test Helper Call
        /// Indicates that the source entity is a test helper.
        /// </summary>
        TestHelperCall = 8,

        /// <summary>
        /// Metadata or code Tag reference
        /// Indicates that the source tag is used on a metadata element, class or a method or a line of code.
        /// </summary>
        Tag = 9,
    }

Let’s try doUpdate:

DoUpdate_UsedBy_D365FOE

As we can see, result is different to AX 2012, where we could search for an individual table, now all Common methods have reference to Common.

 

Advertisements

D365FOE. FormHasMethod extension for form extension methods.

Recently I have seen multiple people asking how to check if form has method added by extension at run-time.  For form methods we can use Global::formHasMethod but it does not work with form extensions. I advised people to use new metadata API to do this and finally community user Axaptus wrote the code!

I tweaked it a bit to ignore method’s name case as AX does and to exclude private methods. Also I used it to extend standard formHasMethod method

/// <summary>
/// The class <c>Global_Extension</c> contains extension methods for the <c>Global</c> class.
/// </summary>

[ExtensionOf(classStr(Global))]
public static final class Global_Extension
{
    static boolean formHasMethod(FormRun fr, IdentifierName methodName)
    {
        boolean ret = next formHasMethod(fr, methodName);

        if (!ret)
        {
            ret = Global::formExtensionHasMethod_IM(fr, methodName);
        }

        return ret;
    }

    private static boolean formExtensionHasMethod_IM(FormRun _formRun, IdentifierName _methodName)
    {
        if (!_formRun || !_methodName)
        {
            return false;
        }

        boolean ret = false;

        try
        {
            System.Object[] extensions = Microsoft.Dynamics.Ax.Xpp.ExtensionClassSupport::GetExtensionsOnType(_formRun.GetType(), true);

            if (extensions)
            {
                System.Type    formRunExtensionType;
                System.Reflection.MethodInfo    methodInfo;
                var bindingFlags = BindingFlags::Public | BindingFlags::Instance | BindingFlags::Static | BindingFlags::IgnoreCase;
for (int i = 0; i < extensions.Length; i++)
                {
                    formRunExtensionType = extensions.GetValue(i);

                    methodInfo = formRunExtensionType.GetMethod(_methodName, bindingFlags);

                    if (methodInfo)
                    {
                        ret = true;
                        break;
                    }
                }
            }
        }
        catch (Exception::CLRError)
        {
            error(CLRInterop::getLastException().ToString());
        }

        return ret;
    }
}

Extending standard method has its pros and cons. From one side it will slow down execution of standard code that calls it when method does not exist, but it’s a rare case. From another side it allows you to reuse standard code without changing it and it could be handy in various places where AX looks for a method on a form.

Source code is available on GitHub

D365FOE. Issue with enums that have “Use Enum Value” property set to “No”.

Recently we have noticed incorrect behavior of enums that have “Use Enum Value” set to “No” and have gaps in enum values. These enums use values from properties when populated from user interface, however, X++ code uses generated values, that cause inconsistent behavior and data corruption.

To illustrate this issue, we will create new enum with two values: 0 and 10. Also we need to set “Use Enum Value” property to “No”.

MyEnum

MyEnumProperties

Zero:

MyEnumZero

Ten:

MyEnumTen

Also, we need simple table that has only one field, form to populate this field from UI and job to create data from X++.

Table:

MyTable

Form:

MyForm

Runnable class:

RunnableClass

Let’s run the class and check data in DB and UI.

In DB value is equal to value from enum properties – 10:

MyEnumInDBFromX++

On the form we can see empty value:

MyFormInsertFromX++

Let’s create new record using UI and check what is saved to DB.

MyFormInsertFromUI

MyEnumInDBFromUI

As you can see, value is equal to 1. For these enums value entered from UI will never be equal to value entered from X++ code, so if it is created in X++ user would see empty values, if it is created from UI X++ comparison like:

If (myTable.MyEnum == MyEnum::Ten)  

would always return false, even if user see “Ten” on UI, because values are different.

It was a common practice to have gaps in enum values for different layers, so you could avoid conflicts when new values are created in new version or a hotfix. AssetTransType is a good example, where localization related values start from 100, because they used to be on another layer.

However, most of standard enums use enum values, so probably that is why this bug was not spotted before.

As a good citizen, I filled a bug, but I have a sneaky suspicion that it won’t be fixed soon, so be aware of this volatile mix, try to avoid changing “Use Enum Value” property and review your enums in case you have one!

D365FO. Working with Azure File storage.

Azure Storage - Files

Current version of AX uses Azure Blob storage for various things like document handling, retail CDX files, DIXF and Excel add-in. You can find several blogs explaining how to upload and download files to Blob, SharePoint or temporary storage. However, what about file shares?

Azure File storage implements SMB 3.0 protocol and could be easily mapped to your local computer. You need just a few minutes to create new storage account and mount it, watch this how-to video for details.

To read file from newly created share we can use next code:

using Microsoft.Azure;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using Microsoft.WindowsAzure.Storage.File;

class RunnableClass1
{
    public static void main(Args _args)
    {
        System.IO.MemoryStream memoryStream;

        var storageCredentials = new Microsoft.WindowsAzure.Storage.Auth.StorageCredentials('AzureStorageAccountName', 'AzureStorageAccountKey');

        CloudStorageAccount storageAccount = new Microsoft.WindowsAzure.Storage.CloudStorageAccount(storageCredentials, true);

        CloudFileClient fileClient = storageAccount.CreateCloudFileClient();

        CloudFileShare share = fileClient.GetShareReference('AzureStorageShareName');

        if (share.Exists(null, null))
        {
            CloudFileDirectory rootDir = share.GetRootDirectoryReference();

            CloudFileDirectory fileDir = rootDir.GetDirectoryReference('folder');

            if (fileDir.Exists(null, null))
            {
                CloudFile file = fileDir.GetFileReference('file.txt');

                if (file.Exists(null, null))
                {
                    memoryStream = new System.IO.MemoryStream();
                    file.DownloadToStream(memoryStream, null, null, null);
                }
            }
        }
    }
}

References:

Edited:

Azure File Storage client source code could be found on GitHub

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.