On the AXSCDPPKG file structure

On the AXSCDPPKG file structure

Application hotfixes for Dynamics 365 for Finance and Operations are distributed via AXSCDPPKG (AX Source Code Deployable Package) files. They can be downloaded from the LCS Issue Search page. The deployment process with the help of the SCDPBundleInstall.exe utility is described in this working instruction.

However, prior to the installation you may want to quickly review the code to check if this is what you are looking for. The LCS Metadata hotfix is nothing but a set of 3+ nested ZIP files, quickly recognizable by the “PK” magic bytes in the header:

 

  1. Unpack the MicrosoftDynamicsAX_KBxxxxxxx.zip file as downloaded from the LCS.
  2. Rename the HotfixPackageBundle.axscdppkg file into a ZIP, unpack it. There are at least 2 files inside: one or many (for cumulative hotfixes) nested DynamicsAX_xxxxxxx.axscdp files, and a diagram with their dependencies PackageDependencies.dgml. The dependencies are well described by Mr. de Cavalon in his blog.
  3. Rename, unpack the nested DynamicsAX_xxxxxxx.axscdp. There is a set of application folders (Deltas) with a list of all affected application objects in PackageManifest.xml.
  4. Traverse the Deltas folder to the bottom. There is an XML file like this with the source code before the update (<Original> … </Original>) and after (<Yours> … </Yours>) in a human readable form, as maintained by the TFS source code versioning system.

Update 25.01.2019
A reader pointed to a recent article about the same topic:
https://community.dynamics.com/365/financeandoperations/b/axsupport/archive/2018/10/19/inspecting-a-d365fo-meta-data-hotfix-content, or Inspecting-a-D365FO-Meta-Data-Hotfix-Content.docx

X++ Decorator pattern in Dynamics 365

X++ Decorator pattern in Dynamics 365

A couple of times in my career I stumbled upon a development requirement to perform a set of loosely connected actions with a similar interface upon a certain business object depending on parameters or conditions:

  • Sometimes do this
  • Sometimes do that
  • Sometimes do this and that
  • In future, you may do something else in addition.

In Dynamics 365 for Finance and Operations, this usually results in a hierarchy of classes. The classes are typically bound to some parameter table containing checkboxes or – better – an enumeration field. This enumeration directs the SysExtension class factory which objects to instantiate.
A simple while_select loop traverses the parameter table and executes the instantiated objects one by one. However, if the classes implement more than one action i.e. method, this external loop may become repetitive. I ended up with a certain design pattern which turned out to have a name in object-oriented programming: a Decorator an internal iterator.
Errata: as readers pointed out, there is more of a Decorator pattern in this, than an Iterator pattern.

The constructor instantiates an object and gives it the previous object as a parameter, thus forming a linked chain of objects. The external actor only needs to take one parameter – the topmost object in the chain – and to call one operation .doThis() upon it. This .doThis() method is exclusively implemented in the parent class of the hierarchy, and the parent class knows how to iteratively call the objects.

Adding a new grid with a parameter table to an existing form is more difficult than adding new field(s) into an existing table via extensions. If the number of possible actions is countable and small, you may think of de-normalizing the parameter table and making an array field with possible options in one record. Instead of iterating records, the constructor will be iterating fields.

Below is an application of this pattern to a simple task: calculation of the nutrition value of one meal, where a meal may consist of an Entrée, an Entrée and a Main course, a Main course and a Dessert and so on:

 

				
					class MealCourseAtrribute extends SysAttribute implements SysExtensionIAttribute
{
    MealCourseEnum mealCourseEnum;
    public void new(MealCourseEnum _mealCourseEnum)
    {
        super();
        mealCourseEnum = _mealCourseEnum;
    }
    public str parmCacheKey()
    {
        return classStr(MealCourseAtrribute) + enum2Symbol(enumNum(MealCourseEnum), mealCourseEnum);
    }
    public boolean useSingleton()
    {
        return true;
    }
}

abstract public class MealCourse
{
    private MealCourse prevMealCourse;
    abstract protected MealKcal kcal()
    {
    }
    final public MealKcal kcalTotal()
    {
        MealKcal ret = this.kcal();
        if (prevMealCourse)
        {
            ret += prevMealCourse.kcalTotal();
        }
    return ret;
    }
    private MealCourse prevMealCourse(MealCourse _prevMealCourse = prevMealCourse)
    {
        prevMealCourse = _prevMealCourse;
        return prevMealCourse;
    }
    protected void new()
    {
    }
    public static MealCourse construct(Meal _meal)
    {
        MealCourse mealCourse, prevMealCourse;
        MealCourseAtrribute attr;
        MealCourseEnum mealCourseEnum;
        int i;
        for (i = 1; i <= dimOf(_meal); i++)
        {
            mealCourseEnum = _meal[i];
            attr = new MealCourseAtrribute(mealCourseEnum);
            mealCourse = SysExtensionAppClassFactory::getClassFromSysAttribute(classStr(MealCourse), attr);
            if (prevMealCourse)
            {
                if (mealCourseEnum == MealCourseEnum::None)
                {
                    continue;
                }
                mealCourse.prevMealCourse(prevMealCourse);
            }
            prevMealCourse = mealCourse;
        }
        return prevMealCourse;
    }
    public static void main(Args _args)
    {
        info(strFmt("Total calories in this meal is %1", MealParameters::find().meal().kcalTotal()));
    }
}

[MealCourseAtrribute(MealCourseEnum::Entree)]
public class MealCourseEntree extends MealCourse
{
    public MealKcal kcal()
    {
        return 140;
    }
}

