Tuesday 1 March 2016

Event handlers and Pre/Post methods AX7

In legacy X++, it was possible to prescribe in metadata that certain methods were to be executed prior to and after the execution of a method. The information about what subscribes call was recorded on the publisher, which isn’t useful in the Dynamics AX environment. It’s now possible to provide Pre and Post handlers through code, by providing the SubscribesTo attribute on the subscribers.

Example

[PreHandlerFor(classStr(MyClass2), methodstr(MyClass2, publisher))]
public static void PreHandler(XppPrePostArgs arguments)
{
int arg = arguments.getArg("i");
}

[PostHandlerFor(classStr(MyClass2), methodstr(MyClass2, publisher))]
public static void PostHandler(XppPrePostArgs arguments)
{
int arg = arguments.getArg("i");
int retvalFromMethod = arguments.getReturnValue();
}

public int Publisher(int i)
{
return 1;
}
This example shows a publishing method called Publisher. Two subscribers are enlisted with the PreHandlerFor and PostHandlerFor. The code shows how to access the variables, and the return values.

Note: This feature is provided for backward compatibility and, because the application code doesn’t have many delegates, to publish important application events. Pre and Post handlers can easily break as the result of added or removed parameters, changed parameter types, or because methods are no longer called, or called under different circumstances.
Attributes are also used for binding event handlers to delegates:

  [SubscribesTo(
classstr(FMRentalCheckoutProcessor),
delegatestr(FMRentalCheckoutProcessor, RentalTransactionAboutTobeFinalizedEvent))]
public static void RentalFinalizedEventHandler(
FMRental rentalrecord, Struct rentalConfirmation)
{
}

delegate void RentalTransactionAboutTobeFinalizedEvent(
FMRental fmrentalrecord, struct RentalConfirmation)
{
}
In this case, the SubscribesTo attribute specifies that the method RentalFinalizedEventHandler should be called when the FmRentalCheckoutProcessor.RentalTransactionAboutToBeFinalizedEvent delegate is called.
Since the binding between the publisher and subscribers is done through attributes, there’s no way of specifying the sequence in which subscribers are called.

Basic Development concept AX7

Before we talk AX7 Development, we need to have look at few terminologies, which are as under

Element: Element is any Object residing in AOT. E.g. any Base Enumeration, any EDT, any Table, any Form, which you see in AOT Tree, is an element itself.

Model: A Model is a group of elements. A model is collection of elements that represent a distributable software solution.

Packages: A package is deployable unit, which may have multiple Models.

Project: Projects are the logical containers for everything that's needed to build your application. A project can be associated with only one model.

After we know basic terminologies, which will be used very frequently, let’s start with ways of developments AX7 provides.

With arrival of AX7, now we can do development in AX, with two methodologies.

  1. OverLayring
    If you have been developer of previous versions of AX such as AX2012 or AX2009, you don’t need explanation of this way of development. Name of this methodology ‘OverLayring’ is self-explanatory.
    In Microsoft Dynamics AX, a layer system is used to manage elements. The USR layer is the top layer and the SYS layer is the bottom layer, and each layer has a corresponding patch layer above it.
    OverLayring means if you do change/customize any element in upper layer, it will override its definition, in lower layer.
    This is what we were doing till AX2012 to customize AX. It basically allows customization of metadata and source code in higher layer. For example, you can add a new field in CustTrans Table in the user layer or in ISV Layer.
  2. Extension
    It refers to concept of extending existing elements, without overriding those into other layers. That means, elements will not be customized, instead elements will be extended. This approach reduces upgrade cost and conflicts. By creating extension elements, all of the customizations and code are stored in a separate assembly. It only contains the code that was added or changed in the extension file. Since this code resides in a separate assembly, it improves performance and the ability to upgrade and test.

Basic Development concept AX7

Before we talk AX7 Development, we need to have look at few terminologies, which are as under

Element: Element is any Object residing in AOT. E.g. any Base Enumeration, any EDT, any Table, any Form, which you see in AOT Tree, is an element itself.

Model: A Model is a group of elements. A model is collection of elements that represent a distributable software solution.

Packages: A package is deployable unit, which may have multiple Models.

Project: Projects are the logical containers for everything that's needed to build your application. A project can be associated with only one model.

After we know basic terminologies, which will be used very frequently, let’s start with ways of developments AX7 provides.

