Classes and methods

Important

Dynamics 365 for Finance and Operations is now being licensed as Dynamics 365 Finance and Dynamics 365 Supply Chain Management. For more information about these licensing changes, see Dynamics 365 Licensing Update.

This topic describes how to create and use classes in X++.

A class is a software construct that defines the data and methods of the instances that are later constructed from that class. The class is an abstraction of an object in the problem domain. The instances that are constructed from the class are known as instances or objects. This topic uses the term instance. The data represents the state of the object, whereas the methods represent the behavior of the object. Variables contain the data for the class. Every instance that is constructed from the class declaration has its own copy of the variables. These variables are known as instance variables. Methods define the behavior of a class. They are the sequences of statements that operate on the data (instance variables). By default, methods are declared to operate on the instance variables of the class. These methods are known as instance methods or object methods.

You can declare static methods and static fields, that do not have access to instance variables. These are described in Static class members.

Declare a class

You must use the Add new item dialog in Visual Studio to add a class to your project.

  1. In Server Explorer, right-click the project, and then click Add.
  2. In the New Item dialog box, select Installed > Dynamics 365 Items > Code in the left navigation. Then select Class, and then enter a name for the class.
  3. Click Add.

All classes are public. If you remove the public modifier, the system still treats the class as public. You can specify other modifiers on the class declaration, such as final and extends.

Variables

Instance variables are protected by default. This means that they can only be accessed in the same class or a derived class. You can modify and instance variable declaration by using the private, protected, or public keywords.

The following example shows how to use accessor methods to make the variable data public. The variable firstName is protected, so accessor (get and set) methods are implemented to allow access to the protected variable. The variable lastName is public, so code can directly get and set the value of the variable.

// This is the class definition.
public class HasAFirstName
{
    str firstName;
    public str lastName;
    public str getFirstName()
    {
        return firstName;
    }

    public void setFirstName(str newName)
    {
       firstName = newName;
    }
}

// This code creates an instance of the class and gets the variables.
public static void TestLastName()
{
    HasAFirstName hasFirstName = new HasAFirstName();
    hasFirstName.setFirstName("Dion");
    info(hasFirstName.getFirstName());
    hasFirstName.lastName = ("Townes");
    info(hasFirstName.lastName);
}
// The output is "Dion" and "Townes".

Constructors

To create an instance of a class, you must instantiate it by using a constructor.

  • You can define only one new method (constructor) in a class.
  • If you do not define a constructor, a default constructor with no parameters is created automatically by the compiler.
  • You can simulate a default constructor by assigning default values to the parameters in the new method.

The following examples defines a parameterless constructor in the Point class.

class Point
{

    // Instance variables that are public. In practice, you would probably make this protected or private 
    // and create accessor methods.
    public real x = 0.0;
    public real y = 0.0;

    void new() {
    }
}

Following is information about how to create a clean inheritance model and minimize problems when code is upgraded:

  • Each class must have a single public construction method unless the class is abstract. If no initialization is required, use a static construct method. Otherwise, use a static new method (the default constructor for the class should be protected).
  • Each class should have at least one static construct method method.
  • Each class should have at least one static new method.
  • Each class should have a new method (the default constructor). This method should be protected.
  • Create accessor methods to get and set class variables.
  • Create init methods to carry out any specialized initialization tasks that should be carried out after instantiation.

Create other objects in a constructor

A class constructor can instantiate other objects in addition to creating an instance of the class. For example, the following code declares a Rectangle class that uses two Point objects to define its bounds. In this case, the Point class has a constructor that has two real parameters.


class Point
{
    // Instance variables that are public. In practice, you would probably make this protected or private
    // and create accessor methods.
    public real x = 0.0;
    public real y = 0.0;

    // Constructor to initialize to a specific or default value
    void new(real _x = 10, real _y = 10)
    {
        x = _x;
        y = _y;
    }
}

class Rectangle
{
    public Point lowerLeft;
    public Point upperRight;

    void new(real _topLeftX = 0.0, real _topLeftY = 0.0, real _bottomRightX = 1.0, real _bottomRightY = 1.0)
    {
        lowerLeft  = new Point(_topLeftX, _topLeftY);
        upperRight = new Point(_bottomRightX, _bottomRightY);
    }

}

// This code creates two instances of the Rectangle class.
Rectangle defaultRectangle = new Rectangle();
info(any2Str(defaultRectangle.lowerLeft.x));
info(any2Str(defaultRectangle.lowerLeft.y));
// Output is "0.0" and "0.0".

