学习MS Dynamics AX 2012编程开发 2. X++语言

X++是用于构建Dynamics AX功能的编程语言。X++是一种与C++类似的面向对象编程语言。

完成本章后,您将能够理解X++语言;您将知道可用的数据类型是什么,如何创建各种循环,如何比较和操作变量,在哪里可以找到预定义的函数,以及如何使用它们。

本章将涵盖:

  • 注释
  • 数据类型
  • 语句和循环
  • 操作符
  • 类和方法
  • 活动

前言

你已经看过Hello World的例子,可能认为这看起来很容易,是的,确实如此。X++语法不是很严格,因为它不区分大小写。但是,您应该尽量遵循AX其余部分中使用的命名约定。

以下列出了其中一些(但不是全部)命名约定:

  • 应用程序对象名称是混合大小写的(名称的第一个字母是大写的,名称中每个单词的第一个字符也是大写的)。这被称为Camel casing,例如SalesFormLetter。
  • 方法、变量和函数的名称大小写混合,第一个字母小写。这被称为Pascal大小写,例如initFromSalesTable。
  • 基元变量类型使用小写名称,例如str。
  • 所有名称应使用美国英语。
  • 命名时保持一致。
  • 只有表、基枚举和扩展数据类型才应具有前缀DEL_。此前缀用于指示该元素将在下一版本中删除,并由AX中的数据升级脚本使用。

要了解有关AX中不同最佳实践的更多信息,请查看开发人员的帮助文件或MSDN for Dynamics AX。

在代码中编写注释时,可以使用单行注释,也可以使用多行注释。一行注释如下所示:

// This is a one line comment

多行注释如下所示:

/* This comment spans over
multiple lines.
This is the last line of this comment */

编辑器窗口中的注释为绿色。

数据类型

现在我们来看看您可以在AX中使用的不同类型的数据类型。它们可以分为两大类:

  • 原始数据类型
  • 复合数据类型

原始数据类型

原始数据类型是编程语言的构建块。它们用于在运行时代码中存储基于类型的单个值。每个数据类型可以存储的值因所使用的数据类型而异。如果要存储文本字符串,可以使用字符串数据类型。如果要存储日期值,可以使用日期数据类型或utcdatetime。如果读取到变量中的数据类型是在运行时决定的,则也可以使用数据类型anytype。

在下一章中,我们将创建扩展数据类型,并了解如何使用它们轻松地在表中创建字段。所有扩展数据类型都扩展原始数据类型。AX中可用的不同原始数据类型如下:

• String
• Integer
• Real
• Boolean
• Date
• Enum
• timeofday
• utcdatetime
• anytype

String

字符串数据类型可能是最常用的数据类型之一。它用于保存文本信息,长度可以在声明变量时设置,也可以是动态的。当使用固定长度时,只需添加要将变量限制在变量类型和变量名称之间的字符数,如以下示例所示(请注意,变量类型关键字是str,而不是字符串):

static void Datatypes_string1(Args _args)
{
    str 7 fixedLengthString;
    str dynamicLengthString;
    fixedLengthString = "Welcome to Carz Inc";
    dynamicLengthString = "Welcome to Carz Inc";
    print fixedLengthString;
    print dynamicLengthString;
    pause;
}

前面代码中的打印语句将显示以下窗口:

使用字符串时,有一些不错的函数可以帮助您操作字符串、搜索字符串中的文本等。这些函数可以在AOT中导航到System Documentation | functions,也可以在Classes | Global中找到。本章介绍了其中一些功能,但您应该通过浏览AOT并亲自尝试来熟悉更多功能。

strfmt方法

strfmt方法用于格式化字符串,并使用参数n更改任何出现的 n:

Syntax: str strFmt(str _string, ...)

为了说明这一点,以下作业将打印100句, Welcome to Carz Inc:

static void Datatypes_string_strmft(Args _args)
{
    str name;
    int a;
    name = "Carz Inc";
    a = 100;
    print strfmt("%1: Welcome to %2", a, name);
    pause;
}

请注意,strfmt方法还将其他数据类型转换为字符串。

substr方法

substr方法返回字符串的一部分。第一个参数是原始字符串,第二个参数是起始位置,第三个参数是要读取的字符数。以下是一个示例:

//Syntax: str subStr(str _text, int _position, int _number)
static void Datatypes_string_substr(Args _args)
{
    str carBrand;
    carBrand = "Volkswagen";
    print substr(carBrand, 6, 5);
    pause;
}

前面方法的打印语句将显示wagen,因为子字符串从第一个参数值的位置6开始,即Volkswagen,并向前读取五个字符。

Integer

整数是没有小数的数字,在AX中,它们被分为两种不同的数据类型:Int32是32位整数,Int64是64位整数。

Int32数据类型的范围从-2147483647到2147483647。Int64数据类型的范围从9223372036854775808到9223372036864775808,这对我们大多数人来说应该足够了。

您可以直接在代码中对整数进行任何类型的算术运算,例如,以下示例中所示的乘法运算将打印Carz Inc总共有24辆车:

static void Datatypes_integer1(Args _args)
{
    int carsInEachOffice;
    int offices;
    carsInEachOffice = 6;
    offices = 4;
    print strfmt("Carz Inc have %1 cars in total", carsInEachOffice * offices);
    pause;
}

除以两个整数将得到一个实数,除非将值返回为整数。这意味着24除以4得到的结果为6.00,但将结果返回为整数并打印整数会返回6作为输出,如您从以下示例中看到的:

static void Datatypes_integer2(Args _args)
{
    int x = 24;
    int y = 4;
    int res = x/y;
    ;
    // Prints a real value
    print strfmt("%1 / %2 = %3", x, y, x/y);
    // Automatically type casted to int
    // to print the integer value
    print strfmt("%1 / %2 = %3", x, y, res);
    pause;
}

Real

实变量可以由十进制数和整数组成。它跨越了范围为–(10)127到(10)27,精度为16位有效数字。它是值得的提到在一些欧洲国家,即使系统语言设置可能使用逗号作为小数分隔符,AX中的代码仅接受句点作为十进制分隔符。

执行代码时,print语句将显示在区域设置中设置的十进制分隔符。

将打印以下作业 The car has used an average of 3,61 gallons gas:

static void Datatypes_real1(Args _args)
{
    real mileage;
    real mpg;
    mileage = 127.32;
    mpg = 35.24;
    print strfmt("The car has used an average of %1 gallons gas",mileage/mpg);
    pause;
}