With arrival of AX7, now we can do development in AX, with two methodologies.

  1. OverLayring
    If you have been developer of previous versions of AX such as AX2012 or AX2009, you don’t need explanation of this way of development. Name of this methodology ‘OverLayring’ is self-explanatory.
    In Microsoft Dynamics AX, a layer system is used to manage elements. The USR layer is the top layer and the SYS layer is the bottom layer, and each layer has a corresponding patch layer above it.
    OverLayring means if you do change/customize any element in upper layer, it will override its definition, in lower layer.
    This is what we were doing till AX2012 to customize AX. It basically allows customization of metadata and source code in higher layer. For example, you can add a new field in CustTrans Table in the user layer or in ISV Layer.
  2. Extension
    It refers to concept of extending existing elements, without overriding those into other layers. That means, elements will not be customized, instead elements will be extended. This approach reduces upgrade cost and conflicts. By creating extension elements, all of the customizations and code are stored in a separate assembly. It only contains the code that was added or changed in the extension file. Since this code resides in a separate assembly, it improves performance and the ability to upgrade and test.

Event handlers and Pre/Post methods AX7

In legacy X++, it was possible to prescribe in metadata that certain methods were to be executed prior to and after the execution of a method. The information about what subscribes call was recorded on the publisher, which isn’t useful in the Dynamics AX environment. It’s now possible to provide Pre and Post handlers through code, by providing the SubscribesTo attribute on the subscribers.

Example

[PreHandlerFor(classStr(MyClass2), methodstr(MyClass2, publisher))]
  public static void PreHandler(XppPrePostArgs arguments)
  {
    int arg = arguments.getArg("i");
  }

  [PostHandlerFor(classStr(MyClass2), methodstr(MyClass2, publisher))]
  public static void PostHandler(XppPrePostArgs arguments)
  {
    int arg = arguments.getArg("i");
    int retvalFromMethod = arguments.getReturnValue();
  }

  public int Publisher(int i)
  {
    return 1;
  }
This example shows a publishing method called Publisher. Two subscribers are enlisted with the PreHandlerFor and PostHandlerFor. The code shows how to access the variables, and the return values.

Note: This feature is provided for backward compatibility and, because the application code doesn’t have many delegates, to publish important application events. Pre and Post handlers can easily break as the result of added or removed parameters, changed parameter types, or because methods are no longer called, or called under different circumstances.
Attributes are also used for binding event handlers to delegates:

  [SubscribesTo(
    classstr(FMRentalCheckoutProcessor),  
    delegatestr(FMRentalCheckoutProcessor, RentalTransactionAboutTobeFinalizedEvent))]
  public static void RentalFinalizedEventHandler(
    FMRental rentalrecord, Struct rentalConfirmation)
  {
  }

  delegate void RentalTransactionAboutTobeFinalizedEvent(
    FMRental fmrentalrecord, struct RentalConfirmation)
  {
  }
In this case, the SubscribesTo attribute specifies that the method RentalFinalizedEventHandler should be called when the FmRentalCheckoutProcessor.RentalTransactionAboutToBeFinalizedEvent delegate is called.
Since the binding between the publisher and subscribers is done through attributes, there’s no way of specifying the sequence in which subscribers are called.

Using clauses AX7

Previously, all references to managed artifacts that weren’t authored in X++ was done using fully qualified names, including the namespace for each type. This is still possible, but you can now provide using clauses to make the use of such artifacts less onerous. As opposed to a using statement, each using clause precedes the class in which the clause is applied.

It’s also possible to provide aliases that introduce a short name for a fully qualified name. Aliases can denote namespaces and classes as shown below.

Example

Consider the following code:
 using System;
using IONS=System.IO; // Namespace alias
using Alist=System.Collections.ArrayList; // Class alias

public class MyClass2
{
public static void Main(Args a)
{
Int32 I; // Alternative to System.Int32
Alist al; // Using a class alias

al = new Alist();
str s;

al.Add(1);

s = IONS.Path::ChangeExtension(@"c:\tmp\test.xml", ".txt");
}
}

Extension methods AX7

The extension method feature lets you add extension methods to a target class by writing the methods in a separate extension class. The following rules apply:
    • The extension class must be static.
    • The name of the extension class must end with the ten-character suffix _Extension. However, there’s no restriction on the part of the name that precedes the suffix.
    • Every extension method in the extension class must be declared as public static.
    • The first parameter in every extension method is the type that the extension method extends. However, when the extension method is called, the caller must not pass in anything for the first parameter. Instead, the system automatically passes in the required object for the first parameter.