Rectangle customRectangle = new Rectangle(1.0, 1.0, 2.0, 2.0);
info(any2Str(customRectangle.lowerLeft.x));
info(any2Str(customRectangle.lowerLeft.y));
// Output is "1.0" and "1.0".

Create an instance of an object

The constructor, new, returns a new instance of the class. The following code example creates two instances of the Point class.

// Declare a variable to refer to a Point instance.
Point myPoint;

// Create an instance of the Point class.
myPoint = new Point();

// Declare and instantiate a Point instance.
Point ap = new Point();

Destructors

You use a destructor to explicitly destroy a class instance. Instances are automatically destroyed when there are no references to them. However, you can destroy objects explicitly in the following ways:

  • Use the finalize method.
  • Set the reference variable to null.

Use the finalize method

Use the finalize method to explicitly destroy an object. There are no implicit calls to the finalize method. You must call the method to run the statements in it. In the finalize method, you should also put any clean-up code that is required. For example, if your class uses a dynamic-link library (DLL) module, you can use the finalize method to release the DLL when you no longer require it. Use the finalize method carefully. It will destroy an object even if there are references to it.

The following example shows the basic structure for a call to the finalize method.

// From any method in a class.
if (condition)
{
    // Removes object from memory.
    this.finalize(); 
}

Set reference variable to null

Set the reference variable to null to terminate an object. This approach destroys an object only if no other variables point to that object. You should verify that other code isn't using the variable. The following example creates an reference variable and then sets it to null.

Point myPoint = new Point();
myPoint = null;

Methods

Instance methods

Instance methods are embedded in each instance that is created from the class. You must instantiate the object before you can use the method. The following code shows how to define an instance method and call it from an instance.

class Square
{

    int side = 0;

    void new(int _side = 1) {
        side = _side;
    }

    int getArea() {
        return side * side;
    }

}

// This code creates an instance of Square and calls getArea.
Square square = new Square(15);
int area = square.getArea();
info(int2Str(area));
// Output is "225".

Static methods

Static methods, which are also known as class methods, belong to a class and are created by using the keyword static. You don't have to instantiate an object before you use static methods. Static methods are often used to work with data that is stored in tables. Member variables can't be used in a static method. You use the following syntax to call static methods.

    ClassName::methodName();

If you convert an instance method to a static method, you must restart the client. Otherwise, the compiler doesn't detect the change. After you've converted an instance method to a static method, you can no longer call the method from the instance of the class. Instead, you must call the method from the class itself. For more information about static methods, see Static class members.

main methods

A main method is a class method that is run directly from a menu option. The method should only create an instance of the object and then call the required member methods. The _args parameter lets you transfer data to the method.

static void main (Args _args)
{
    // Your code here.
}

Declaration of methods

Method declarations consist of a header and a body. The method header declares the method's name and return type), the method modifiers, and parameters. (The return type might be void.) The method body consists of variable declarations, method declarations, and statements.

Return type

A return type is required for each method. If a method doesn't return anything, use the void keyword as the return type. The following example shows two methods. One method has a return type, but the other method doesn't have a return type.

    void methodNameNoReturnValue()
    {
        // Your code here.
    }

    // If a method returns something, you must specify the return type and include a return statement.
    int methodNameIntegerReturnValue()
    {
        return 1;
    }

Syntax

Method declaration = Heading Body Heading = [ Modifiers ] ReturnType MethodName ( ParameterList )

Modifiers = [client] [server] [edit | display | public | protected | private] [static | abstract | final ]

ReturnType = Datatype | void | anytype MethodName = Identifier

ParameterList = [ Parameter { , Parameter }]

Parameter = Datatype Variableidentifier [ = Expression ]

Body = { [ VariableDeclarations ] [ EmbeddedFunctionDeclarations ] [ Statements ] }

EmbeddedFunctionDeclaration = Heading {[ VariableDeclarations ] [ Statements ]}

If you use the anytype return type, the method can return any data type.

Example of a method that doesn't have a return type

void update ()
{   
    // Variable declared and initialized
    CustTable this_Orig = this.orig();

    // First statement in body (begin transaction)
    ttsBegin;
    this.setNameAlias();
    // Calls super's implementation of update
    super();
    this.setAccountOnVend(this_Orig);
    if (this_Orig.custGroup != this.custGroup)
        ForecastSales::setCustGroupId(
            this.accountNum,
            this_Orig.custGroup,
            this.custGroup);
    // Commits transaction
    ttsCommit;
}

Example of a method that has parameters

In the following example, the checkAccountBlocked method returns a Boolean value and acts on the amountCur parameter.

boolean checkAccountBlocked(AmountCur amountCur)
{
    if (this.blocked == CustVendorBlocked::All 
        ||(this.blocked == CustVendorBlocked::Invoice 
        && amountCur > 0 ))
    return checkFailed(strFmt("@SYS7987",this.accountNum));
    return true;
}