还有一些有用的函数可用于AX中的真实数据类型。一些常用的数据类型是str2num和num2str。

str2num方法

如果您有一个表示数字的字符串变量,则可以使用str2num函数将该值转换为实数变量:

//Syntax: real str2Num(str _text)
static void Datatypes_str2num(Args _args)
{
    str mileage;
    real mileageNum;
    mileage = "388272.23";
    mileageNum = str2num(mileage);
    print strfmt("The car has run %1 miles", mileageNum);
    pause;
}

num2str函数

您也可以采用另一种方法,将实际变量的值转换为字符串。但是,此操作需要了解有关希望如何表示数字的更多信息。代码如下:

Syntax: str num2Str( real number, int character, int decimals,
int separator1, int separator2)

查看以下示例中的注释以了解参数的解释:

static void Datatypes_num2str(Args _args)
{
    str mileage;
    real mileageNum;
    mileageNum = 388272.23;
    // num2str(number to be converted,
    // minimum characters required,
    // required number of decimals,
    // decimal separator <1=point, 2=comma>,
    // thousand separator <0=none, 1=point,
    // 2=comma, 3=space>)
    mileage = num2str(mileageNum,0,2,2,0);
    print strfmt("The car has run %1 miles", mileage);
    pause;
}

将打印上一个作业 The car has run 388272,23 miles.

Boolean

布尔数据类型只是一个只能具有值0(false)或1(true)的整数的表示:

boolean isAvailable = true;

Date

Date数据类型显然包含日期值。日期的系统格式为dd\mm\yyyy:

date birthday = 29\01\2008

日期类型的变量可以包含从1\1\1900到31\12\2154的值,您可以使用整数对日期进行加法或减法运算。

要获取AX中设置的会话日期,可以使用systemdateget()函数。
AX中的会话日期自动设置为等于启动AX客户端时运行AX客户端的计算机上的计算机日期。您可以按Alt+F并转到“工具”|“会话日期”来更改会话日期。在打开的表单中,您可以更改会话日期和时间。这样做通常是为了发布日记账,就好像它是在与实际日期不同的日期发布的一样。

要检索本地机器的日期,请使用today()函数。

Enum

枚举数据类型在AOT中通过导航到数据字典|基本枚举来表示。您可以在代码中使用这些枚举作为文字列表,因为它比整数列表更便于阅读和理解。每个枚举最多可以有251个文字。

枚举中的第一个文字由整数0索引,下一个文字由1索引,依此类推;这些整数中的每一个都表示一个更容易理解的值,如下面的示例所示。在这里,您可以看到如何将枚举值分配给Weekdays类型的枚举变量

Weekdays day = Weekdays::Monday;

存储在变量day中的值将是整数1,因为枚举Weekdays也有一个表示枚举None的值0,但读取此代码要容易得多,而不是以下代码:

Weekdays day = 1;

enum2str函数

要获取一个特定枚举值的标签,只需使用enum2str函数,如下例所示:

static void Datatypes_enum2str(Args _args)
{
    SalesType salesType; // a standard enum in AX
    salesType = SalesType::Sales;
    info(strfmt("The name of the current sales type is '%1'", enum2str(salesType)));
}

此示例将显示如下屏幕截图所示的输出:

实际上,在前面的例子中,您不必使用enum2str,因为枚举值会自动转换为字符串,就像strfmt()函数中使用的那样。
但是,尝试直接在信息中使用枚举变量(如下面的行)会导致编译错误,因为您无法将枚举变量添加到字符串中。因此,在本例中,enum2str函数将派上用场:

info("The name of the current sales type is " + salesType);

请注意,我们在本例中使用了info方法,而不是print语句。Info方法是向最终用户显示信息的一种更用户友好的方式。它被定义为Global类中的一个静态方法,只需向AX信息日志中添加一条类型为info的新消息。可以放入信息日志的其他类型是错误和警告。使用error()和warning()而不是info()来尝试它们。

enum2int函数

与获取枚举值的标签相同,也可以使用enum2int函数获取它所表示的整数。以下示例显示了这一点:

static void Datatypes_enum2int(Args _args)
{
    SalesType salesType;
    salesType = SalesType::Sales;
    info(strfmt("The value of the current sales type element is %1",enum2int(salesType)));
}

info语句将打印以下输出:

timeofday数据类型

变量类型timeofday是一个整数,表示自午夜以来的秒数。它可以具有从0到86400的值,该值存储在数据库中。在报表或表单中使用时,会从凌晨12:00:00自动转换为值。至晚上11:59:59。

str2time函数

str2time函数将时间的字符串表示形式转换为timeofday值,只要字符串是有效时间即可。如果时间无效,函数将返回-1.这显示在以下代码中:

static void Datatypes_str2time(Args _arg)
{
    str timeStr;
    timeofday time;
    ;
    timeStr = "09:45";
    time = str2Time(timeStr);
    info(strfmt("%1 seconds have passed since midnight when the clock is %2", time, timeStr));
}

前面的示例将把以下输出打印到信息日志中:

35100 seconds have passed since midnight when the clock is 09:45

utcdatetime数据类型

utcdatetime数据类型包含日期、时间和时区信息(尽管时区部分不能在X++查询中使用)。

utcdatetime的一个非常好的地方是,您可以将一个值存储在utcdatetime变量中,并将其以本地时区和时间格式显示给世界各地的用户。

UTC是世界协调时的缩写。

anytype数据类型

anytype数据类型可以包含任何其他基元数据类型。变量数据的类型由为变量设置的第一个值决定。

在以下示例中,any变量将在运行时作为字符串变量工作,因为第一个值是字符串。由于第二个值是整数,它实际上在运行时被转换为字符串,因此在执行作业时print语句将显示33。代码如下:

static void Datatypes_anytype1(Args _args)
{
    anytype any;
    any = "test";
    any = 33;
    print any;
    pause;
}

像这样的数据类型肯定有问题,对吧?请考虑以下代码:

static void Datatypes_anytype2(Args _args)
{
    anytype any;
    any = systemdateget();
    any = "test";
    print any;
    pause;
}

编译器不会在这里给出任何错误,因为我们可以为变量分配任何值,就像我们使用anytype数据类型一样。但是,尝试将字符串值设置为日期类型的变量会产生运行时错误。