It’s perfectly valid to have private or protected static methods in an extension class. These are typically used for implementation details and are not exposed as extensions.
The example below illustrates an extension class holding a few extension methods:
public static class AtlInventLocation_Extension
{
public static InventLocation refillEnabled(
InventLocation _warehouse,
boolean _isRefillEnabled = true)
{
_warehouse.ReqRefill = _isRefillEnabled;
return _warehouse;
}

public static InventLocation save(InventLocation _warehouse)
{
_warehouse.write();
return _warehouse;
}
}

Extension methods AX7

The extension method feature lets you add extension methods to a target class by writing the methods in a separate extension class. The following rules apply:
    • The extension class must be static.
    • The name of the extension class must end with the ten-character suffix _Extension. However, there’s no restriction on the part of the name that precedes the suffix.
    • Every extension method in the extension class must be declared as public static.
    • The first parameter in every extension method is the type that the extension method extends. However, when the extension method is called, the caller must not pass in anything for the first parameter. Instead, the system automatically passes in the required object for the first parameter.
It’s perfectly valid to have private or protected static methods in an extension class. These are typically used for implementation details and are not exposed as extensions.
The example below illustrates an extension class holding a few extension methods:
public static class AtlInventLocation_Extension
{
  public static InventLocation refillEnabled(
    InventLocation _warehouse, 
    boolean _isRefillEnabled = true)
  {
    _warehouse.ReqRefill = _isRefillEnabled;
    return _warehouse;
  }

  public static InventLocation save(InventLocation _warehouse)
  {
    _warehouse.write();
    return _warehouse;
  }
}

Using clauses AX7

Previously, all references to managed artifacts that weren’t authored in X++ was done using fully qualified names, including the namespace for each type. This is still possible, but you can now provide using clauses to make the use of such artifacts less onerous. As opposed to a using statement, each using clause precedes the class in which the clause is applied.

It’s also possible to provide aliases that introduce a short name for a fully qualified name. Aliases can denote namespaces and classes as shown below.

Example

Consider the following code:
 using System;
using IONS=System.IO; // Namespace alias
using Alist=System.Collections.ArrayList; // Class alias

public class MyClass2
{
  public static void Main(Args a)
  {
    Int32 I; // Alternative to System.Int32
    Alist al; // Using a class alias

    al = new Alist();
    str s;

    al.Add(1);

    s = IONS.Path::ChangeExtension(@"c:\tmp\test.xml", ".txt");
  }
}

Differences between legacy X++ and new X++ AX7

In this section, we’ll see some of the differences between legacy X++ and the new X++.

Reals are Decimals

The type used to represent real values has changed from interpreted X++. This won’t require you to rewrite any code, because the new type can express all of the values that the old one could. We provide this material in the interest of full disclosure only.


All instances of the real type are now implemented as instances of the .NET decimal type (System.Decimal). Just as the real type in previous versions, the decimal type in a binary coded decimal type that, unlike floating point type, is resilient to rounding errors. The range and resolution of the decimal type are different from the original types.


The original X++ real type supported 16 digits and an exponent that defined where the decimal point is placed. The new X++ real type can represent decimal numbers ranging from positive

79,228,162,514,264,337,593,543,950,335 (296-1)

to negative

79,228,162,514,264,337,593,543,950,335 (-(296-1)).

The new real type doesn’t eliminate the need for rounding. For example, the following code produces a result of 0.9999999999999999999999999999 instead of 1. This is readily seen when using the debugger to show the value of the variables below.


public class MyClass2
{
public static void Main(Args a)
{
real dividend = 1.0;
real divisor = 3.0;
str stringvalue;
System.Decimal valueAsDecimal;

real value = dividend/divisor * divisor;

valueAsDecimal = value;
info(valueAsDecimal.ToString("G28"));

}
}
No number of decimals will suffice to represent the value of 1/3 accurately. The discrepancy obtained here is due to the fact that only a finite number of decimals are provided.
You should use the Round function to round to the number of decimals required.
  value = round(value, 2);

Differences between legacy X++ and new X++ AX7

In this section, we’ll see some of the differences between legacy X++ and the new X++.

Reals are Decimals

The type used to represent real values has changed from interpreted X++. This won’t require you to rewrite any code, because the new type can express all of the values that the old one could. We provide this material in the interest of full disclosure only.