Method modifiers

Several modifiers can be applied to method declarations. Some of the modifiers can be combined (for example, final static). Here are the method modifier keywords:

  • abstract: The method is declared but isn't implemented in a parent class. The method must be overridden in subclasses. If you try to create an object from a subclass where one or more abstract methods that belong to the parent class haven't been overridden, you receive a compiler error. Classes can also be abstract. Sometimes, a class should not be instantiated even though it represents an abstract concept. Only subclasses should be instantiated. Base classes of this type can be declared as abstract. For example, you want to model the concept of an account. Accounts are abstract, because only derived classes (ledger accounts and so on) exist in the real world. This examples describes a clear case where you should declare the Account class as abstract.
  • display: The method's return value should be shown on a page or a report. The value can't be modified on the page or report. Typically, the return value is a calculated value, such as a sum.
  • edit: The method's return type should be used to provide information for a field that is used on a page. The value in the field can be modified.
  • final: The method can't be overridden in any class that derives from its class.
  • public: Methods that are declared as public can be accessed anywhere that the class is accessible, and they can be overridden by subclasses. Methods that have no access modifier are implicitly public.
  • protected: Methods that are declared as protected can be called only from methods in the class and in subclasses that extend the class where the method is declared.
  • private: Methods that are declared as private can be called only from methods in the class where the private method is declared.
  • static: The method is a class method and doesn't act on an instance. Static methods can't refer to instance variables. They aren't invoked on an instance of the class. Instead, they are invoked by using the class name (for example, MyClass::aStaticProcedure()).

Methods that have modifiers

The following examples show only the method headers.

// A method that cannot be overridden
final int dontAlterMe() 

// A static method 
static void noChange()

// A display method that returns an integer
display int value()

Method access control

You use the accessor keywords public, protected, and private to control whether the methods in other classes can call the methods on your class. The accessor keywords on methods also interact with the rules for class inheritance. Here are the accessor keywords that you use with methods:

  • public: Methods that are declared as public can be called from anywhere that the class is accessible. In addition, a public method can be overridden by a subclass, unless the method is declared as final.
  • protected: Methods that are declared as protected can be called only from the following methods:
    • Methods in the class.
    • Methods in a subclass of the class that contains the protected method. Methods that are protected can be overridden in subclasses.
  • private: Methods that are declared as private can be called only from methods in the class where the private method is declared. No private method can be overridden in a subclass. By default, when you create a new method, the private accessor keyword appears in the code editor. For maximum security, private is the most conservative default accessor keyword.

Static and instance methods

The accessor keywords on methods never restrict calls between two methods that are in the same class, regardless of which method is static or non-static. In a static method, calls to the new constructor method are valid even if the new constructor method is decorated with the private modifier. The syntax for these calls requires that the new keyword be used. The code in a static method must construct an instance object of its own class before it can call any instance methods on the class.

Increasing access during overrides

When a method is overridden in a subclass, the overriding method must be at least as accessible as the overridden method. For example, the following compiler rules apply when a protected method is overridden in a subclass:

  • A public method in a superclass can be overridden only by a public method in the subclass.
  • In a subclass, a public or protected method can override a protected method of the superclass.
  • In a subclass, a private method can't override a protected method of the superclass.

Optional parameters

Parameters can be initialized in the method declaration. In this case, the parameter becomes an optional parameter. If no value is supplied in the method call, the default value is used. All required parameters must be listed before the first optional parameter. The following examples show how to create and call a method that has optional parameters. The example of the AddThreeInts method shows that you can't skip default parameters when you call a method.

Examples of optional parameters

The following code example shows a class with a default parameter.

// This is an example of a function being used as the default.
public class Person 
{
    date birthDate;

    // The constructor that takes a date type as
    // a parameter. That value is assigned to the field member birthDate. 
    void new(date _date)
    {
        birthDate = _date;
    }

    // The CalculateAgeAsOfDate method references the birthDate field and has an
    // optional parameter. In this example, the default value is the
    // return value of a function.
    public real CalculateAgeAsOfDate(date _calcToDate = DateTimeUtil::getToday(DateTimeUtil::getUserPreferredTimeZone()) )  
    {
        return (_calcToDate - birthDate) / 365;
    }


// This code instantiates a Person and calls CalculateAgeAsOfDate.
Person person = new Person(13\5\2010);   // birthDate is initialized.
// Optional parameter's default is used.
info("Age in years: " + num2str(person.CalculateAgeAsOfDate(),2,0,0,0));
// Output is "9" in July 2019.

// January 2, 2044  is the parameter value for _date.
info("Age in years: " + num2str(person.CalculateAgeAsOfDate(2\1\2044),2,0,0,0));
// Output is "34".
}