正如您可能理解的那样,您应该尽量避免使用anytype数据类型。anytype数据类型可能有用的唯一例外是可以将不同数据类型作为输入或输出的方法的参数或返回值。考虑一个例子,其中可以从不同的地方使用不同的输入调用方法。在一种情况下,将使用字符串调用该方法,而在另一种情况中,将使用整数调用该方法。您可以使用anytype数据类型作为方法的输入参数,并让方法检查传递给它的是整数还是字符串,而不是创建两个方法或一个具有两个不同输入参数的方法。

复合数据类型

复合数据类型是指每个变量可以有多个其他变量的数据类型。不同类型的复合数据类型包括:

• Container
• Class
• Table
• Array

Container

容器可以由混合在一起的任何基元数据类型的多个值组成。可以使用容器字段将容器作为字段存储在表中。

容器是不可变的,在容器中添加或删除值需要系统实际创建一个新容器,在新值之前和之后复制值,并删除删除的值。

Container的函数

在以下示例中,您可以看到一些容器函数,这些函数将用于将值插入容器、从容器中删除值、从容器读取值以及获取容器中的元素数。

检查Global类以查找其他方法。

static void Datatypes_container_functions(Args _args)
{
    container con;
    // conins - Insert values to the container
    con = conins(con, 1, "Toyota");
    con = conins(con, 2, 20);
    con = conins(con, 3, 2200.20);
    con = conins(con, 4, "BMW");
    con = conins(con, 5, 12);
    con = conins(con, 6, 3210.44);
    // condel - Delete the third and the fourth element
    // from the container
    con = condel(con, 3, 2);
    // conpeek - Read values from the container
    info(conpeek(con,1));
    info(conpeek(con,2));
    info(conpeek(con,3));
    info(conpeek(con,4));
    // connull - Reset the container
    con = connull();
    // conlen - Get the length of the container
    info(strfmt("Length: %1",conlen(con)));
}

Class

Dynamics AX中的类大多与其他语言中的类相似;它们是定义应用程序逻辑的蓝图。

创建类的对象时,还希望能够在使用类方法和存储在对象中的数据的整个范围内引用该对象。

请参阅以下示例,以获得有关如何从类创建对象以及类变量如何引用对象的提示:

static void Datatypes_class_variable(Args _args)
{
    // Declare a class variable from the RentalInfo class
    RentalInfo rentalInfo;
    // An object is created and referenced by the
    // class variable rentalInfo
    rentalInfo = new RentalInfo();
    // Call methods to set/get data to/from the object
    rentalInfo.setCar("BMW 320");
    info(strfmt("The car is a %1", rentalInfo.getCar()));
}

RentalInfo类的实现方式如下:

public class RentalInfo
{
    str car;
}
void setCar(str _car)
{
    car = _car;
}
str getCar()
{
    return car;
}

类和方法在下面的小节中有更详细的描述。

Table

