最近在MSDN上发现了两篇文章介绍RunBase的用法,文章作者介绍的内容平时做开发的应该都耳熟能详了,RunBase的源代码也可以查看,所以倒也没什么奥秘可言,不过还是很欣赏老外写文章的风格.
废话少说啦,转入正题.这个老外写这两篇文章可谓旷日持久,第一篇2005年7月就搞定了,第二篇到今年9月才出来......
原文地址:
http://msdn.microsoft.com/library/en-us/dnaxap/html/axp_perprocessingp1.asp
Axapta中的周期性处理 第一部分
(译注:原文的标题是Periodic Processing in Axapta, Part I,文章的内容主要是介绍了RunBase这个类的应用,我想作者之所以取这个名字作为标题,是因为Axapta中Periodic部分中的内容,大部分都是通过RunBase这个类的子类去实现的,这部分功能有个共同点是界面相对简单,不是每天都要用到的功能而是周期性操作的动作,于是通过RunBase来提供一些通用的功能,不同于日记帐,每个模块都有自己特性化的东西,界面相对复杂)
Patrick G. Hodo
Chief Technical Officer
Red Maple Press, Inc.
Red Maple Technologies, Inc.
应用于:
微软商务解决方案---Axapta
2005年7月
概要:这篇文章描述了在微软商务解决方案---Axapta中如何设置周期性(基于Job)处理(这篇文章是两篇关于此系列文章的第一篇)(15个打印页).
读者应该是初级或者中级水平的X++程序员(译注:其实不一定需要了解X++的,只要用过一门OO语言就行了,比如Java,C#等),熟悉RunBase和RunBaseBatch类.程序员(通常情况下)需要受过X++培训并且熟悉一些概念,比如继承和面向对象编程OOP(译注:其实OOP本身包含继承,作者可能是要强调继承于是单独提出来).这些有所帮助但也不是必需的(译注:我倒感觉应该了解一些OOP才行,要不然还真云里雾里的).假定用户了解微软商务解决方案---Axapta.
下载示例代码:axp_PerProcessingP1.exe
内容
概述
基于Job的处理概览
Axapta中的周期性处理
结论
概述
这篇文章展示了在微软商务解决方案---Axapta中如何设置周期性(基于Job)处理.可以通过期间菜单项(译注:Axapta Periodic文件夹中的菜单项)调用自定义的类来实现,这些自定义类使用RunBase和RunBaseBatch类(译注:实际上自定义的类是继承自RunBase或者RunBaseBatch,而不是使用,这里作者的意思应该也是如此,也不较真了).当然也会对这些类型的类做主要的介绍.
可以下载空的模板(blankperiodic.xpo)来帮助创建周期类,下面的步骤和模板将帮助创建操作数据和运行报表的周期性处理.
第二篇文章将介绍关于周期性处理的更高级的主题.它将介绍使用RunBase和RunBaseBatch的细节并且揭示更多的高级特性.
注意:要使用示例代码,必须启用"Commissions"这个配置键.这里用英语表示这个字符串而不是标签的ID,(比如用"Commissions"而不是"@SYS71025")使代码更具可读性.
基于Job的处理概览
在AOT中编程,需要写周期性处理过程或者创建复杂的报表,在过去写代码的时候,可能用过两种方式,从其他周期菜单中拷贝一些功能或者使用接近该功能的窗体来凑活一下.这样做在开始的时候可以奏效,但会遇到问题,首先,拷贝类而不知道其底层的方法可能会造成不可预期的结果;第二,使用窗体采用的界面会跟标准的Axapta周期性处理的交互界面不同.另外,使用窗体没办法执行批处理.
周期性处理需要收集参数(译注:用户通过交互界面输入的参数),获取数据(译注:从数据库或者缓存中),根据收集的参数处理数据.Axapta提供了两个类来简化编程过程,RunBase和RunBaseBatch.
这篇文章将会创建一个周期性处理过程,该处理过程修改库存物料以便反映新的佣金物料组.收集参数,定义需要修改的数据,获取数据,根据需要修改数据.
Axapta中的周期性处理
这篇文章会展示在Axapta中如何创建一个周期性处理过程,包含如下内容:
1.创建一个类(或者使用可下载的blankperiodic.xpo 模板);
2.收集参数;
3.利用这些参数获取要处理的数据;
4.处理数据.
创建一个基于Runbase和RunbaseBatch的类
Runbase和Runbase是最常用的两个类,它们驱动Axapta中的周期性处理过程.使用Runbase可以创建一个周期性处理过程,不过不支持批处理.RunbaseBatch继承自Runbase,使用RunbaseBatch可以创建批处理过程.为了使用这两个类,必须创建一个新的类继承其中一个类的方法和属性.
如果不想创建自己创建类,可以把myPeriodic.xpo 导入AOT,它包含完成下面创建周期性处理过程所需要的数据.
注意:熟悉继承是一个很好的编程概念,关于继承的解释,使用它的好处和例子,可以看Steve McConnell写的Code Complete, Second Edition.
在Axapta中创建一个Runbase类
1.在AOT中创建一个类;
2.打开类中的classdeclaration方法;
3.在ClassDeclaration 方法中,用如下代码替代类的声明(或者类似的代码)
class myRunbase extends Runbase
{
}
4.点击工具栏上的保存按钮,类将会变成新的类名.{
}
继承使该类具有了很多功能,尽管只呈现了ClassDeclaration 一个方法.
收集参数
收集参数可以是一个周期性处理过程对用户更加有帮助和有意义.其实,一个周期性处理很少在没有用户输入的情况下运行,在运行周期性处理之前,应该提供给用户输入参数的机会.
用户可能需要保留上次运行周期性处理所输入的参数.保留参数可以让用户不用重复向Axapta输入相同的信息.
保留参数
在收集参数之前,必须为保留类中的参数值做好准备.保留参数值有几个步骤.第一,定义要保留的参数值;第二,保留用户的选择(参数),这些输入通过操作Runbase的内存区域完成,使用Pack和Unpack方法(译注:看起来有些啰嗦,实际上用Pack和Unpack方法来保存用户的输入值,这两个方法是虚方法,必须覆盖).这两个方法通过使用预先定义的宏来保留和获取用户的选择.
定义类需要保留的字段
1.定义将要在Runbase类中用到的字段;
2.在ClassDeclaration 方法中,定义一个宏包含这些字段,字段之间用逗号隔开;
3.在ClassDeclaration 方法中敲入如下代码:
CommissItemGroup commissItemGroup; // Extended Data Type
// for Item Groups
NoYes CreateNew; //Enum field for a check box
QueryRun queryRun; //QueryRun object for later use
#define.CurrentVersion(1) //Define the version of data in the macro
//(more later)
#localmacro.CurrentList //Data macro; notice that you include
// the two variables
commissItemGroup, //Declared previously, separated by commas
createNew
#endmacro
创建保存和获取这些字段的方法// for Item Groups
NoYes CreateNew; //Enum field for a check box
QueryRun queryRun; //QueryRun object for later use
#define.CurrentVersion(1) //Define the version of data in the macro
//(more later)
#localmacro.CurrentList //Data macro; notice that you include
// the two variables
commissItemGroup, //Declared previously, separated by commas
createNew
#endmacro
现在已经创建了需要保存的字段,创建两个不同的方法,一个保存字段一个获取字段.如下所示:
1.Pack方法保存字段;
2.Unpack获取字段
敲入如下代码创建Pack方法
container pack()
{
return [#CurrentVersion,#CurrentList, QueryRun.pack()];
}
Pack方法创建一个Container类型的数据,该Container包含在ClassDeclaration方法中定义的宏所定义的信息,这段代码包含在ClassDeclaration定义的两个宏和QueryRun对象.{
return [#CurrentVersion,#CurrentList, QueryRun.pack()];
}
敲入如下代码创建Unpack方法
boolean unpack(container packedClass)
{
Integer _version = conPeek(packedClass,1);
container _packedQuery;
switch (_version)
{
case #CurrentVersion:
[_version,#CurrentList,_packedQuery] = packedClass;
if (!_packedQuery)
return false;
QueryRun = new QueryRun(_packedQuery);
break;
case 1 :
[_version,#CurrentList] = packedClass;
QueryRun = new QueryRun(QueryStr(InventTable));
break;
default :
return false;
}
return true;
}
前面的代码超出了本文所要讨论的范围(译注:晕.....),但是这个地方注意一下,Unpack不仅仅分析了container中的版本参数,还定义了一个将要运行的InventTable 查询.{
Integer _version = conPeek(packedClass,1);
container _packedQuery;
switch (_version)
{
case #CurrentVersion:
[_version,#CurrentList,_packedQuery] = packedClass;
if (!_packedQuery)
return false;
QueryRun = new QueryRun(_packedQuery);
break;
case 1 :
[_version,#CurrentList] = packedClass;
QueryRun = new QueryRun(QueryStr(InventTable));
break;
default :
return false;
}
return true;
}
在定义完字段并定义了保存和获取字段值的方法后,需要将这些字段跟dialog上的字段对应.
创建一些dialog字段以便用户输入参数值,为了完成这个任务,必须修改ClassDeclaration这个类.
敲入如下代码,在类中增加dialogCommissItemGroup 和dialogNewField 两个变量.
DialogField dialogCommissItemGroup;
DialogField dialogNewField;
敲入如下代码创建Dialog方法DialogField dialogNewField;
Object dialog()
{
DialogRunBase dialog = super(); //This uses the inherited method.
dialogCommissItemGroup.value(commissItemGroup); //Inserting our
//retrieved value
dialogNewField.value(newItem); //Inserting our
//retrieved value
return super();
}
上述代码做了两件事情:{
DialogRunBase dialog = super(); //This uses the inherited method.
dialogCommissItemGroup.value(commissItemGroup); //Inserting our
//retrieved value
dialogNewField.value(newItem); //Inserting our
//retrieved value
return super();
}
1.继承了Runbase的Dialog方法;
2.将两个值插入到Dialog的两个字段中
通常情况下,定义一个变量,然后插入值并没有啥作用(译注:如果没有地方对插入的值赋值的话).Runbase会在调用Dialong方法之前调用Unpack,在Unpack方法中给变量赋值.于是,在用户打开类("MyPeriodic")的时候会显示用户上次选择的值.现在先别着急打开,本文的后面部分会描述.
收集参数
有两种方式可以收集信息(参数):如下所示
1.定义一个标准的查询,这样就允许用户自定义查询的字段以及字段对应的值(本文后面会有所描述);
2.定义字段和信息,它们可以在查询和周期性处理中使用.
添加标准查询
一个标准的查询是AOT中的一个对象,它定义了一个数据库查询,一个标准查询可以涉及一个表也可以涉及多个表.图1显示了AOT中的InventTable查询.
图1.InventTable查询
在图1中,Ranges被高亮显示,只包含了ItemId. Ranges包含了想收集的字段信息,比如,如果想包含ItemName,可以添加到Ranges中.
注意:如果想收集特定的信息,请另外创建查询,在标准的查询上增加Ranges可能会给其他用户带来不便.
敲入如下代码,将查询添加到处理过程中(这个在ClassDeclaration 方法中已经介绍过)
QueryRun queryRun;
QueryRun是运行定义的查询的方式,在增加QueryRun之后,必须要保证在处理过程开始前初始化QueryRun对象.可以用如下两种方式初始化类1.New()
2.initParmPault()
(译注:initParmPault()方法是SysSaveable这个接口中定义的方法,RunBase实现了该接口)
在这里,我们将代码放在New()中
初始化QueryRun对象
1.创建一个名为New的方法
2.敲入如下代码
;
super();
queryRun = new QueryRun(querystr(inventTable));
当运行类的时候,QueryRun对象被自动初始化(实例化),QueryRun对象用前面已经讨论过的InventTable查询填充.这里可以用自定义的查询填充(在第二篇文章中将介绍如何白手起家创建查询),确保使用了queryStr函数,否则Axapta将会把inventTable解析成表inventTable而不是查询inventTable.super();
queryRun = new QueryRun(querystr(inventTable));
敲入如下代码,添加QueryRun方法,完成查询的创建
QueryRun queryRun()
{
return QueryRun;
}
注意:为了是用户能够修改正代码中所使用的查询,需要给他们提供一个选项,通过敲入如下代码创建一个showQueryValues(译注:原文为QueryValues方法,疑为笔误)方法:{
return QueryRun;
}
boolean showQueryValues()
{
return true;
}
否则,返回false;{
return true;
}
定义自定义参数
定义自定义参数用来收集在查询中不存在的信息.在前面的代码中,在ClassDeclaration方法中定义了两个类型的DialogField的字段:
1.dialogCommissItemGroup
这个字段用户可以定义佣金物料组
2.dialogNewItem
这个字段用来在运行周期性处理时自动创建一个新的佣金物料组.
为了使用他们,必须执行以下步骤
1.通过修改Dialog方法发布这两个字段(让用户可以看到它们);
2.通过创建getFromDialog 方法将用户输入的值传递给变量.
在Dialog方法中敲入如下代码:
;
dialogCommissionItemGroup =
dialog.addField(typeid(CommissItemGroup),"Commission Item Group");
dialogNewField = dialog.addField(typeId(NoYes),"Create new group");
dialogCommissItemGroup.value(commissItemGroup); //Inserting our
//retrieved value
dialogNewField.value(createNew); //Inserting our
//retrieved value
return super();
敲入如下代码创建getFromDialog方法dialogCommissionItemGroup =
dialog.addField(typeid(CommissItemGroup),"Commission Item Group");
dialogNewField = dialog.addField(typeId(NoYes),"Create new group");
dialogCommissItemGroup.value(commissItemGroup); //Inserting our
//retrieved value
dialogNewField.value(createNew); //Inserting our
//retrieved value
return super();
public boolean getFromDialog()
{
;
commissItemGroup = dialogCommissItemGroup.value();
createNew = dialogNewField.value();
return super();
}
这样就把用户在Dialog字段中输入的值传递给前面定义的变量中.{
;
commissItemGroup = dialogCommissItemGroup.value();
createNew = dialogNewField.value();
return super();
}
到此为止已经完成了从用户获取信息的任务,不过现在MyPeriodic 这个类还不能运行,下面的内容将会描述如何让其运行起来.
运行周期性功能
必须创建一个菜单项来打开类以便运行MyPeriodic 周期性处理,菜单项通过调用预先定义的Main方法来打开类.这样为了允许用菜单项打开类必须手动创建Main方法和运行时指令.一般通过如下步骤进行:
1.创建一个名为Main的静态方法,这样可以保证该类可通过菜单项调用;
2.创建Run方法,进行运行时处理.
Main方法是继承自底层的RunBase的静态方法(译注:我觉得不是继承自RunBase的类,Main方法本身是一个程序入口方法,通过菜单项调用哪个类,哪个类必须有静态Main方法,所以不存在谁继承谁的问题,况且RunBase本身并没有定义Main方法,是一个抽象类,要通过其子类进行实例化).(关于继承的讨论已经超出了本文讨论的范围).
敲入如下代码,创建Main方法:
static void main(Args args)
{
MyRunbase myRunbase;
;
myRunbase = new myRunbase();
if (myRunbase.prompt())
{
myRunbase.run();
}
}
关于Main方法(上述代码)中的更多信息如下:{
MyRunbase myRunbase;
;
myRunbase = new myRunbase();
if (myRunbase.prompt())
{
myRunbase.run();
}
}
1. MyRunBase有个修饰符:Static ,这表示这个类不用实例化就可以使用(感觉这个地方作者好像搞错了,并不是这个类有这个修饰符,而是类的方法.这是两个不同的概念,静态类是不能实例化的,相当于sealed abstract类型).这样类就可以通过菜单项调用(译注:一个类能被菜单项所调用的前提是:有一个静态Main方法).
2.给Main方法传递了Args类型的参数,Args是一个通用类型,可以存放任意数量的变量,在这里Args存放调用这个类的菜单项传递过来的参数.
3.声明类并实例化它.在该类的一个静态方法中完成.
4.调用了Prompt方法,这个方法继承自RunBase,该方法会调用Dialog方法,它会显示栏位和前面创建的查询.
Run方法
Run方法为类创建运行时指令.也是实际进程数据处理的地方,用Try/Catch结构来实现Axapta中的高级(译注:说实在的,Axapta中的事件处理比起C#的事件处理简直是小儿科,呵呵)事件处理.
敲入如下代码,在Run方法中实现Try/Catch结构
Void run()
{
try
{
}
catch (Exception::Deadlock)
{
retry;
}
catch (Exception::Error)
{
}
}
这时就可以选中这个类,点击右键,选择Open,如果代码没写错的话,就可以看到Dialog窗体了.{
try
{
}
catch (Exception::Deadlock)
{
retry;
}
catch (Exception::Error)
{
}
}
如果想了解前面提到的Try/Catch结构,可以参照Axapta开发帮助,或者Microsoft Axapta Developer's Guide.
敲入如下代码,给Dialog窗体添加描述(这时窗体还没有描述)
client server static public ClassDescription description()
{
return "SPIFF Creator";
}
现在需要处理用户的选择,并根据用户的选择处理数据.{
return "SPIFF Creator";
}
处理数据
为了处理数据,需要整合用户的选择和新定义的查询,该查询在Run方法中获取数据.用户通过选择要包含在佣金物料维组中的物料,改变在QueryRun中创建的Query,佣金物料维组这个栏位通过dialog和getFromDialog这两个方法得到,该栏位用来指定使用的物料维组.最后一个字段CreateNew用来允许用户创建一个新的佣金物料维组而不是使用实际的佣金物料维组窗体中的物料维组.如果用户选择了该栏位,则在运行类的其余代码之前需要首先创建这个值.
敲入如下代码,在Run方法中创建本地变量.
InventTable inventTable;
InventTable updateTable;
;
确保用户输入了需要的信息InventTable updateTable;
;
if (!commissItemGroup)
throw error("You must enter a commission item group.");
throw error("You must enter a commission item group.");
如果用户选择了CreateNew,则用CreateItemGroup 方法创建一个新的佣金物料维组.
if (createNew == NoYes::Yes)
this.createItemGroup();
敲入如下代码,创建CreateItemGroup 方法this.createItemGroup();
void createItemGroup()
{
CommissionItemGroup itemGroupTable;
;
itemGroupTable.clear();
itemGroupTable.GroupId = commissItemGroup;
itemGroupTable.Name = "New Item Group";
itemGroupTable.insert();
}
在CreateItemGroup 方法中使用用户输入的信息创建了一个新的佣金物料维组,名称用包含在双引号的字符串(英语)赋值,当然这不是必须的,可以不赋值,保留其为空.{
CommissionItemGroup itemGroupTable;
;
itemGroupTable.clear();
itemGroupTable.GroupId = commissItemGroup;
itemGroupTable.Name = "New Item Group";
itemGroupTable.insert();
}
更改InventTable中的记录以反映新的佣金物料维组,在当前场景下,不能修改查询中的记录,不过可以把查询中的记录传递给一个表变量,这样就可以修改它们了,这也就是为什么前面要声明InventTable这个查询(译注:应该是作者的笔误,这里应该是InventTable表变量).
敲入如下代码,遍历Query中的记录,并传递给表变量修改.
try
{
while (queryRun.next())
{
inventTable = queryRun.get(tableNum(inventTable));
}
}
inventTable = queryRun.get(tableNum(inventTable));
ttsbegin;
select firstonly forupdate updateTable
index hint itemIdx
where updateTable.RecId == inventTable.RecId;
updateTable.CommissionGroupId = commissItemGroup;
updateTable.update();
tscommit;
1.选择出将要改变其值的记录,需要在select语句中用到forupdate关键字.
2.将值放到InventTable变量中(前面定义的updateTable变量)
3.用update方法更新记录.事务可以确保记录更改被保存.
下面是到目前为止,完成的事情:
1.从用户处收集数据.用户可以指定特定的准则和修改查询.
2.利用查询获取要修改的数据.
3.用Run方法修改数据,并保存数据库中.
如果想进行额外的错误处理,可以在catch (Exception::Error)中添加代码.
从菜单中运行类
1.做如下动作中的一个创建菜单项
把类拖进Menu Items->Action中(Axapta会自动创建菜单项)
或者
在Menu Items->Action上右键New Menu Item,在Label属性上输入描述性的单词或短语,比如Item commission population.
2.在AOT中展开Menus找到Cust下的Periodic.
3.将菜单拖进来,这样菜单就OK了,打开后的效果如图2所示.
图2.SPIFF Creator
批处理
RunBaseBatch类允许批处理,为了使用批处理,需要对类进行如下三个改动:
1.更改ClassDeclaration方法,将继承自RunBase改成继承自RunBaseBatch,可以复制原来的类,然后改名为myRunBaseBatch
class myRunbaseBatch extends Runbasebatch
{
}
2.用如下代码创建一个静态构造函数{
}
server static myRunbaseBatch construct()
{
return new myRunbaseBatch();
}
3.在Main方法中调用新创建的构造器.{
return new myRunbaseBatch();
}
MyRunbasebatch myRunbasebatch;
;
myRunbasebatch = myRunbasebatch::construct();
if (myRunbasebatch.prompt())
{
myRunbasebatch.run();
}
当再次打开窗体时,会出现新的窗体,有General(普通信息)和Batch(批处理功能)两个选项页,如图3所示:;
myRunbasebatch = myRunbasebatch::construct();
if (myRunbasebatch.prompt())
{
myRunbasebatch.run();
}
图3.SPIFF Creator General Tab
现在就可以批处理的用户界面了.
结论:
这篇文章展示了如何用RunBase和RunBaseBatch来做周期性处理,本系列的第二篇文章将会更深入地展示如何写周期性处理类,涵盖更多高级特性.