This is an example of how you cannot skip to a second optional parameter. The first method has two optional parameters. The second method is a caller of the first method. The caller wants to override only the _i3 default value, but the compiler requires that all prior optional parameters also be overridden in the call.

public class Additions 
{
    static public int AddThreeInts(int _i1, int _i2 = 2,int _i3 = 3)
    {
        return _i1 + _i2 + _i3;
    }
}

// This code calls the AddThreeInts method.
// No way to skip the first optional parameter (so it can default)
// while also specifying the value of the second optional parameter.
// The next statement does not compile.
// info(int2Str(Additions::AddThreeInts(1, 2, 99)));

// Settle for overriding both optional parameters.
info(int2Str(Additions::AddThreeInts(1, 2, 99)));
// Output is "102".

Accessor methods

Class variables are protected by default. By hiding details of the internal implementation of a class, you can change the implementation of the class later without breaking any code that uses that class. To access the data from reference variables, you must create accessor methods. The following example defines a Point class that uses accessor methods to access the variables x and y.

class Point
{
    // Instance variables
    real x; 
    real y;

    // Constructor to initialize to a specific or default value
    void new(real _x = 10, real _y = 10) 
    {
        x = _x;
        y = _y;
    }

    // Accessor methods
    void setX(real _x) 
    {
        x = _x;
    }

    void setY(real _y) 
    {
        y = _y;
    }

    real getX() 
    {
        return x;
    }

    real getY() 
    {
        return y;
    }
}

These method declarations show how the Point class provides access to its variables from the outside world. Other objects can manipulate the instance variables of Point objects by using the accessor methods.

Point myPoint = new Point();
// Set the x variable using the accessor method.
myPoint.setX(4.0);
// Get the x variable using the accessor method.
info(any2Str(myPoint.getX()));

Parameters

All methods have their own scope. A method can take one or more parameters. Within the scope of the method, these parameters are treated as local variables and are initialized with a value from the parameter in the method call. All parameters are passed by value, which means that you can't change the value of the original variable. You can change only the local variable in the method. This local variable is a copy of the original variable.

Scope of variables in methods

A scope defines the area in which an item can be accessed. Variables that are defined in a class are available to the methods within that class. Variables in methods can be accessed only within the current block.

Local functions

You can declare functions inside a method. These are called local functions. While possible, it is not a best practice. Instead, you should add private methods to the class.

  • The declarations of local functions must physically precede any non-declaration statements in the method.
  • You can declare more than one local function in your method. However, all local functions must be declared in an uninterrupted series, and the set must be terminated by one semicolon (;).
  • Code that is inside the local function can access variables that are declared in the method that contains the local function.
  • Code that is outside the local function can't access variables that are declared in the local function.
  • A local function can be called only by code in the same method where the local function is declared.
  • A local function should never call itself. Such recursion can prevent successful compilation.

The following example shows valid declarations of two local functions, localFunctionA and localFunctionB. Calls to the local functions occur after the function declarations in the example, as is required.

static void StaticFunction()
{
    int number = 654;

    void localFunctionA(int _iNum)  // The local function.
    {
        str innerString = "String in localFunctionA";
        str output = strFmt("localFunctionA: %1 , %2 , %3", _iNum, innerString, number);
        info(output);
    }

    void localFunctionB()
    {
        info("Printing from inside localFunctionB.");
    }

    localFunctionA(55);
    localFunctionB();
    // Next info statement would fail to compile,
    // because innerString is restricted to the
    // scope of the local function in which it is declared.
    // print innerString;
}

// When called, the output is:
// localFunctionA: 55 , String in localFunctionA , 654
// Printing from inside localFunctionB.

The this keyword

The this keyword is a reference to the instance of the class or table where the this keyword is used. The this reference is never required, but it can clarify your code and enhances the behavior of IntelliSense in the code editor. All calls to instance methods must be qualified by either the this reference or a variable. The this reference can be used to qualify the following information:

  • The names of other instance (non-static) methods in the same class where the this reference is used. Here is an example: boolColorChanged = this.colorItOrange();
  • The names of methods that are inherited by the this object.
  • The names of fields on the table that contains the method that the this keyword is used in.

The this reference can't be used in the following ways:

  • It can't qualify the names of member variables that are declared in the classDeclaration code.
  • It can't be used in a static method.
  • It can't qualify the names of static methods of the class or table.

Call stack limitation

The depth of the call stack is limited to 100.