Find assembly for a given type – in AX 7

Five years ago, I wrote a blog post called Assembly containing a given type, demonstrating how to find the assembly where a given type is defined. I needed the same thing in AX 7, but instead of using the same code, I decided to utilize the fact that I can now easily write parts of my application in C#.

I added a C# class library project to the assembly with my X++ project and created a class with this method:

public static System.Reflection.Assembly findAssembly(string typeName)
{
    var assemblies = AppDomain.CurrentDomain.GetAssemblies();
    return assemblies.FirstOrDefault(a => a.GetType(typeName, false, true) != null);
}

I built the solution, went to my X++ project and added a project reference to the C# project. (Right-click References node, choose Add reference…, select the project on the Projects tab and confirm.)

This allowed me to call my C# method from X++, which I did:

var assembly = MyClassLibrary.MyClass::findAssembly('Dynamics.AX.Application.AsyncTaskResult');
if (assembly != null)
{
    info(assembly.FullName);
    info(assembly.Location);
}

I saved a few lines of code, but more importantly I showed you how easy it is to use C# projects together with X++ projects. It’s extremely powerful, because C# (and other .NET languages) give you quite a few options that you don’t have in X++ (although it’s not the case of this particular example).

Installing deployable packages with Powershell

Installing deployable packages to an AX 7 environment can often be done just by a few clicks on LCS (as described in Apply a deployable package on a Microsoft Dynamics 365 for Operations system). Unfortunately the situation isn’t always that simple and you may have to install the package manually, using the process explained in Install a deployable package. It consists of quite a few manual steps and where there are repeated manual steps, one should always consider automation.

I’ve built a few Powershell functions to help with these tasks:

#region Parameters
$folder = 'C:\Temp'
$archiveFileName = 'Updates.zip'
$runbookId = 'MyRunbook1'
$ErrorActionPreference = 'Stop'
#endregion
 
#region Derived values
$file = Join-Path $folder $archiveFileName
$runbookFile = Join-Path $folder "$runbookId.xml"
$extracted = Join-Path $folder ([System.IO.Path]::GetFileNameWithoutExtension($archiveFileName))
$topologyFile = Join-Path $extracted 'DefaultTopologyData.xml'
$updateInstaller = Join-Path $extracted 'AXUpdateInstaller.exe'
#endregion
 
Function ExtractFiles
{
    Unblock-File $file
    Expand-Archive -LiteralPath $file -Destination $extracted
}
 
Function SetTopologyData
{
    [xml]$xml = Get-Content $topologyFile
    $machine = $xml.TopologyData.MachineList.Machine
 
    # Set computer name
    $machine.Name = $env:computername
 
    #Set service models
    $serviceModelList = $machine.ServiceModelList
    $serviceModelList.RemoveAll()
 
    $instalInfoDll = Join-Path $extracted 'Microsoft.Dynamics.AX.AXInstallationInfo.dll'
    [void][System.Reflection.Assembly]::LoadFile($instalInfoDll)
 
    $models = [Microsoft.Dynamics.AX.AXInstallationInfo.AXInstallationInfo]::GetInstalledServiceModel()
    foreach ($name in $models.Name)
    {
        $element = $xml.CreateElement('string')
        $element.InnerText = $name
        $serviceModelList.AppendChild($element)
    }
 
    $xml.Save($topologyFile)
}
 
Function GenerateRunbook
{
    $serviceModelFile = Join-Path $extracted 'DefaultServiceModelData.xml'
    & $updateInstaller generate "-runbookId=$runbookId" "-topologyFile=$topologyFile" "-serviceModelFile=$serviceModelFile" "-runbookFile=$runbookFile"
}
 
Function ImportRunbook
{
    & $updateInstaller import "-runbookfile=$runbookFile"
}
 
Function ExecuteRunbook
{
    & $updateInstaller execute "-runbookId=$runbookId"
}
 
Function RerunRunbook([int] $step)
{
    & $updateInstaller execute "-runbookId=$runbookId" "-rerunstep=$step"
}
 
Function SetStepComplete([int] $step)
{
    & $updateInstaller execute "-runbookId=$runbookId" "-setstepcomplete=$step"
}
 
Function ExportRunbook
{
    & $updateInstaller export "-runbookId=$runbookId" "-runbookfile=$runbookFile"
}

When you set parameters (such as the name of your package file) and run the script, you can then execute whole process by the following list of function calls:

ExtractFiles
SetTopologyData
GenerateRunbook
ImportRunbook
ExecuteRunbook

If needed, you can also use RerunRunbook and SetStepComplete (e.g. SetStepComplete 10).

Note that SetTopologyData takes data just from the current machine, but you can borrow the code and modify it, if you need something more sophisticated.

This should make things a bit easier and reduce unnecessary errors such as mistyped runbook IDs.

Application Explorer filtering

Application Explorer in AX 7 allows easy filtering, which is extremely useful, because trying to locate something in AOT is what we do all the time (I really missed this feature for many years). It looks simple, but it’s very powerful, if you know how to use it.

First of all, you can simply put a string in the search box on the top of Application Explorer and press enter. In AOT, it will show only elements with names containing the given string.

trivial

You can also click the little drop-down arrow on the right and choose additional filtering options, such as filtering by element type or model name.

filteroptions

Using filtering by type:”form”, I’m now getting only forms with names containing ListPage.

filterbytype

