Code highlighting

Saturday, October 08, 2016

Tutorial: Visual Studio Debugger capabilities in Microsoft Dynamics AX '7', or the case-sensitive horror of C# syntax


With the move to Visual Studio with the release of Microsoft Dynamics AX 7, the debugging experience also moved to use the standard Visual Studio debugger.
That means that we get some goodies that were previously not available in the MorphX debugger.

One example of that is the Immediate Window, which allows you to write expressions that are evaluated in the context of the currently hit breakpoint in X++ code.
This basically gives you the ability to call methods, look up variable values, ultimately allowing to change the current state. That is obviously a very useful feature.
Unfortunately, the current version does not fully support X++, meaning there are certain quirks when it comes to using it.

In this post I will describe the capabilities and syntax you need to use, so you can overcome some of the learning curve that comes with the new debugger.


Here's a list of the quirks you'll have to account for:

  • There is no native support for X++, so you need to use C# syntax.
  • Intellisense for X++ is not provided. This is a consequence of the way the expression evaluator works.
  • X++ is case insensitive, while C# is not. This means that references made to identifiers need to be in the case that was used at the place of definition.
  • Not all expressions allowed in X++ are applicable. One unfortunate example is select statements. You can however use static find() methods if they exist
  • Since it's not X++, you cannot use X++ types, like str, boolean, utcdatetime. Instead, use the C# equivalents. EDTs are not preserved during compilation either, so, again, use base types. Base enums is the only exception, but, again, you need to use C# syntax
  • When invoking methods from class Global, you will need to use the full notation, Global.methodName()
  • Single quotes are used to represent characters in C#, so you should only use double quotes for representing strings
  • The Expression evaluator has no knowledge of labels, so you will need to use workarounds, like SysLabel.labelId2String("@WHS1399"), if necessary.
  • Intrinsic functions like fieldNum() are not available - you'll need to use a workaround, as I will show below, using Microsoft.Dynamics.Ax.Xpp.PredefinedFunctions
  • You may end in a situation where the types you want to use are not loaded. You can use the ReflectionCallHelper to load these types – As soon as they are loaded you will be able to use them normally. Use the following command in the immediate window to load a particular type: Microsoft.Dynamics.Ax.Xpp.ReflectionCallHelper.getType("Global")
Now, with that out of the way, let's look at some examples.


Immediate Window capabilities in Microsoft Visual Studio for Dynamics AX 7
Let's walk through these examples line my line, and I'll explain what happened in each case:
  1. worKLine - as you can see, it's not a problem for the compiler, because X++ is not case sensitive, but it is a problem for the debugger, which is. So worKLine with capital K will not be recognized, while workLine will be treated just fine. This is the reason for one of the most confusing moments with the new debugger - hovering over the worKLine variable in the code editor will not show its value, even though everything looks fine and compiles. 
  2. Even more evident is the following example, where workLine.wMSLocationId value cannot be shown when hovering over it. Nor can it be recognized as an existing field in the Immediate Window. That's because it was defined as WMSLocationId on the table. Again, casing is very important in the new debugger, so pay attention when you write code
  3. Finally, success, we use the right record variable name and the right field name - so we got our result, the value of that field in the current record.
  4. We are trying to invoke a method which resides on the Global class exactly the same way it is done in the code we are debugging, but that won't work, the method is not recognized.
  5. Now we try to invoke it using the full notation, Global::exceptionTextFallThrough(); - That does not work either, because we must use C# syntax, and :: is only X++
  6. Finally, we use the right notation, invoking Global.exceptionTextFallThrough() - that works. The method does nothing and returns no result, and we are informed about that
  7. Trying to get the value of a Base Enum using X++ notation will not work
  8. Using the "correct" C# notation will return the right result, WHSWorkStatus.Open
  9. Microsoft.Dynamics.Ax.Xpp contains a number of helpful classes to compensate for lack of full X++ support. TrueFalseHelper is one of them, and its method TrueFalse() will use the X++ logic for evaluating if an expression is true or false. We use it here and pass in the record buffer. It returns true, because the record has been selected. In real C# that would fail, as the record cannot be implicitly converted to bool, along with most other X++ types, like str, integer, etc. 
    1. Another example from this namespace is EqualHelper.Equal() which can compare two X++ types
  10. Yet another example is the PredefinedFunctions class. You can see all the available methods in the Appendix. Here we invoke the tableName2Id(), passing in the string containing WHSWorkLine. Remember 'single quotes' do not work, only "double quotes". In this case all looks good, but the function is not recognized. That's again because of the casing. This class is very inconsistent about the casing of its methods - so you just have to remember the ones you commonly use, or use the robust "trial-and-error" approach.
  11. Finally, using the right casing we get the expected result, the ID of WHSWorkLine table