All instances of the real type are now implemented as instances of the .NET decimal type (System.Decimal). Just as the real type in previous versions, the decimal type in a binary coded decimal type that, unlike floating point type, is resilient to rounding errors. The range and resolution of the decimal type are different from the original types.


The original X++ real type supported 16 digits and an exponent that defined where the decimal point is placed. The new X++ real type can represent decimal numbers ranging from positive

79,228,162,514,264,337,593,543,950,335 (296-1)

to negative

79,228,162,514,264,337,593,543,950,335 (-(296-1)).

The new real type doesn’t eliminate the need for rounding. For example, the following code produces a result of 0.9999999999999999999999999999 instead of 1. This is readily seen when using the debugger to show the value of the variables below.


public class MyClass2
{
  public static void Main(Args a)
  {
    real dividend = 1.0;
    real divisor = 3.0;
    str stringvalue;
    System.Decimal valueAsDecimal;

    real value = dividend/divisor * divisor;

    valueAsDecimal = value;
    info(valueAsDecimal.ToString("G28"));

  }
}
No number of decimals will suffice to represent the value of 1/3 accurately. The discrepancy obtained here is due to the fact that only a finite number of decimals are provided.
You should use the Round function to round to the number of decimals required.
  value = round(value, 2);

String truncation in AX7

This isn’t really a new feature, since it has been in place in the legacy product for the whole life of Dynamics AX. However, when code is executed in IL in prior versions of Dynamics AX, the automatic string truncation described here doesn’t take place.

Because it’s not a well-known or well understood feature, we’ll briefly discuss it here.
String values can be declared in X++ to contain a maximum number of characters. Typically, this is achieved by encoding this information in an extended data type, as shown below: Credit card numbers cannot exceed 20 characters.

StringTruncationSolutionExplorer_DebugFeatures
It’s also possible to specify length constraints directly in the X++ syntax:
str 20 creditCardNumber;
All assignments to these values are implicitly truncated to this maximum length.

Exercise

Run the following code in the debugger by including it in a static main method:
creditCardNumber = "12345678901234567890Excess string";

String truncation in AX7

This isn’t really a new feature, since it has been in place in the legacy product for the whole life of Dynamics AX. However, when code is executed in IL in prior versions of Dynamics AX, the automatic string truncation described here doesn’t take place.

Because it’s not a well-known or well understood feature, we’ll briefly discuss it here.
String values can be declared in X++ to contain a maximum number of characters. Typically, this is achieved by encoding this information in an extended data type, as shown below: Credit card numbers cannot exceed 20 characters.

StringTruncationSolutionExplorer_DebugFeatures
It’s also possible to specify length constraints directly in the X++ syntax:
str 20 creditCardNumber;
All assignments to these values are implicitly truncated to this maximum length.

Exercise

Run the following code in the debugger by including it in a static main method:
creditCardNumber = "12345678901234567890Excess string";

Compiler diagnoses attempts to store objects in containers AX7

In previous incarnations of the X++ compiler, it was possible to store object references into containers, even though this would fail at runtime. This is no longer possible. When the compiler sees an attempt to store an object reference into a container:
container c = [new Query()];
It will issue the error message:
Instances of type 'Query' cannot be added to a container.
If the type of the element that is added to the container is anytype the compiler can’t make the determination of whether or not the value is a reference type. The compiler will allow this under the assumption that the user knows what they’re doing. The compiler won’t diagnose the following code as erroneous:
anytype a = new Query();
container c = [a];
but an error will be thrown at runtime.

Casting AX7

The previous version of X++ was very permissive in its treatment of type casting. Both up-casting and down-casting were allowed without intervention from the programmer. Some of the casting permitted in legacy X++ can’t be implemented in the confines of the .NET runtime environment.
In object oriented programming languages, including X++, casting refers to assignments between variables whose declared types are both in the same inheritance chain. A cast is either a down-cast or an up-cast. To set the stage for this discussion, we introduce a few self-explanatory class hierarchies:
Casting_DebugFeatures
As you can see, the MotorVehicle class isn’t related to the Animal cast.
An up-cast happens when assigning an expression of a derived type to a base type:
  Animal a = new Horse();
