Code highlighting

Saturday, October 08, 2016

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

Introduction

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.

Restrictions

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.

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.

Conclusion

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?

Appendix

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);

2 comments:

  1. If you want to use extensible enums, you can find them in Dynamics.AX.Application.ExtensibleEnumValues with _Values as suffix, e.g. Dynamics.AX.Application.ExtensibleEnumValues.InventRefType_Values.ProdLine

    ReplyDelete
  2. I use a breakpoint with a condition. When I execute the condition in the Immediate Window I get the result true. If, on the other hand, the breakpoint is executed, the following error message appears: The debugger is unable to ecaluate this expression. Are your described problems from 2016 still not fixed?

    ReplyDelete

Please don't forget to leave your contact details if you expect a reply from me. Thank you