I have on purpose taken the full screenshot, so you could see some of the other windows open in Visual Studio while debugging:
  • Locals window, which is similar to the Watch window, but shows the values for all local variables without you first adding them to the list. 
  • The Infolog window will show all the infolog messages, which is very convenient
  • The Callstack is pretty much the same as in X++, with the downside of showing the full types, meaning you see a lot of useless type namespace information which X++ developers are not used to
  • The Breakpoints window shows all of your breakpoints, and you can for each one decide to configure it further, disable it or remove it. You can now make the breakpoints conditional, however since it uses the same Expression evaluator, I had trouble with it, so I stopped using it after a while. The counter condition works fine though, so you can use that in various complex loops and stuff, setting the breakpoint inside the loop.
  • Autos window, which is supposed to show the current line variables plus any from the previous line is useful, because it shows the global state variables on top of that, ttslevel in particular. Company, Partition and UserId are of lower interest.
  • Watch window - that's as expected, you add variables, their values are shown and can be edited on the fly. Note all the above restrictions apply here as well, so watch the casing and syntax.


As you can see, the Visual Studio debugger is much more powerful than what we had in AX 2012 and prior, however it also has a number of limitations due to lack of support for X++ language. Note, that it's not just X++, other languages which you can use in VS also have problems here and there.

Let me know how you find the new debugger. What features do you like? Something you miss from the old days?


This appendix lists the predefined functions in the Microsoft.Dynamics.Ax.Xpp.PredefinedFunctions class. Pay special attention to the casing for the below methods.