down-cast happens when assigning an expression of a base type to a derived variable.
Horse h = new Animal();
Both up-casts and down-casts are supported in X++. However, down-casts are dangerous and should be avoided whenever possible. The example above will fail with an InvalidCastException at runtime, since the assignment doesn’t make sense.
X++ supports late binding on a handful of types, like object and formrun. This means that the compiler won’t diagnose any errors at compile-time when it sees a method being called on those types, if that method isn’t declared explicitly on the type,. It’s assumed that the developer knows what they’re doing.
For instance, the following code may be found in a form.
Object o = element.args().caller();
o.MyMethod(3.14, “Banana”);
The compiler can’t check the parameters, return values, etc. for the MyMethod method, since this method isn’t declared on the object class. At runtime, the call will be made using reflection, which is orders of magnitude slower than normal calls. note that calls to methods that are actually defined on the late binding types will be naturally checked.
For example, the call to ToString():
o.ToString(45);
will cause a compilation error:
'Object.toString' expects 0 argument(s), but 1 specified.
because the ToString method is defined on the object class.
There’s one difference from the implementation of previous version of X++, related to the fact that methods could be called on unrelated objects, as long as the name of the method was correct, even if the parameter profiles weren’t entirely correct. This isn’t supported in CIL.

Example

public class MyClass2
{
public static void Main(Args a)
{
Object obj = new Car();
Horse horse = obj; // exception now thrown
horse.run(); // Used to call car.run()!
}
}
You should use the IS and AS operators liberally in your code. The IS operator can be used if the expression provided is of a particular type (including derived types); the AS operator will perform casting into the given type and return null if a cast isn’t possible.

Casting AX7

The previous version of X++ was very permissive in its treatment of type casting. Both up-casting and down-casting were allowed without intervention from the programmer. Some of the casting permitted in legacy X++ can’t be implemented in the confines of the .NET runtime environment.
In object oriented programming languages, including X++, casting refers to assignments between variables whose declared types are both in the same inheritance chain. A cast is either a down-cast or an up-cast. To set the stage for this discussion, we introduce a few self-explanatory class hierarchies:
Casting_DebugFeatures
As you can see, the MotorVehicle class isn’t related to the Animal cast.
An up-cast happens when assigning an expression of a derived type to a base type:
  Animal a = new Horse();
down-cast happens when assigning an expression of a base type to a derived variable.
Horse h = new Animal();
Both up-casts and down-casts are supported in X++. However, down-casts are dangerous and should be avoided whenever possible. The example above will fail with an InvalidCastException at runtime, since the assignment doesn’t make sense.
X++ supports late binding on a handful of types, like object and formrun. This means that the compiler won’t diagnose any errors at compile-time when it sees a method being called on those types, if that method isn’t declared explicitly on the type,. It’s assumed that the developer knows what they’re doing.
For instance, the following code may be found in a form.
Object o = element.args().caller();
  o.MyMethod(3.14, “Banana”);
The compiler can’t check the parameters, return values, etc. for the MyMethod method, since this method isn’t declared on the object class. At runtime, the call will be made using reflection, which is orders of magnitude slower than normal calls. note that calls to methods that are actually defined on the late binding types will be naturally checked.
For example, the call to ToString():
o.ToString(45);
will cause a compilation error:
'Object.toString' expects 0 argument(s), but 1 specified.
because the ToString method is defined on the object class.
There’s one difference from the implementation of previous version of X++, related to the fact that methods could be called on unrelated objects, as long as the name of the method was correct, even if the parameter profiles weren’t entirely correct. This isn’t supported in CIL.

Example

public class MyClass2
{
  public static void Main(Args a)
  {
    Object obj = new Car();
    Horse horse = obj; // exception now thrown
    horse.run();    // Used to call car.run()!
  }
}
You should use the IS and AS operators liberally in your code. The IS operator can be used if the expression provided is of a particular type (including derived types); the AS operator will perform casting into the given type and return null if a cast isn’t possible.

Compiler diagnoses attempts to store objects in containers AX7

In previous incarnations of the X++ compiler, it was possible to store object references into containers, even though this would fail at runtime. This is no longer possible. When the compiler sees an attempt to store an object reference into a container:
container c = [new Query()];
It will issue the error message:
Instances of type 'Query' cannot be added to a container.
If the type of the element that is added to the container is anytype the compiler can’t make the determination of whether or not the value is a reference type. The compiler will allow this under the assumption that the user knows what they’re doing. The compiler won’t diagnose the following code as erroneous:
anytype a = new Query();
        container c = [a];