But what if you need some more advanced filtering, such as you want to find elements with names ending with a given text? Here is where it starts to be interesting. You can actually use regular expressions for filtering, therefore you can easily easily achieve that with ListPage$.

The dollar sign means the end of the string, therefore only names that satisfy this patter are those containing ListPage immediately followed by the end of the string.

Here is my result:

endswith

Similarly, you can use ^ character to match the beginning of a string. If you use them together to create a pattern like ^ListPage$, it will find only elements that are called ListPage. No characters before or after ListPage are allowed.

Only a single element satisfy this patter: the ListPage class.

exactname

We don’t have to stop there. What if you’re looking for a form with name starting with Cust and ending with ListPage, with any number of characters in between? Here we go:

complexexp

The dot means that there may be any character and * means that there may be any number of such characters.

In most cases, this is all you need to know about regular expressions to compose really powerful filtering patterns. It’s surely no problem for any software developer.

But regular expressions offer much more, if needed. Just to give you one more example, you might want to look for element names ending with numbers, such as Class1. Simple \d$ pattern will do the job.

endswithnumber

When writing more complicated regular expressions, you may find Quick Reference on MSDN very helpful.

I said that I missed this feature for many years and indeed, I now use it all the time. I simply love it!

Expression builder in AX 7

If you want to allow users configure certain conditions by themselves, considering using the Expression Builder control.

This is how it looks like:

simpleexpression

It exists already in AX 2012, where you can add it through the ManagedHost control. AX 7 doesn’t support managed controls anymore, but the Expression Builder has been redesigned and it’s now available as a native AX control.

Add control

The control allows you to add and remove conditions and combine them with AND and OR operators.

You can select fields, possibly from several tables.

fields

Then you choose an operator – which operators are available depends on field’s data type.

stringoperators

And finally you select a value. It will give you a lookup for available values, if applicable.

Some data types are handled in a special way.  For instance, fields with data types extending Money gets an additional field for currency, and the amount is converted to the right currency for comparison at runtime.

numwcurrency

Dealing with dates is even more complex. You can either pick a fixed date:

fixeddate

or define a date relatively to the current date or the current month:

relativedate

In addition, Expression Builder understands surrogate keys, expands financial dimension fields to individual dimensions and has a support for hierarchies.

If you want to see an example in the standard AX application, look at Organization administration > Workflow > Work item queue assignment rules.

A bit of technical details

To be able to use your own tables in Expression Builder, you have to define an AOT query (that’s where fields are taken from) and a class (“document”) inhering from WorkflowDocument.

If you want to add Expression Builder to your own form, you have to add a little bit of code, most importantly to indicate which document class (and – indirectly – which query) it will use. Expression Builder has ExpressionDocumentClass() method for that. Then you also need some logic for loading and saving expressions – look at an existing implementation for details.

The document class can have a few useful attributes and it can also define calculated values, which are then displayed in the list among normal fields. Calculated fields are important – not only they allow you to do any kind of calculation, but you can also use them to work around limitations of what expression you can write.

Consider this scenario: You want to define a condition for ShippingDate > Deadline. How can you put the Deadline field on the right side of an expression in expression builder? You can’t (or at least I haven’t figured out how), but you can create a calculated field, such as DaysToShipDeadline, defined as Deadline – ShippingDate. Then it’s trivial to configure the condition as DaysToShipDeadline < 0.

When your expression is defined and saved, you can call Expression::evaluate() to see if the condition it true or false for a given record:

ExpressionResultType result = Expression::evaluate(
    'usmf',            // Company
    tableNum(MyTable), // Table where is the record to check
    5637144576,        // ID of a record in MyTable
    expressionId,      // From ExpressionTable
   ExpressionDataSources::newExpressionDataSources());

This indicates a problem with how conditions are evaluated. Because you always provide only TableId and RecId, the evaluation logic must always make a query to database. If you need to run it once, it’s not a big deal, but if you’re evaluation many expressions, it may become a problem. To make it worse, methods for computed fields and the currency convertor also get only TableId and RecId, making additional DB queries. Caching helps a bit, but it’s still all quite expensive. I consider making a child expression class accepting a temporary buffer (or buffers) instead of just a record ID; the first prototype suggests it’s feasible and worth the effort.

If you’re interested in inner workings of the evaluation, let me give you a brief overview:

  • AX executes the query defined in the document class, filtered by RecId provided by the caller.
  • Data returned by the query (plus calculated values) are put into a XML document.
  • When you save your rule in Expression Builder, the condition is converted to an XPath query and saved to ExpressionTable.XPathQuery field. For evaluation, AX simply runs the XPath query against the XML document and returns the result.
  • XPath syntax there has a few extra functions, such as ConvertAmountValue() calling X++ class ExpressionCurrencyDefaultProvider.

If you want more details, you can look at source code, because most of it is available to you.

  • Expression Builder control is built in the same way that you can use to build your own controls. The runtime class is SysExpressionBuilderControl, its HTML code is in SysExpressionBuilderControlHTM resource, and so on.
  • A lot of logic of logic regarding saving and loading expressions is directly on ExpressionTable table. saveExpression() is a good example.
  • The evaluation is done mainly in SysExpression class (which unfortunately isn’t designed to be easily extensible).

Some logic is also contained in Microsoft.Dynamics.AX.Framework.Model.dll.

It looks useful, doesn’t it?