Note. The methods starting with q deal with containers.
  • decimal Abs(decimal arg);
  • decimal AcceleratedDepreciation(decimal price, decimal scrap, decimal life, int period);
  • decimal Acos(decimal arg);
  • void AddToContainer(object element, int index, object[] container);
  • object Any2Enum(object a);
  • Guid any2guid(object input);
  • Date Anytodate(object arg);
  • decimal Asin(decimal arg);
  • object[] AssignPlusToContainer(object element, object[] container);
  • decimal Atan(decimal arg);
  • void Beep();
  • void catchUCDK(int ttsCount);
  • IDisposable changecompany(string newCompany);
  • int Char2Num(string text, int position);
  • int classget(object value, int classIdByType);
  • string ClassId2Name(int classId);
  • int classidget(XppObjectBase obj, int objId);
  • int ClassName2Id(string className);
  • int CompareStrings(string l, string r);
  • int ConfigurationKeyNum(string configurationKey);
  • object ContainerPack(object element);
  • object ContainerUnpack(object element);
  • decimal ContributionRatio(decimal sale, decimal purchase);
  • decimal corrflagset(decimal real, int arg);
  • decimal Cos(decimal arg);
  • decimal Cosh(decimal arg);
  • string curext();
  • string curusrid();
  • int Date2Num(Date date);
  • string Date2Str(Date date, int sequence, int day, int separator1, int month, int separator2, int year);
  • string Date2StrConvert(Date date, int sequence, int day, int separator1, int month, int separator2, int year, int convert_to_calendar);
  • string Datetime2Str(utcdatetime d, int f);
  • string DayName(int number);
  • int Dayofmth(Date d);
  • int DayOfWeek(Date arg);
  • int Dayofyr(Date d);
  • decimal Decround(decimal figure, int decimals);
  • object DefaultValue(Types t);
  • string dellspc(string text);
  • IntPtr delprefix(IntPtr value);
  • string delrspc(string text);
  • string delstr(string text, int position, int number);
  • decimal Depreciation(decimal price, decimal scrap, decimal life, int period);
  • int Dimof(object o);
  • Date EndMonth(Date arg);
  • string Enum2Str(object e);
  • string EnumExtension2Str(object value, string enumTypeName);
  • int Enumname2id(string enumName);
  • int EnumSymbol2EnumValue(string enumName, string enumValueName);
  • string EnumTypeToString(Types type);
  • decimal Exp(decimal arg);
  • decimal Exp10(decimal arg);
  • string Fieldid2name(int tableId, int field, int arrayIndex);
  • string Fieldid2pname(int tableId, int field, int arrayIndex);
  • int Fieldname2id(int tableId, string fieldName);
  • void FillArray(int size, T value, Dictionary array, T zeroValue);
  • void FillEdtArray(int size, T value, EdtArray array);
  • string FldPNam(int dataset, int fieldnum);
  • void Flush(int dataset);
  • decimal formattedstr2num(string text);
  • decimal Frac(decimal arg);
  • decimal FutureValue(decimal Payment, decimal Interest, decimal Life);
  • string getbuildversion();
  • string getcurrentauthor();
  • string getcurrentbranchname();
  • string getcurrentcustomerid([Optional, DefaultParameterValue(0)] int dbFlag);
  • string getcurrentdevicename();
  • string getcurrentipaddress();
  • string getcurrentmachinename();
  • long getcurrentpartitionrecid();
  • Guid getcurrentrequestid();
  • string getcurrentruntimemessage();
  • string getcurrentserviceunitid();
  • string getcurrentserviceunittype();
  • string getcurrentsessionid();
  • string getcurrenttenant();
  • string getcurrentuserid();
  • string getcurrentuserlanguage();
  • Dictionary GetFieldQCollection();
  • T GetFromArray(int position, Dictionary array, T zeroValue);
  • Date GetNullDate();
  • utcdatetime GetNullDateTime();
  • string GetNullString();
  • string getprefix();
  • void GroupQ(Dictionary collection);
  • string Guid2Str(Guid value);
  • decimal Idg(decimal purchase, decimal contribution_ratio);
  • string image(object o);
  • string Indexid2name(int tableId, int index);
  • int Indexname2id(int tableId, string indexName);
  • string insstr(string text1, string text2, int position);
  • string int2str(int param);
  • string int642str(long param);
  • int IntervalMax(DateTime inputDate, DateTime refDate, int func);
  • string IntervalName(DateTime refDate, int col, int func);
  • int IntervalNo(DateTime inputDate, DateTime refDate, int func);
  • DateTime IntervalNorm(DateTime inputDate, DateTime refDate, int func);
  • int intvmax(Date input_date, Date ref_date, int func);
  • string intvname(Date d, int col, int func);
  • int intvno(Date input_date, Date ref_date, int func);
  • Date intvnorm(Date input_date, Date ref_date, int func);
  • bool IsNonEmpty(string s);
  • int LicenseCodeNum(string licenseCode);
  • bool Like(string arg1, string arg2);
  • decimal Log10(decimal arg);
  • decimal Logn(decimal arg);
  • string LookupLabel(string pattern);
  • int Match(string pattern, string text);
  • object Max(object[] args);
  • object Min(object[] args);
  • Date Mkdate(int day, int month, int year);
  • string MonthName(int number);
  • int Mthofyr(Date d);
  • Date NextMonth(Date arg);
  • Date NextQuarter(Date arg);
  • int nextTraceSequence();
  • Date NextYear(Date arg);
  • bool NullDate(Date d);
  • bool NullDateTime(utcdatetime d);
  • bool NullGuid(Guid g);
  • string Num2char(int figure);
  • Date Num2Date(int days);
  • string Num2Str(decimal number, int character, int decimals, int separator1, int separator2);
  • string ObjectToString(object o);
  • void OrderQ(Dictionary collection);
  • decimal PercentAdd(decimal amount, decimal percentage);
  • decimal Periods(decimal payment, decimal interest, decimal future_value);
  • decimal PeriodsRequired(decimal Interest, decimal FutValue, decimal PresValue);
  • decimal Power(decimal arg, decimal exponent);
  • decimal PresentValue(decimal Paym, decimal Interest, decimal Life);
  • Date PreviousMonth(Date arg);
  • Date PreviousQuarter(Date arg);
  • Date PreviousYear(Date arg);
  • decimal PricePerPeriod(decimal principal, decimal interest, decimal life);
  • object[] qdel(object c, int position, int numElements);
  • int qfind(object c, object[] parameters);
  • object[] qins(object c, int position, object[] parameters);
  • int qlen(object[] container);
  • object qpeek(object c, int position);
  • object[] qpoke(object c, int position, object[] parameters);
  • decimal Rate(decimal future_value, decimal current_value, decimal terms);
  • string remove(string text1, string text2);
  • decimal Round(decimal dbl0, decimal dbl1);
  • void SecAuthzCheck(string className, string methodName);
  • int sessionid();
  • void SetInArray(int position, T value, Dictionary array, T zeroValue);
  • int setprefix(string prefix, ref IntPtr ptr);
  • decimal Sin(decimal arg);
  • decimal Sinh(decimal arg);
  • int Sleep(int duration);
  • decimal Sln(decimal cost, decimal salvage, decimal life);
  • int Sound(int frequency, int duration);
  • Date Str2Date(string text, int sequence);
  • utcdatetime Str2Datetime(string text, int sequence);
  • object Str2Enum(object e, string s);
  • object Str2EnumExtension(object dummyParm, string valueName, string enumTypeName);
  • Guid str2guid(string input);
  • int str2int(string text);
  • long str2int64(string text);
  • decimal Str2Num(string text);
  • int str2time(string text);
  • string StrAlpha(string text);
  • string strcolseq(string text);
  • int StrFind(string text, string characters, int position, int number);
  • string strfmt(string text, object[] parameters);
  • Types StringToType(string t);
  • string StrKeep(string text1, string text2);
  • int Strlen(string text);
  • string StrLine(string s, int count);
  • string Strlwr(string text);
  • int StrNFind(string text, string characters, int position, int number);
  • string StrPoke(string arg1, string arg2, int position);
  • string StrPrompt(string _string, int _len);
  • string StrRep(string text, int number);
  • int StrScan(string text1, string text2, int position, int number);
  • string Strupr(string text);
  • string Substr(string text, int position, int number);
  • Date systemdateget();
  • Date systemdateset(Date d);
  • string Tableid2name(int tableId);
  • string Tableid2pname(int tableId);
  • int Tablename2id(string table);
  • string TabPNam(int dataset);
  • decimal Tan(decimal arg);
  • decimal Tanh(decimal arg);
  • string Time2str(int time, int separator1, int separator2);
  • int Timenow();
  • Date Today();
  • decimal Trunc(decimal arg);
  • void truncate_infolog();
  • int TryStart(ref IntPtr ptr);
  • void ttsabort();
  • void ttsbegin();
  • void ttscommit();
  • int ttscount();
  • int Typename2id(string typeName);
  • Types Typeof(object o);
  • string uint2str(int param);
  • IDisposable Unchecked(int uncheckValue, string className, string methodName);
  • int WeekOfYear(Date arg);
  • void Where(exprNode node, Common table);
  • int Year(Date d);