but an error will be thrown at runtime.

Obsolete statement types AX7

The following statements are no longer supported.

Pause and Window statements

The X++ pause statement is no longer supported because the pop-up Print Window that it affected has been removed. the pause and window statement were mainly used for debugging within the MorphX development environment, which was the same as the execution environment. Since the two are now separated, with Visual Studio taking the place of the MorphX environment, these statements are no longer relevant.

Print statement

The X++ print statement is another statement that existed only for debugging purposes in Dynamics AX. It still exists, and its basic idea is unchanged. But print now outputs through System.Diagnostics.WriteLine. The product configuration determines the detail of the written information is sent.
DebuggingAdmin_DebugFeatures
You may find that using the Infolog is more compelling, since its output appears in the debugger and the running form.

The mkDate predefined function no longer accepts shorthand values

In legacy systems, it was possible to use “shorthand” values for the year argument of the mkDate function. The effect can be seen in the following code sample.
static void Job16(Args _args)
{
int y;
date d;

for (y = 0; y < 150; y++)
{
d = mkDate(1,1,y);
info(strFmt("%1 - %2", y, year(d)));
}
}
Running this code in the legacy system will produce the following values:
0 – 2000
1 – 2001
2 – 2002

27 – 2027
28 – 2028
29 – 2029
30 – 2030
31 – 1931
32 – 1932
33 – 1933

97 – 1997
98 – 1998
99 – 1999
100 – 1900
We no longer support these values. Attempts to use such values will cause the mkDate function to return the null date (1/1/1900).

Cross company clause can contain arbitrary expressions AX7

The crosscompany clause can be used on select statements to indicate the companies that the search statement should take into account. The syntax hasn’t been enhanced to allow arbitrary expressions (of type container) instead of a single identifier, which is a variable of type container.
  private void SampleMethod()
{
MyTable t;
container mycompanies = ['dat', 'dmo'];
select crosscompany: mycompanies t;
}
Now, it’s possible to provide the expression without having to use a variable for this purpose.
  private void SampleMethod()
{
MyTable t;
container mycompanies = ['dat', 'dmo'];
select crosscompany: (['dat'] + ['dmo']) t;
}

Cross company clause can contain arbitrary expressions AX7

The crosscompany clause can be used on select statements to indicate the companies that the search statement should take into account. The syntax hasn’t been enhanced to allow arbitrary expressions (of type container) instead of a single identifier, which is a variable of type container.
  private void SampleMethod()
  {
    MyTable t;
    container mycompanies = ['dat', 'dmo'];
    select crosscompany: mycompanies t;
  }
Now, it’s possible to provide the expression without having to use a variable for this purpose.
  private void SampleMethod()
  {
    MyTable t;
    container mycompanies = ['dat', 'dmo'];
    select crosscompany: (['dat'] + ['dmo']) t;
  }

The mkDate predefined function no longer accepts shorthand values

In legacy systems, it was possible to use “shorthand” values for the year argument of the mkDate function. The effect can be seen in the following code sample.
static void Job16(Args _args)
{
  int y;
  date d;
  
  for (y = 0; y < 150; y++)
  {
    d = mkDate(1,1,y);  
    info(strFmt("%1 - %2", y, year(d)));
  }
}
Running this code in the legacy system will produce the following values:
0 – 2000
1 – 2001
2 – 2002

27 – 2027
28 – 2028
29 – 2029
30 – 2030
31 – 1931
32 – 1932
33 – 1933

97 – 1997
98 – 1998
99 – 1999
100 – 1900
We no longer support these values. Attempts to use such values will cause the mkDate function to return the null date (1/1/1900).

Obsolete statement types AX7

The following statements are no longer supported.

Pause and Window statements

The X++ pause statement is no longer supported because the pop-up Print Window that it affected has been removed. the pause and window statement were mainly used for debugging within the MorphX development environment, which was the same as the execution environment. Since the two are now separated, with Visual Studio taking the place of the MorphX environment, these statements are no longer relevant.

Print statement

The X++ print statement is another statement that existed only for debugging purposes in Dynamics AX. It still exists, and its basic idea is unchanged. But print now outputs through System.Diagnostics.WriteLine. The product configuration determines the detail of the written information is sent.
DebuggingAdmin_DebugFeatures
You may find that using the Infolog is more compelling, since its output appears in the debugger and the running form.