通过创建表对象,可以在X++中使用表。这种将数据元素引用为对象的技术对于DynamicsAX来说是相当独特的(C#中的LINQ也有类似的概念)。

这意味着您可以轻松地直接对表数据进行插入、更新、删除、搜索和执行其他操作,而无需创建连接和语句。

在代码中,您可以简单地编写一个select语句,如以下示例所示:

static void Datatypes_table(Args _args)
{
    CustTable custTable; // This is the table variable
    CustAccount custAccount;
    custAccount = "1000";
    select Name from custTable
    where custTable.AccountNum == custAccount;
    info(strfmt("The name of the customer with AccountNum %1 is %2",
    custAccount, custTable.Name));
}

数据字典中的所有表实际上都是扩展系统类公共的类。它们可以被视为数据库中相应表的包装器,因为它们具有读取、创建、修改和删除存储在它们包装的表中的数据的所有必要功能。此外,开发人员当然可以添加比公共类继承的方法更多的方法。在下面的小节中,您可以找到一些常用的表方法。

find方法

所有表都应该至少有一个find方法,该方法从表中选择并返回一条与输入参数指定的唯一索引匹配的记录。find方法中的最后一个输入参数应该是一个名为forupdate或update的布尔变量,默认值为false。当它设置为true时,调用方对象可以更新find方法返回的记录。

请参阅InventTable类中的以下示例:

static InventTable find(ItemId itemId,boolean update = false)
{
    InventTable inventTable;
    inventTable.selectForUpdate(update);
    if (itemId)
    {
        select firstonly inventTable
        index hint ItemIdx
        where inventTable.ItemId == itemId;
    }
    return inventTable;
}

exist方法

与find方法一样,也应该存在一个exist方法。它基本上与find方法相同,只是在找到具有由输入参数指定的唯一索引的记录时返回true。

在以下示例中,同样从InventTable类中,您可以看到,如果输入参数有值,select语句返回值,则exist方法返回true:

static boolean exist(ItemId itemId)
{
    return itemId && (select RecId from inventTable index hint ItemIdx
    where inventTable.ItemId == itemId
    ).RecId != 0;
}

initFrom方法

相互关联的表共享构成关系的数据以及其他信息。在一对多关系中的多个表中创建新记录时,可以创建initFrom方法来设置表中的公共字段。

initFrom方法还可以用于默认来自另一个表的表中字段中的值。

以下示例取自BOMTable类,显示了它如何从InventTable类中的相应字段启动BOMTable中的ItemId和ItemGroupId字段:

void initFromInventTable(InventTable table)
{
    this.bomId = table.ItemId;
    this.ItemGroupId = table.ItemGroupId;
}

Array

数组是所有类型相同的值的列表,而容器可以由不同类型的值组成。值列表从元素1开始。
如果将值设置为数组的索引号0,则会重置该数组。

使用数组有两种不同的方法。如果您知道数组中最多有多少个元素,则可以使用固定长度的数组。如果您不知道在运行时数组中可以存储多少元素,可以使用动态数组。

除此之外,您还可以指定一种称为部分磁盘阵列的东西,它指定引用阵列时应将多少元素加载到内存中。如果您有包含大量数据的阵列,这可能是一个很好的性能优化。

以下示例解释了数组的不同用途:

static void Datatypes_array(Args _args)
{
    // Fixed length array
    str licenceNumber[10];
    // Fixed length array partly on disk.
    // 200 elements will be read into memory when this array
    // is accessed.
    int serviceMilage[1000,200];
    // Dynamic array
    str customers[];
    // Dynamic length array partly on disk.
    // 50 elements will be read into memory when this array
    // is accessed.
    Amount prices[,50];
}

语句和循环

语句和循环用于控制程序的执行流,如果没有它们,大多数编程任务都是不可能的。

在AX中,您可以访问以下语句和循环:

• for循环
• continue语句
• break语句
• while循环
• do-while循环
• if-else if-else循环
• switch语句

for循环

如果代码在运行时知道在循环开始之前应该循环一段代码多少次,则可以使用for循环。AX中for循环的伪代码与从C编程语言派生的大多数编程语言中的任何for循环相同。它实际上被称为循环的三个表达式,因为它由三个步骤组成:

1.初始化用于递增for循环的变量。

2.测试语句的初始化。

3.设置变量的增量值或减量值。

如下面的示例所示,变量i被初始化为1,然后它告诉for循环保持循环,只要i小于或等于10。然后,它为每个循环将i递增一:

int i;
for (i=1; i <= 10; i++)
{
    info (strfmt("This is the %1 Toyota", i));
}

continue语句

如果希望代码直接跳转到代码的下一次迭代,则可以在任何循环中使用continue语句。

break语句

break语句可以用于跳出循环,即使根据循环条件需要执行更多迭代。

在前面的示例中,使用了info函数,而不是Hello World示例中的print函数。信息功能更方便,从用户的角度来看也更好。打印窗口只能用于在执行测试作业或类似操作时向开发人员显示消息。

while循环

您可以使用while循环多次执行一段代码,直到满足某个条件。只有满足while条件时,才会执行while循环。
以下是一个示例:

int carsAvailable=10;
while (carsAvailable != 0)
{
    info (strfmt("Available cars at the moment is %1",carsAvailable));
    carsAvailable --;
}

do while循环

do-while循环与while循环几乎相同,只是即使不满足while条件,它也会执行一次。在do-while循环中,循环至少执行一次,因为while表达式是在第一次迭代后求值的。以下是一个示例:

int carsAvailable=10;
do
{
    info (strfmt("Available cars at the moment is %1",carsAvailable));
} while (carsAvailable != 0);

if-else if-else循环

if-else if-else循环的执行流程如下:

1.if语句检查语句中使用的条件是否返回true。如果是,系统将执行If语句的主体。

2.如果If语句返回false,则检查else-If语句内的条件是否返回true。如果是,系统将执行else-If语句的主体。

3.最后一个else语句可以直接与if语句一起使用,也可以在else-if语句之后使用。只有当if语句和所有else-if语句返回false时,系统才会执行最后一个else语句内部的正文。

以下是if-else-if-else循环的示例:

if (carGroup == CarGroup::Economy)
{
    info("Kia Picanto");
}
else if (carGroup == CarGroup::Compact)
{
    info("Toyota Auris");
}
else if (carGroup == CarGroup::MidSize)
{
    info("Toyota Rav4");
}
else if (carGroup == CarGroup::Luxury
{
    info("BMW 520");
}
else
{
    info("Standard cars");
}

当然,您可以添加任意多的其他if语句,也可以将它们嵌套在if语句的主体中,使另一个if语句存在,但这可能不是完成任务的最佳方式。相反,您应该考虑使用switch语句。原因是必须为每个if和else-if语句计算条件,而switch语句只计算一次条件,然后找到正确的命中。

switch语句

switch语句在功能上类似于if语句;然而,switch语句语法使代码更加易读。请注意,您必须在每个案例的底部使用break语句。如果不使用,系统也将继续执行下一个案例。

default语句可以以与else语句类似的方式使用,表示如果其他情况都不包含正确的值,则执行default语句。
以下是一个示例:

switch (carGroup)
{
    case CarGroup::Economy :
        info("Kia Picanto");
        break;
    case CarGroup::Compact :
        info("Toyota Auris");
        break;
    case CarGroup::MidSize :
        info("Toyota Rav4");
        break;
    case CarGroup::Luxury :
        info("BMW 520");
        break;
    default
        info("Standard cars");
        break;
}

异常处理

作为一名开发人员,预料到意想不到的事情总是很重要的。确保程序能够处理异常情况的一种方法是使用异常处理。在AX中,这意味着使用以下语句:try、catch、throw和retry。

try和catch语句应该始终放在一起。使用try语句而不使用catch语句,反之亦然,将导致编译器错误。

当您使用try语句时,您表示无论try块中有什么代码,它都可能会生成应该处理的异常情况。
这种情况在catch块中通过指定catch块处理的异常类型来处理。以下示例显示如何捕获错误异常和死锁异常。在本例中永远不会出现死锁,但这里只是向您展示如何使用retry语句:

static void ExceptionHandling(Args _args)
{
    try
    {
        // Do something
        info("Now I'm here");
        // A situation that causes an error occur and you would
        // like to stop the execution flow
        if (true)
        throw error("Oops! Something happened");
        info("Now I'm there");
    }
    catch (Exception::Error)
    {
        // Handle the error exception
        info ("I would like to inform you that an error occurred");
    }
    catch (Exception::Deadlock)
    {
        // Handle the deadlock exception
        // Wait for 10 seconds and try again
        sleep(10000);
        retry;
    }
    info ("This is the end");
}

操作符

运算符用于操作或检查变量的值。在开发AX时,我们使用三种不同类型的运算符:

• 分配操作符
• 关系运算符
• 算术运算符

分配操作符(赋值操作符)

为了给变量赋值,必须在X++中使用赋值运算符。当然,最明显的运算符是=运算符,它将把=运算符右边的值赋给左边的变量。这意味着下例中的汽车变量将被赋值为BMW

str car = "BMW";

其他赋值运算符包括++、--、+=和-=,如果您一直在用C#、C++、Java等进行编程,您就已经知道了。有关解释,请参阅以下示例:

static void AssignmentOperators(Args args)
{
    int carsInStock = 10;
    carsInStock++; // carsInStock is now 11
    carsInStock+=2; // carsInStock is now 13
    carsInStock--; // carsInStock is now 12
    carsInStock-=3; // carsInStock is now 9
}

增量和减量运算符也可以是前缀运算符,而不是后缀运算符,如前面的示例所示,但它们在X++中的用法没有区别,最好将它们用作后缀运算符。

关系操作符

正如您已经在循环和if语句中看到的那样,两个常用的关系运算符是==和!=。这些用于确定变量是否等于其右侧的值。

这意味着在返回true或false的语句中使用关系运算符。让我们看看我们可以使用的各种关系运算符:

• 如果两个表达式相等,则==运算符返回true

• !=如果第一个表达式不等于第二个表达式,则运算符返回true

• 如果第一个表达式和第二个表达式为true,则&&运算符返回true

• 如果第一个表达式、第二个表达式或两者都为true,则||运算符返回true

• !如果表达式为false(只能与一个表达式一起使用),则运算符返回true

• 如果第一个表达式大于或等于第二个表达式,则>=运算符返回true

• 如果第一个表达式小于或等于第二个表达式,则<=运算符返回true

• 如果第一个表达式大于第二个表达式,则>运算符返回true

• 如果第一个表达式小于第二个表达式,则<运算符返回true

• 最后一个关系运算符是like运算符,在以下示例中对此进行了解释:

static void RelationalOperatorLike(Args _args)
{
    str carBrand = "Toyota";
    // use the * wildcard for zero or more characters
    if (carBrand like "Toy*")
    {
        info ("The car brand starts with 'Toy'");
    }
    // use the ? wildcard for one character
    if (carBrand like "Toy??a")
    {
        info ("The car brand starts with 'Toy' and the lastcharacter is 'a'");
    }
}

算术运算符

最后一种类型的运算符是算术运算符。这些用于对变量进行数学计算,如乘法、除法、加法、减法以及二进制运算。

由于二进制算术很少在标准AX中使用,这里不会详细介绍它,但您可以在SDK中了解更多信息。

以下代码显示算术运算符的示例:

static void ArithmeticOperators(Args _args)
{
    int x, y;
    x = 10;
    y = 5;
    info(strfmt("Addition: %1 + %2 = %3", x, y, x+y));
    info(strfmt("Subtraction: %1 - %2 = %3", x, y, x-y));
    info(strfmt("Multiplication: %1 * %2 = %3", x, y, x*y));
    info(strfmt("Division: %1 / %2 = %3", x, y, x/y));
}

这将把以下输出打印到信息日志中:

类和方法

面向对象编程语言的核心组件之一是类。类是对象的蓝图,用于定义特定类型的对象可以做什么以及它具有什么类型的属性。

AX中的类与C#中的类相似,但您没有在类的花括号内指定类的方法;相反,在AOT中的类下为每个方法创建新的节点。


类声明用于保存类中全局的变量。相对于私有和公共变量,这些变量始终受到保护。这意味着该类的对象或子类的对象可以直接访问这些变量。

在开发过程中,您编写类,并且这些类在运行时被实例化为对象。因此,如果Car是一个定义汽车特征和行为的类,那么许可证号为DE2223ED的特定汽车就是Car类的对象(实例)。

方法访问

可以使用访问修饰符或关键字(如public、protected和private)来控制一个类中的方法是否可以调用另一个类的方法。可以通过这样一种方式构建类,即可以隐藏执行基本任务且仅在类中使用的方法。这使得类使用起来更安全,因为只有故意公开的方法才能被外部类调用。这个概念被称为封装,是面向对象编程的一个关键原则。

封装可确保对象保护和处理自己的信息。只能通过安全的方法更改对象的状态。这是通过在方法定义的开头使用访问修饰符来完成的:

• Public:这些方法可以从任何可访问类的方法中使用,也可以被扩展父类(子类)的类覆盖。如果代码中没有显式写入访问修饰符,编译器将假定它们是公共的,并相应地处理它们。

• Protected:这些方法可以从同一类或其子类中的任何方法中使用。

• Private :这些方法只能从同一类中的任何方法中使用。

RunOn属性

AX中的代码可以在客户端或服务器上运行。此处的服务器将是AOS服务器。有两种方法可以控制某个类的对象应该在哪里执行。在开发静态方法时,可以选择在方法头中使用客户端或服务器修饰符,如下例所示:

// This method will execute on the client.
static client void clientMethod()
{
}
// This method will execute on the server.
static server void serverMethod()
{
}

还可以设置某个类的RunOn属性,使该类的所有对象都在客户端或服务器上运行。还可以将类的RunOn属性设置为CalledFrom。这将使该类的对象与创建对象的方法在同一层上执行。

确保方法在服务器或客户端上运行的一个示例是尝试从磁盘导入文件。当引用固定的文件位置时,当代码在服务器上执行时,C驱动器显然是服务器的C驱动器,而当方法在客户端上执行时则是客户端的C驱动器。

Static方法

在AX中,您将看到一些方法有一个名为static的方法修饰符。事实上,到目前为止,我们看到的所有作业都使用了静态修饰符。这意味着可以在不创建对象的情况下直接调用该方法。当然,这也意味着静态类不能访问在类的类声明中定义的受保护数据。

访问修饰符用于方法定义的开头,紧接在方法访问修饰符之后和返回值之前,如以下示例所示:

public static void main(Args args)
{
}

要调用静态方法,请使用双冒号,而不是像在对象方法中那样使用句点:

SomeClass::staticMethod()

默认参数

要为方法中的输入参数设置默认值,只需添加=<默认值>,如下例所示:

void defaultParameterExample(boolean test=true)

在本例中,如果在调用方法时未给定任何参数,则变量test将默认为true。这也意味着该参数是可选的。如果在参数设置为false的情况下调用该方法,则执行该方法时该参数将为false。
但是,如果不使用任何参数调用,测试变量将自动设置为true。

Args类

Args类在AX中广泛使用,以便为执行方法创建指向调用方对象的指针。

它在窗体、查询和报表中用作构造函数中的第一个参数。这意味着,对于任何窗体、查询和报表,都可以使用Args()方法获取已传递给它的Args类。Args类还用于通过使用以下方法传递附加信息:

• record:这将从表中传递一条记录
• parm:这将传递一个字符串值
• parmEnum(以及parmEnumType):这将传递一个枚举值
• parmObject:这将传递任何类型的对象

此示例演示了从另一个元素中的方法调用类中的主方法时使用Args的效果。RentalInfoCaller是调用RentalInfo类的主要方法的调用类:

public class RentalInfoCaller
{
}
// The main method here is just used
// to be able to execute the class
// directly and show the example.
public static void main(Args args)
{
    RentalInfoCaller rentalInfoCaller;
    rentalInfoCaller = new RentalInfoCaller();
    rentalInfoCaller.callRentalInfo();
}
// This is the class that calls the
// main method if the RentalInfo class
void callRentalInfo()
{
    Args args;
    args = new Args(this);
    RentalInfo::main(args);
}
// This method is just used as
// a proof that you are able
// to call methods in the calling
// object from the "destination" object
void callBackMethod()
{
    info ("This is the callBackMethod in an object of the RentalInfoCaller class");
}

AX有一个名为的对当前对象的内置引用,该引用在前面的Args(this)内部的示例中使用。

以下代码显示了RentalInfo类的主要方法:

public static void main(Args args)
{
    RentalInfoCaller rentalInfoCaller;
    rentalInfoCaller = args.caller();
    rentalInfoCaller.callBackMethod();
}

当执行RentalInfoCaller类时,程序将在主方法中启动,并创建RentalInfoCaller的新对象,然后调用callRentalInfo方法。然后,callRentalInfo方法将创建一个新的args对象,并将rentalInfoCaller对象作为参数执行。这可以通过将rentalInfoCaller对象作为构造函数中的第一个参数传递,也可以在创建args对象后调用args.caller()方法来实现。然后,使用新创建的args对象作为参数来调用RentalInfo类的主方法。

当RentalInfo类中的主方法启动时,它使用args.caller()方法来获取对调用RentalInfo的对象的引用。这将启用对调用方对象的回调并执行其方法。

因此,当执行RentalInfoCaller类时,以下结果将打印在信息窗口中:

继承

面向对象编程的核心概念之一是继承系统中更高级别定义的功能的可能性。这可以通过类层次结构来实现,其中子类中的方法覆盖超类(更高级别)中的方法。子类中的方法仍然可以通过使用超级函数来使用超类中相同方法的功能,如下例所示:

public void sellCar()
{
    super();
}

此方法意味着该方法所属的当前类扩展了sellCar方法中的功能已经写入的另一个类,或者sellCar方法是在当前类所属的类层次结构中的更高级别上定义的。

在我们的例子中,我们有一个名为Car的超类,它扩展了Object,我们有扩展Car类的Car_Economy、Car_Compact、Car_MidSize和Car_Luxury类。我们还重写了所有这些类的toString方法。Car_Economy类的示例如下所示:

public str toString()
{
    str ret;
    ret = "Economy";
    return ret;
}

为了确保所有子类都有关于超类的更新信息,最好在修改类层次结构时向前编译。这是通过右键单击超类,选择Add-Ins,然后单击CompileForward来完成的。要查看类可以从父类使用的方法,只需右键单击AOT中的类并选择Override方法。您将获得一个可以在此类中重写的方法列表,如以下屏幕截图所示:


继承的示例:

class Point
    {
        // Instance fields.
        real x; 
        real y; 
        ;
     
        // Constructor to initialize fields x and y.
        new(real _x, real _y)
        { 
            x = _x;
            y = _y;
        }
    }
     
    class ThreePoint extends Point
    {
        // Additional instance fields z. Fields x and y are inherited.
        real z; 
        ;
    
        // Constructor is overridden to initialize z.
        new(real _x, real _y, real _z)
        {
            // Initialize the fields.
            super(_x, _y); 
            z = _z;
        }
    }

构造方法

正如在本章前面所看到的,您可以通过使用保留词new来创建类的对象。AX将对象的创建提升到了一个新的层次,并创建了一个名为construct的方法。从construct方法创建所有对象是最佳实践之一。

construct方法应该考虑发送给它的任何参数,并基于这些参数创建并返回正确的对象。我在这里说正确的对象的原因是,超类应该有一个构造方法,可以为其所有子类创建对象,并根据发送给它的参数创建正确的对象。

构造方法应始终是静态的、公共的和命名的构造,如以下示例所示:

public static Car construct(CarGroup carGroup)
{
    Car car;
    switch (carGroup)
    {
        case CarGroup::Economy :
            car = new Car_Economy();
            break;
        case CarGroup::Compact :
            car = new Car_Compact();
            break;
        case CarGroup::MidSize :
            car = new Car_MidSize();
            break;
        case CarGroup::Luxury :
            car = new Car_Luxury();
            break;
    }
    return car;
}

main方法

应该能够从使用菜单项(窗体中的按钮或菜单中的元素)开始的类需要一个起点。

在AX中,这是通过名为main的静态方法实现的。main方法总是采用一个Args类型的参数。这样做是为了能够判断主方法是从哪里调用的。一个例子可能是在窗体中调用一个类,并且您需要知道用户在按下按钮时选择了哪个记录。这些信息可以传递给args对象中的类。

main方法的任务是通过调用构造方法(或者在某些不存在构造方法的情况下,调用新方法),在需要时提示用户提供信息(提示方法),然后调用控制类流的方法(通常为run方法)来创建类的实例。

这是取自标准AX类CustExchAdj的main方法的典型示例:

public static void main(Args args)
{
    CustExchAdj custExchAdj = new CustExchAdj();
    if (custExchAdj.prompt())
    {
        custExchAdj.run();
    }
}

RunBase框架

每当您编写一个类,该类通常要求用户输入,然后根据该输入启动进程时,都会使用RunBase框架。这是因为此类作业所需的大多数典型方法已经在RunBase类中实现。

RunBase框架可通过直接扩展RunBase或通过扩展扩展RunBase的类来使用。

RunBase框架中实现的一些功能如下:

• Run:此功能遵循操作的主要流程
• Dialog:此功能提示用户输入并存储该输入
• Batch execution:此功能安排作业以供以后执行
• Query:此功能用于选择数据
• Progress bar:此功能用于查看当前操作的进度
• Pack/unpack with versioning:此功能用于存储变量的状态(以便用户打开表单时可以恢复以前选择的选项)

查看tutorial_RunbaseForm,了解如何扩展RunBaseBatch类以及如何使用前面的所有功能的示例。

SysOperation框架

在很高的级别上,SysOperation框架可能看起来与RunBase框架相似,即SysOperation框架允许代码以批处理模式执行,也可以通过对话框进行查询/参数化。然而,SysOperation框架完全不同。SysOperation基于Windows Communication Foundation(WCF),使用类似于模型-视图-控制器(MVC)的开发模式。

使用SysOperation框架编写的代码可以作为(web)服务公开,并可以在中执行。NET公共语言运行时(CLR)环境(因此性能明显优于X++(RunBase框架代码))。正如人们所期望的,SysOperation框架是Dynamics AX 2012中开发基于批处理的操作的推荐方法。

SysOperation框架可以根据需要简单或复杂,例如,如果开发人员只是想开发一个可以在批处理模式下执行的函数,则不需要设计复杂的对话框。

让我们开发一个简单的批处理。在本例中,我们将创建一个批处理过程,该过程循环浏览客户呼叫记录,并记录那些将统计组设置为Car(或指定为参数的任何其他值)的记录。

首先,我们需要一份合同。数据协定是一个有效的类,它包含将在批处理对话框中显示的字段(称为数据成员)。

数据契约类必须具有DataContractAttribute属性,成员函数应具有DataMemberAttribute属性:

[DataContractAttribute]
public class CarDataContract
{
    CustAccount custaccount;
    //NoYes is Enum Type
    NoYes selectQuery;
    str salesQuery;
}
[DataMemberAttribute,SysOperationLabelAttribute(literalStr(@SYS96450))
,AifQueryTypeAttribute('_salesQuery', querystr(SalesTableSelect))
]
public str parmQuery(str _salesQuery = salesQuery)
{
    salesQuery = _salesQuery;
    return salesQuery;
}

类的层次结构如以下屏幕截图所示:

请注意前面屏幕截图中的SysOperationLabelAttribute属性。Dynamics AX 2012使开发人员能够使用其他属性装饰成员函数。这些属性有效地充当表字段上的元数据,例如,它们可以用于更改对话框上显示的标签。以下是成员函数上使用的一些关键属性:

• SysOperationLabelAttribute:此属性用于指定应由成员函数表示的字段的对话框上应显示的标签
• SysOperationHelpTextAttribute:此属性用于显示与成员函数链接的字段的帮助文本
• AifQueryTypeAttribute:如果成员函数表示查询,则使用此属性

接下来,我们将创建一个服务,它实际上是批处理过程将调用的最后一个函数。

我们将创建一个名为checkCustomer的方法,并使用属性SysEntryPointAttribute对其进行装饰。此属性表示函数是一个服务操作。我们还将把合同作为参数传递。代码如下:

[SysEntryPointAttribute]
public void checkCustomer(CarDataContract _carDataContract)
{
    CustTable custtable;
    QueryRun salesQueryRun;
    if(_carDataContract.parmSelectQuery())
    {
        salesQueryRun = new QueryRun(_carDataContract.parmQuery());
        while(salesQueryRun.next())
        {
            custtable = salesQueryRun.get(tableNum(CustTable));
            if(custtable.StatisticsGroup == "CAR")
            info(custtable.AccountNum);
        }
    }
    else
    {
        select custtable where custtable.AccountNum == _
        carDataContract.parmCustAccount();
        if(custtable.StatisticsGroup == "CAR")
            info("Yes");
    }
}

要点:

  • 请确保参数签名如下所示:_dataContract。因此,在我们的例子中_CarDataContract。
  • 确保类属性设置为在服务器上执行。

现在我们有两种选择:

  • 我们可以将前面的操作直接添加到服务中,这使我们能够快速部署批处理功能。
  • 我们可以创建一个自定义控制器类(通过扩展SysOperationServiceController),这将给我们更多的灵活性。例如,我们可能希望从供应商表单调用我们的服务,并在查询中预先填充供应商帐户。

让我们运行第一个选项。

使用SysOperationServiceController的标准实例,我们将创建一个新服务:

1. 创建一个新的服务,并将其称为CarDataService。
2. 将类属性更改为CarDataService。
3. 右键单击“操作”节点,然后选择“添加”操作。
4. 添加checkVendor操作。

现在,我们将添加一个菜单项,步骤如下:

1. 创建一个新的操作菜单项,并将其命名为CarDataService。
2. 将ObjectType属性设置为Class。
3. 将Object属性设置为SysOperationServiceController。
4. 将Parameter属性设置为CarDataService.checkCustomer。请注意,下拉列表不可用。

在运行菜单项之前,我们必须用公共中间语言(CIL)编译代码。

集合类

除了我们在本章前面介绍的复合数据类型之外,还有一些类可以用来以给定的方式存储多个值甚至对象。

这些类被称为集合类,以前被称为基础类。它们是系统类,所以不能更改它们;但是,您可以扩展它们来创建自己的集合类。以下集合类位于标准AX中:

•  Array
•  List
•  Map
•  Set
•  Struct

Array

我知道这有点令人困惑,因为还有一种称为array的复合数据类型,但array的集合类有点不同,正如您在下面的示例中看到的那样。它还可以存储数组数据类型无法存储的对象。示例代码如下:

static void Collection_Array(Args _args)
{
    // Create a new Array where the
    // elements are objects
    Array cars = new Array(Types::Class);
    Car car;
    int i;
    // Set new elements to the car Array
    // at the given index positions
    cars.value(1, new Car_Economy());
    cars.value(2, new Car_Luxury());
    cars.value(3, new Car_Compact());
    // Display the content of the Array
    info (cars.toString());
    // Loop through the Array to display
    // each element
    for (i=1; i<=cars.lastIndex(); i++)
    {
        car = cars.value(i);
        info(strfmt("Class: %1", car.toString()));
    }
}

List

列表包含按顺序访问的一种给定类型的元素。从下面的例子中可以看出,您可以随时使用addStart或addEnd来存储元素。但是,当您使用ListEnumerator循环浏览列表时,将按正确的顺序访问元素。示例代码如下:

static void Collection_List(Args _args)
{
    // Create a new list of type string
    List names = new List(Types::String);
    ListEnumerator listE;
    // Add elements to the list
    names.addEnd("Lucas");
    names.addEnd("Jennifer");
    names.addStart("Peter");
    // Display the content of the list
    info (names.toString());
    // Get the enumerator of the list
    // to loop through it
    listE = names.getEnumerator();
    while (listE.moveNext())
    {
        info (strfmt("Name: %1", listE.current()));
    }
}

Map

映射用于使用键值对值进行索引。键和值都可以是Types枚举中指定的任何类型。示例代码如下:

static void Collection_Map(Args _args)
{
    // Create a new map with a key and value type
    Map cars = new Map(Types::Integer, Types::String);
    MapEnumerator mapE;
    // Insert values to the map
    cars.insert (1, "Volvo");
    cars.insert (2, "BMW");
    cars.insert (3, "Chrysler");
    // Display the content of the map
    info (cars.toString());
    // Get the enumerator to loop
    // through the elements of the map
    mapE = cars.getEnumerator();
    while (mapE.moveNext())
    {
    info(strfmt("Car %1: %2", mapE.currentKey(), mapE.
    currentValue()));
    }
}

Set

一个集合可以包含types枚举的有效类型之一。集合中的值是唯一的,它们会自动排序。执行以下示例后可以看到,这些值将按以下顺序存储:Ford、Mazda和Toyota。示例代码如下:

static void Collection_Set(Args _args)
{
    // Create a new set of type String
    Set cars = new Set(Types::String);
    SetEnumerator setE;
    // Add elements to the set
    cars.add("Toyota");
    cars.add("Ford");
    cars.add("Mazda");
    // Check to see if an element
    // exists in the set
    if (cars.in("Toyota"))
        info ("Toyota is part of the set");
    // Display the content of the set
    info (cars.toString());
    // Get the enumerator of the set
    // to loop through it
    setE = cars.getEnumerator();
    while (setE.moveNext())
    {
        info(setE.current());
    }
}

Struct

结构可以被视为只有属性而没有方法的类。它可以存储不同数据类型的多个值,但一个结构只能容纳一组值。以下是一个示例:

static void Collection_Struct(Args _args)
{
    // Create a struct with two fields
    Struct myCar = new struct ("int ModelYear; str Carbrand");
    int i;
    // Set values to the fields
    myCar.value("ModelYear", 2000);
    myCar.value("Carbrand", "BMW");
    // Add a new field and give it a value
    myCar.add("Model", "316");
    // Loop through the fields of the struct
    for (i=1; i<=myCar.fields(); i++)
    {
        info(strfmt("FieldType: %1, FieldName: %2, Value: %3",
        myCar.fieldType(i),
        myCar.fieldName(i),
        myCar.value(myCar.fieldName(i))));
    }
}

Macros(宏)

宏是编译器在代码的其余部分之前处理的常量或代码段,目的是用宏的内容替换使用宏的代码。有三种不同类型的宏:独立宏、本地宏和宏库。

宏通常是只有开发人员才能更改的常数值。使用它们是为了让开发人员不必在X++代码中对这些值进行硬编码。

宏库可以在AOT的“Macors”下找到。每个宏都可以包含多个宏,这些宏可以在AX的其余部分中使用。

要在AX中使用宏库中的宏,只需将它们包含在要使用的范围中即可。以下示例显示如何在作业中使用同一宏库中的两个不同宏。

首先,我们创建一个由两个宏组成的宏库:

#define.Text('This is a test of macros')
#define.Number(200)

然后,我们在作业中使用这些宏:

static void Datatypes_macro_library(Args _args)
{
    // Referencing macro library has to be done in the class declaration
    // or in the declaration like in this example
    #MacroTest
    info(strfmt("Text: %1. Number: %2", #Text, #Number));
}

此作业将在信息日志中打印以下输出:

局部宏是在类的类声明或方法的变量声明中定义的,如以下示例所示:

static void Local_Macro(Args _args)
{
    // Define the local macro
    #localmacro.WelcomeMessage
    {
        info("Welcome to Carz Inc.");
        info("We have the best offers for rental cars");
    }
    #endmacro;
    // Use the local macro
    #WelcomeMessage
}

独立宏与宏库中使用的宏相同,不同之处在于它们是在方法的一个变量的声明中或在类的类声明中定义的。

Events

事件可能是Dynamics AX 2012中引入的最强大的技术功能之一。在以前的版本(以及Dynamics AX 2012)中,当开发人员更改任何标准代码(较低层的代码)时,会在开发人员使用的层中创建代码(方法)的副本,从而可以在不删除或覆盖Microsoft开发的任何代码的情况下扩展应用程序。但是,如果应用程序在项目中进行了显著扩展,则很难将代码与Dynamics AX的未来版本(或service Pack/修复程序)合并。事件有助于解决这个问题,也就是说,它们允许扩展应用程序,但很容易升级到未来的版本。

关键术语
• 事件:程序模块中的一个关键事件,其中其他模块必须处理该事件
• 事件处理程序:订阅事件的方法

在AOT中,可以将一个方法指定为另一个方法的方法前事件或方法后事件的事件处理程序。在任何一种情况下,我们都说事件处理程序订阅了宿主方法的事件。

方法节点下面的事件处理程序可以在方法运行之前或之后运行。可以通过在事件处理程序节点上设置CalledWhen属性来控制时间。CalledWhen属性具有以下选项:

• Pre:事件处理程序在方法之前运行
• Post:事件处理程序在方法结束后运行

事件处理程序方法可以使用XpPrePostArgs对象操作参数(预处理程序)和/或返回对象(后处理程序)。

总结

在本章中,了解了AX编程的基本构建块。可用于存储数据的不同数据类型,还看到了可用于操作变量的一些函数。

还了解了如何使用条件语句和循环来控制代码流,以及如何使用运算符来分析、分配和操作数据。

在解释类和方法如何工作的部分中,了解了RunBase框架,还了解了如何创建类的对象、继承如何在AX中工作以及静态方法如何工作。

在本章的最后,看到了如何使用不同类型的宏来替换X++代码。

在下一章中,将讨论如何创建表、字段和关系,以便获得第三种范式的数据模型,该模型可以将数据存储在AX数据库中。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Dynamics 365是微软提供的一款综合性企业管理解决方案,可帮助企业管理客户关系、销售、服务、运营、财务等多个方面的业务。而Contract Controller Service是Dynamics 365中的一个模块或服务,用于合同管理和控制。 Contract Controller Service提供了一套完整的合同管理流程,从合同创建、审批、签订到执行和结束,全程协助企业进行合同管理和控制。该服务可以帮助企业提高合同管理的效率和准确性,降低合同风险和纠纷的发生。 在Dynamics 365中使用Contract Controller Service可以实现以下功能: 1. 合同创建:在系统中创建新合同,包括输入合同相关信息和条款,确保数据的准确性和完整性。 2. 合同审批:将合同提交给相关部门或人员进行审批,系统可以自动跟踪审批流程,提醒相关人员审批进度。 3. 合同签署:支持电子签名和合同文件的上传,方便各方快速签署合同,减少纸质文件的使用和储存。 4. 合同执行:合同签署后,系统可以提醒相关人员履行合同,并跟踪合同执行的进度和结果。 5. 合同结束:合同有效期结束后,系统可以自动归档合同文件,方便日后查询和备案。 通过使用Dynamics 365中的Contract Controller Service,企业可以实现合同管理的标准化和自动化,提高工作效率,减少人为错误和纰漏,同时降低合同管理的风险和成本。服务的使用方便灵活,适用于各种企业类型和规模。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Martin-Mei

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值