Thursday, October 06, 2016

Development tool: Copy 'Find references' results to clipboard (to Excel) in Visual Studio for AX 7


With the release of Microsoft Dynamics AX 7 the development moved to the Visual Studio environment.

This had a lot of advantages, like the ability to use any 3rd party add-ons of various sort (which we actually have not seen that many of being applied to AX so far), all the VS goodies that come out of the box, a more familiar IDE for new developers, etc.

But it also has its disadvantages, like the Cross-References display window.

  • First of all, the indicator of whether a particular reference is writing or reading is gone. 
  • And we now display the xRefs using the standard Visual Studio "Find Symbols Results" window, which has one huge drawback as well - no way to filter on the data displayed, or copy that data somewhere else to do that.


I was bothered by this lack of functionality for a while, so I went out to find if there's an existing solution. I came across this post, which seems like a sufficient solution for the problem, in my opinion. Kudos to the author!

I have modified the project a bit to better suite AX needs, as I planned to browse the data in Excel, which has rich filtering capabilities and more convenience in navigation, and uploaded it to GitHub so anyone can use and extend it. You can also just download the executable, if are OK with the out-of-the-box functionality I will describe below.

Project on GitHub:

Executable on my OneDrive:

Installation guide

Once you have the executable, place it in a folder on your environment running Visual Studio for AX, say, C:\Tools or whatever you prefer.