New Debugger features in AX7

This section provides information about the new features that we’ve added to the debugging experience in Visual Studio.

Adding ToString methods to your classes

It’s often a benefit to add ToString() methods to your classes. The effort spent doing this comes back many times and it’s easy to do. This advice also holds true for legacy X++.
Note: Since ToString methods can be called at unpredictable times, it isn’t a good idea to change the state of the object here.

Identifying unselected fields

It’s a common source of bugs to use fields from a table when these fields don’t appear in the field list in a select statement. Such fields will have a default value according to their type. It’s now possible in the debugger to see if a value has been selected or not.

Exercise

Consider the following code:
class MyClass
{
public static void Main(Args a)
{
FMRental rental;

select EndMileage, RentalId from rental;

rental.Comments = "Something";
}
}
Set a breakpoint on the assignment statement. Make your class the startup object in your project, and start by pressing F5.
When the breakpoint is encountered, view the rental variable by expanding it in the locals window. You’ll see something similar to the following graphic.
DebuggingAdmin2_DebugFeatures
You can see that the fields that have been selected (EndMileage and RentalId) appear with their selected values, while the unselected fields appear as null. This signifies their value wasn’t fetched from the database. Obviously, this is a debugging artifact. The values of the unselected fields will be the default value for the type of the field.
Step over this and notice how the debugger changes the rendering to the actual value.
Note: If the table is set to Cache, the system will always fetch all fields from the entire table, irrespective of the field list provided in the code.

The Auto and Infolog Windows

The debugger will allow you to easily access certain parts of the state of the application easily. This information is available in the autos window, where the current company, the partition, the transaction level, and the current user id are listed.
Autos_DebugFeatures
There is also a window showing the data that is written to the Infolog.
Infolog_DebugFeatures

New breakpoint features

The Visual Studio debugger supports conditional breakpoints and breakpoints that are triggered by hit count. You can also have the system perform specific actions for you as you hit the breakpoint. None of these features were available in the legacy debugger. These are explained below:
  • Hit count enables you to determine how many times the breakpoint is hit before the debugger breaks execution. By default, the debugger breaks execution every time that the breakpoint is hit. You can set a hit count to tell the debugger to break every 2 times the breakpoint is hit, or every 10 times, or every 512 times, or any other number you choose. Hit counts can be useful because some bugs don’t appear the first time your program executes a loop, calls a function, or accesses a variable. Sometimes, the bug might not appear until the 100th or the 1000th iteration. To debug such a problem, you can set a breakpoint with a hit count of 100 or 1000.
  • Condition is an expression that determines whether the breakpoint is hit or skipped. When the debugger reaches the breakpoint, it’ll evaluate the condition. The breakpoint will be hit only if the condition is satisfied. You can use a condition with a location breakpoint to stop at a specified location only when a certain condition is true.
    For example, suppose you’re debugging a banking program where the account balance is never allowed to go below zero. You might set breakpoints at certain locations in the code and attach a condition such as balance < 0 to each one. When you run the program, execution will break at those locations only when the balance is less than zero. You can examine variables and program state at the first breakpoint location, and then continue execution to the second breakpoint location, and so on.
  • Action specifies something that should occur when the breakpoint is hit. By default, the debugger breaks execution, but you can choose to print a message or run a Visual Studio macro instead. If you decide to print a message instead of breaking, the breakpoint has an effect very similar to a Trace statement. This method of using breakpoints is called tracepoints

Exercise

Consider the following code:
class PVsClass
{
public static void Main(Args a)
{
int i;
for (i = 0; i < 10; i++)
{
print i;
}
}
}
Put a breakpoint on the print statements by pressing F9 while that statement is selected. This will create a normal, unconditional breakpoint. Now, use the mouse to open the context menu for the breakpoint and select Condition. Put in a condition that indicates that the breakpoint should happen when the value of the ‘i’ variable exceeds 5. Set the class as a startup project, and the class containing the code as the startup item in the project. Run the code. Feel free to modify the value of ‘i’ using the debugger.
Now, remove this breakpoint, and use the Hit count feature to accomplish the same thing.
Note: A breakpoint can have several conditions. It’s often helpful to hover the cursor over the breakpoint, causing an informative tooltip to appear.
Tracepoints are often useful tot race execution. Insert a tracepoint on the line in question and log the value of the variable. The trace output will appear in the output window in the debugger.

New Debugger features in AX7