[MealCourseAtrribute(MealCourseEnum::Main)]
public class MealCourseMain extends MealCourse
{
    public MealKcal kcal()
    {
        return 600;
    }
}

[MealCourseAtrribute(MealCourseEnum::Dessert)]
public class MealCourseDessert extends MealCourse
{
    public MealKcal kcal()
    {
        return 200;
    }
}
				
			

Dynamics 365 FO may return no records for a certain table, but it cannot return an NULL value for a field; in addition, I would like to spare the caller an if (! parameter) {return}; validation. This is why the caller is always given at least one object of the MainCourseNone type which returns a zero and does nothing. The API then reduces to a one-liner:
… = MealParameters::find().meal().kcalTotal();
here for a main course and a dessert:
Decorator pattern: Nutrition value calculation

Adding a new meal course type e.g. a soup is as simple as adding a new enumeration element to the MealCourseEnum, adding a new array element to the Meal extended data type (but only if the Soup may be ordered in addition to the other courses), and implementing a class tagged with this enumeration element as an attribute:

				
					[MealCourseAtrribute(MealCourseEnum::Soup)]
public class MealCourseSoup extends MealCourse
{
    public MealKcal kcal()
    {
        return 100;
    }
}
				
			

Beware of the current limitations in the array fields support in D365FO: they cannot be exposed in Excel or imported properly in the Data management module.
You may download the source code here: TheMealProject

Copy-paste automation in D365 FO with a keyboard emulator

Copy-paste automation in D365 FO with a keyboard emulator

The Microsoft Dynamics Product Group in their infinite wisdom first deprecated in D365 FO the import into an arbitrary table (because entities can do much more, and they became the First Class Citizens in the development environment), then the table browser in Production (because Microsoft Support Engineers were complaining the partners did not behave).

However, the ~2600 First Class Citizens are still too few in front of the common crowd of 9000+ application tables, and Microsoft Support Engineers themselves never execute SQL scripts against the database. In the absence of other mass update tools in Dynamics 365 FO, the consultant’s last resort became the clipboard: <Ctrl-C> <Tab> <Ctrl-V> <Ctrl-C> <Tab> <Ctrl-V> <Ctrl-C> <Tab> <Ctrl-V> <Ctrl-C> <Tab> <Ctrl-V> <Ctrl-C> <Tab> <Ctrl-V> <Ctrl-C> <Tab> <Ctrl-V> <Ctrl-C> <Tab> <Ctrl-V>… This activity is well paid like any other consulting routine, but it is as entertaining as watching the grass grow. Most importantly, it is error prone.

Desperate times call for desperate measures. Many large partners out there have already deployed their table browser forms and SQL injection tools, but in simple cases a keyboard emulator may help. I used the program called AutoHotkey with its rich scripting language.

The below scripts are started by this program with the hotkey <Ctrl-J>. They send key combinations into the browser window to perform repetitive actions. The <F1> key is your panic button to stop a script running amok. The D365 browser UI is not instantly responsive: allow for 10-100 milliseconds to enter data across different fields in one record, and 1000-1500 milliseconds on database operations of saving, deleting, or opening/closing a form. This is achieved by the “Sleep(x)” command. Even the tabulator key and <Alt-Something> combinations require some time for the JavaScript presentation layer to respond. The scripts simulate the key pressed, give some time to D365 to respond, and depress the key.

Watch it in action:

 

Mass-deleting lines

‘Simple list’ forms in D365 FO often offer no multi select capability, and with the table browser locked, mass deletion of lines becomes a sweaty exercise for the consultant. The following script calls <Alt-Del> and confirms ‘yes, delete’ one hundred times in a loop:
; ---------Press Ctrl-J to trigger
^j::
Loop 100 {
Sleep, 500
Send, {alt down}
Sleep, 100
Send, {Del}
Sleep, 100
send, {alt up}
Sleep, 500
Send, {TAB down}{TAB up}
Send, {TAB down}{TAB up}
Send, {Space}
Sleep, 500
}
Return
; Press F1 to stop
F1::ExitApp

Copy language translations

Many master tables come with translations into the customer/supplier language: terms of payment, terms and modes of delivery, miscellaneous charges etc. Entities such as the ‘Unit translation’ only exist for a few of them. Here is the rescue: import the language text with the entity into an existing field (e.g. Description), then copy and paste it into the Translation form. The below script copies 200 payment term descriptions into the ‘en-us’ language:
^j::
Loop 200 {
Sleep, 100
; ---------Copy the content of the Description field
Send, ^a
Sleep, 200
Loop 20{
Send, {Shift Down}{Right}{Shift Up}
Sleep, 10
}
Send, ^c
Sleep, 100
; --------- Open the ribbon, 'click' Translations
Send, !m
Sleep, 100
Send, a
Sleep, 100
Send, {TAB down}{TAB up}
Send, {TAB down}{TAB up}
Send, {TAB down}{TAB up}
Send, {Space}
Sleep, 1500
; ---------Create a new record
Send, {Alt Down}
Sleep, 100
Send, n
Sleep, 100
Send, {Alt Up}
Sleep, 1000
Send, en{-}us
; ---------Paste the content, save the record
Send, {TAB down}{TAB up}
Send, {TAB down}{TAB up}
Sleep, 200
Send, ^v
Sleep, 200
Send, ^s
Sleep, 200
; ---------Close the Translation form
Send, {Escape Up}
Sleep, 200
Send, {Escape Down}
Sleep, 500
; ---------Scroll down to the next record, and repeat the cycle
Send, {LControl Down}{Down}{LControl Up}
}
Return
; Press F1 in panic
F1::ExitApp