Now, from Visual Studio, go to Tools and select External tools..., as shown below:

External Tools... under Tools menu in Visual Studio

Now add a new tool by clicking Add, and specify the Title, Command and Initial directory.
The Command  should contain the path to the CopyFindReferencesToClipboard executable

Add the CopyFindReferencesToClipboard tool

User guide

Using the tool is very simple. Say I wanted to find all references to the WHSLoadLine.Qty field.
I would navigate to that field and select Find references from the context menu. 

Find references to the WHSLoadLine.Qty field
This would bring up the standard Visual Studio dockable window Find Symbol Results, containing all the references to the selected table field.

Now, all you need to do is go to Tools and select the newly added tool from the list, as shown below:

Run the tool to copy the references to clipboard
After a few seconds you will get a message box to pop up telling you the references have been copied to clipboard successfully, which means you can now to and paste the data to Microsoft Excel.

Note. Since the tool uses UI-level automation to copy the cross-references, the Find references window needs to be open and visible for the tool to work.

Now, you can do whatever you want with that data in Excel.
The way I typically use it is by just showing the data as a Table, after which:
- Exclude test related files (Actual tests and Test frameworks we used)
- Filter out only elements in a certain area, like WHS
- Build pivot tables/charts, if I am doing complexity analysis for a change / feature
- etc.

Here's how it looks:

Analyze the cross-references in Microsoft Excel


Give it a try and let me know what you think!

Sunday, October 02, 2016

Tutorial: Extending the label printing functionality in Microsoft Dynamics AX 7

Today I would like to shed a bit more light onto how to extend the label printing functionality, specifically, show how to add a new field from the work line to be displayed on the label.

This is something we have created as a demo for Tech Conf a while back, and you can still see the recording where Per and Zach from our team showcases the different label printing scenarios.
You can view the different Microsoft conference videos here.

Just as a refresher, here is a guide for how to set up label printing in Dynamics AX 7:

Technical introduction

The label printing "framework" consists of three main components:

  • WHSLicensePlateLabel table, which contains the information for the label, which is substituted into the label through the variables.
  • WHSLicensePlateLabelBuild class, which is responsible for populating the WHSLicensePlateLabel table with data for a particular work order (line).
  • WHSDocumentRouting class, which is responsible for the actual printing of the label, as well as the substitution of variable values, using the below methods:
    • initMenuFields() method is responsible for building the list of substitute fields based on the WHSLicensePlateLabel table fields
    • printDocument() method finds the specific document routing record that matches the flow criteria (e.g., warehouse, work order type, carrier, etc.), performs the translation of the label and sends it to the selected printer (Note that the label can be printed to multiple printers and you can for each printer decide, which layout to use).
    • translate() method is responsible for actually replacing any variables in the label layout with the corresponding values from the WHSLicensePlateLabel table