This section provides information about the new features that we’ve added to the debugging experience in Visual Studio.

Adding ToString methods to your classes

It’s often a benefit to add ToString() methods to your classes. The effort spent doing this comes back many times and it’s easy to do. This advice also holds true for legacy X++.
Note: Since ToString methods can be called at unpredictable times, it isn’t a good idea to change the state of the object here.

Identifying unselected fields

It’s a common source of bugs to use fields from a table when these fields don’t appear in the field list in a select statement. Such fields will have a default value according to their type. It’s now possible in the debugger to see if a value has been selected or not.

Exercise

Consider the following code:
class MyClass
{
  public static void Main(Args a)
  {
    FMRental rental;

    select EndMileage, RentalId from rental;

    rental.Comments = "Something";
  }
}
Set a breakpoint on the assignment statement. Make your class the startup object in your project, and start by pressing F5.
When the breakpoint is encountered, view the rental variable by expanding it in the locals window. You’ll see something similar to the following graphic.
DebuggingAdmin2_DebugFeatures
You can see that the fields that have been selected (EndMileage and RentalId) appear with their selected values, while the unselected fields appear as null. This signifies their value wasn’t fetched from the database. Obviously, this is a debugging artifact. The values of the unselected fields will be the default value for the type of the field.
Step over this and notice how the debugger changes the rendering to the actual value.
Note: If the table is set to Cache, the system will always fetch all fields from the entire table, irrespective of the field list provided in the code.

The Auto and Infolog Windows

The debugger will allow you to easily access certain parts of the state of the application easily. This information is available in the autos window, where the current company, the partition, the transaction level, and the current user id are listed.
Autos_DebugFeatures
There is also a window showing the data that is written to the Infolog.
Infolog_DebugFeatures

New breakpoint features

The Visual Studio debugger supports conditional breakpoints and breakpoints that are triggered by hit count. You can also have the system perform specific actions for you as you hit the breakpoint. None of these features were available in the legacy debugger. These are explained below:
  • Hit count enables you to determine how many times the breakpoint is hit before the debugger breaks execution. By default, the debugger breaks execution every time that the breakpoint is hit. You can set a hit count to tell the debugger to break every 2 times the breakpoint is hit, or every 10 times, or every 512 times, or any other number you choose. Hit counts can be useful because some bugs don’t appear the first time your program executes a loop, calls a function, or accesses a variable. Sometimes, the bug might not appear until the 100th or the 1000th iteration. To debug such a problem, you can set a breakpoint with a hit count of 100 or 1000.
  • Condition is an expression that determines whether the breakpoint is hit or skipped. When the debugger reaches the breakpoint, it’ll evaluate the condition. The breakpoint will be hit only if the condition is satisfied. You can use a condition with a location breakpoint to stop at a specified location only when a certain condition is true.
    For example, suppose you’re debugging a banking program where the account balance is never allowed to go below zero. You might set breakpoints at certain locations in the code and attach a condition such as balance < 0 to each one. When you run the program, execution will break at those locations only when the balance is less than zero. You can examine variables and program state at the first breakpoint location, and then continue execution to the second breakpoint location, and so on.
  • Action specifies something that should occur when the breakpoint is hit. By default, the debugger breaks execution, but you can choose to print a message or run a Visual Studio macro instead. If you decide to print a message instead of breaking, the breakpoint has an effect very similar to a Trace statement. This method of using breakpoints is called tracepoints

Exercise

Consider the following code:
class PVsClass
{
  public static void Main(Args a)
  {
    int i;
    for (i = 0; i < 10; i++)
    {
      print i;
    }
  }
}
Put a breakpoint on the print statements by pressing F9 while that statement is selected. This will create a normal, unconditional breakpoint. Now, use the mouse to open the context menu for the breakpoint and select Condition. Put in a condition that indicates that the breakpoint should happen when the value of the ‘i’ variable exceeds 5. Set the class as a startup project, and the class containing the code as the startup item in the project. Run the code. Feel free to modify the value of ‘i’ using the debugger.
Now, remove this breakpoint, and use the Hit count feature to accomplish the same thing.
Note: A breakpoint can have several conditions. It’s often helpful to hover the cursor over the breakpoint, causing an informative tooltip to appear.
Tracepoints are often useful tot race execution. Insert a tracepoint on the line in question and log the value of the variable. The trace output will appear in the output window in the debugger.