Let's take a closer look at the fields available on the WHSLicensePlateLabel table:

WHSLicensePlateLabel table fields
These fields represent the full list of potential variables that can be put into the label in the Document Routing layout. Depending on the item flow, some of these fields are not populated. Note that when setting up the placeholders in the Document Routing Layout form, only visible fields are displayed. That allows for storing some "plumbing" information for each record, like the PrinterSettings.

Adding placeholder variables to a label layout

Fragile Example extension

For some scenarios you might want to display some additional fields on the label, which are not part of the list shown above.

Imagine a scenario where we want to display a "Fragile" message on the label if the items on the work line are accordingly marked. 

Since there is no such field in the label, we will need to extend this functionality. And because we are working in AX 7, we will try to use Extension instead of Overlayering the elements, as we would do in AX 2012 R3.

Step 0 - Create a new model for our changes

First of all, let's create a new model, which will contain all the elements we add through extension.

Create a new model in Visual Studio
We will put this model into a new package, as we do not intent to overlayer existing AOT elements. We will reference the Application Suite package, so that we can access the elements defined in it, such as the ones mentioned above. 
Note that by default the new package will also reference Application Platform.

Select referenced packages for model FragileExample
Confirm the creation of the new model and create a new project for it. Name it FragileExample.

Step 1 - Extend the WHSLicensePlateLabel table and add a new field FragileCode

Since we want to display an extra field in the label, we need to add it to the WHSLicensePlateLabel table. In order to do that, let's create an extension of this table in the new model:

Adding a new table extension from Application Explorer
You need to have selected the right project in your Solution Explorer before you attempt creating an extension. This is to ensure that the extension is actually created in the right model.

We are now going to add a new field of base type String, and call it FragileCode with the corresponding label.

Note I am not going to bother with labels or Extended Data types for sake of simplifying the demo

Table extension for WHSLicensePlateLabel in FragileExample model
New field FragileCode on table extension WHSLicensePlateLabel.Extension
In order for the field to appear in the Document routing layouts form all we need to do is compile the new code and synchronize it against the database. I have decided to do that at build time by configuring it accordingly, as shown below:

Synchronize on build = true for FragileExample project
If we now reopen the Document routing layouts form, we can see that our new placeholder is available in the list and we can add it to the label:

Fragile code placeholder available in Document routing layouts form

Step 2 - Populating the Fragile code field on WHSLicensePlateLabel record

Now that we have the field available on the label, we need to populate it. 
As described in the Technical information section above, the label details are populated in the class WHSLicensePlateLabelBuild. Let's quickly examine how this happens:

Sequence of calls to build a license plate label
As you all know, any and all work creation and execution in Dynamics AX happens through the WHSWorkExecuteDisplay* classes, e.g., purchase order registration can execute WHSWorkExecuteDisplayPOItemReceiving, while general work execution flows can execute WHSWorkExecuteDisplayUserDirected, etc.
Within these classes the WHSLicensePlateLabelBuild class is initialized, and the buildLicensePlateLabels() public method is called, when a label needs to be generated and printed.
Depending on the flow, either the insertSingleLabelPrintLine or the insertSingleLabelMenuItem method will be invoked internally, after which the generated label is printed through the printDocument API of the WHSDocumentRouting class, as described above.

Note. insertSingleLabelPrintLine supports the flow where the Print step is part of the work template lines, while insertSingleLabelMenuItem is for the case, where the Print is configured through the mobile device menu item.

So, in order for us to populate the extra new field we need to look into these insert* methods. With the recent AX 2012 R3 changes these methods have been refactored in a way, where most of the logic for populating the WHSLicensePlateLabel table has been pulled out into a new private initLabel() method.

In Dynamics AX 7 this method has been extended (who could have done that? :)) and now has a delegate which is invoked at the end of the method, which means that event handlers can be created for it. With these minor changes, we can now extend this class by subscribing to the event of the initLabel method being invoked, as shown below:

Copy event handler code for labelInitialized delegate
We can now put this generated event handler signature into a new class in our model, as shown below:

Signature of the event handling method for labelInitialized delegate
This is awesome and is one of the new extensibility language features in Dynamics AX 7 - I have subscribed to the event of this method being called without touching the original class at all.
As you can see, instead I have a SubscribesTo attribute that subscribes me to the corresponding event in a static fashion. 

Now all we need to do is actually populate the Fragile code on the label. Let's do that:

Implementation for the event handling method for labelInitialized delegate
As you see above, the code is super simple, all we do is get the Filter code value from the corresponding item, and populate it into the FragileCode field.

Let's now compile everything and give it a go in AX web client.

You can download the VS project file from my OneDrive here.

AX flow steps and result

  1. Modify item A0001, setting Filter Code 4 to a new value called FRAGILE
  2. Create a new purchase order for 10 pcs of A0001 to WH 24
  3. Modify the Mobile device menu item for Purchase receipt to "Print label"
  4. Ensure you have a Document routing record matching WH 24 for Purchase orders
    1. Ensure you have a document routing layout being sent to a label printer
  5. Receive the above purchase order through the WMDP
As a result, you should get a new label created, which will have the Fragile code populated with the value FRAGILE, as below (I've personalized the form to show the new field):

License plate label with the populated Fragile code

Depending on your label layout you would have this message printed on the label as well.


With this blog post I hoped to show you how to extend the existing set of fields available on the license plate labels.

At the same time we took a look at some of the new language constructs, design paradigms and tooling that allows for a much cleaner approach to extending existing functionality, as compared to overlayering, which many are used to.

Consider extending instead of overlayering next time you need to make a change!

Saturday, October 01, 2016

Tutorial: Label printing in Microsoft Dynamics AX 7

A while back I posted a tutorial on how to configure and use the label printing functionality in Microsoft Dynamics AX 2012 R3:

With the release of Microsoft Dynamics AX 7 and the move to the Cloud, printing has become a bit more complicated than before, since the printers are not on the same domain / network as it used to be when everything was installed on-premise.

The major change that I will talk about in this post is the way we set up printers now.

Installing & configuring the Document Routing Agent

The wiki article describing the installation and configuration of the Document routing agent is very well written and contains a lot of details about the restrictions and requirements for this to work, so I will not repeat it here. Here is the link:

For example, one of the unexpected requirements is that Adobe Acrobat Reader is installed.

Note, that with a recent update of the AX platform, the Document Routing Agent can now be run in the background as a Windows service, which adds a number of benefits. Read more about that in the below wiki article:

Now that the document routing agent application / service is installed and can sign in to AX, you can select the printers you want to expose, activate them in the Network printers form, as described in the above wiki article.

After that, they will show up in the Printer name lookup on the Document Routing form as before.

Everything else is pretty much the same from a user standpoint, nothing changed in terms of document routing configuration or the WMDP configuration.

So when a label needs to be printed, here's what happens now:
  1. The label is generated in the ZPL code, as before, containing all the replaced variables from the document routing layout
  2. It is then saved in a file in the Azure Blob Storage (since the AOS does not have access to the client machine, we cannot just go and do something with it) in a pre-configured folder, including all the relevant settings, specifically, which printer to use for this label
  3. The Document Routing Agent application / service on the network printer server or just as a local application one of the network PCs will periodically query if there are any pending files to be printed, downloads them from the Azure Blob storage and, depending on the printer settings defined for the specific file, re-routes the file to be printed, whether that is to a Zebra printer, or, in the case of regular SSRS reports, to a PDF document, or a regular printer.

Note that before Platform Update 2 there was an unpleasant bug in this framework, which prevented printing of labels from the Warehouse Mobile Devices Portal. That has since been fixed, and you should be able to print labels without any problems (Workaround for people on earlier installations is to use the WMDP enumlator form from AX web client)

Give it a try and report back here in case you find some of the instructions unclear, or if something is not working according to your expectations.