高级Bold开发
作者陈省
在上一节中,我介绍了如何使用Bold的UML设计器,以及对象感知组件,再几乎不写代码的情况下完成一个具有初步的编辑功能的家庭小账本程序。不使用代码的编程无疑是最简单、快捷的,但付出的代价就是不够灵活和强大,特别是无法使用DelphiIDE中的CodeSight来获得对象的属性,在代码编辑器中我现在要是不按.来激活CodeInsight,简直就不会编程了。
比如有时我们需要判断对象的类型,并根据不同的对象执行不同的操作,通常需要执行下面的伪代码:
ifAObjisTPersonthen
doxxx
elseifAObjisTAcctItemthen
doxxx;
但是现在所有的类定义的元数据都储存在BoldModel组件中,并没有生成TPerson和TAcctItem的类定义代码,编译器无法编译通过上面的代码。
幸好Bold除了支持组件开发外,也能根据UML类图中的模型设计自动生成类别定义及实现代码,将上一节的代码复制到一个单独的目录,然后在UMLEditor中选中MoneyModel节点,将默认的UnitName属性从BusinessClasses改为MoneyClasses,如下图示意:
之后Bold生成类的定义代码时,生成的单元名将为MoneyClasses.pas,每次我们更新了类的定义,都可以重新生成一遍类定义代码,Bold新生成的代码将会覆盖原有的版本,保证系统的正确更新。
在UMLEditor中执行菜单命令Tools|GenerateCode,Bold会将生成的代码保存在MoneyClasses_Interface.inc和MoneyClasses.pas文件中,生成的代码文件同时会被自动加载到DelphiIDE中。生成的类定义代码示意如下:
(*****************************************)
(* Thisfileisautogenerated *)
(* AnymanualchangeswillbeLOST! *)
(*****************************************)
(*Generated2003-7-116:14:11 *)
(*****************************************)
(*Thisfileshouldbestoredinthe *)
(*samedirectoryastheform/datamodule*)
(*withthecorrespondingmodel *)
(*****************************************)
(*Copyrightnotice: *)
(* *)
(*****************************************)
{$IFNDEFMoneyClasses_Interface.inc}
{$DEFINEMoneyClasses_Interface.inc}
{$IFNDEFMoneyClasses_unitheader}
unitMoneyClasses;
{$ENDIF}
interface
uses
//interfaceuses
//interfacedependencies
//attributeclasses
BoldAttributes,
//other
Classes,
Controls,//forTDate
SysUtils,
BoldDefs,
BoldSubscription,
BoldDeriver,
BoldElements,
BoldDomainElement,
BoldSystemRT,
BoldSystem;
type
{forwarddeclarationsofallclasses}
TMoneyModelRoot=class;
TMoneyModelRootList=class;
TAcctItem=class;
TAcctItemList=class;
TPerson=class;
TPersonList=class;
TMoneyModelRoot=class(TBoldObject)
private
protected
public
end;
TAcctItem=class(TMoneyModelRoot)
private
function_Get_M_Name:TBAString;
function_GetName:String;
procedure_SetName(constNewValue:String);
function_Get_M_Amount:TBACurrency;
function_GetAmount:Currency;
procedure_SetAmount(constNewValue:Currency);
function_Get_M_HappenDate:TBADate;
function_GetHappenDate:TDate;
procedure_SetHappenDate(constNewValue:TDate);
function_GetPayPerson:TPerson;
function_Get_M_PayPerson:TBoldObjectReference;
procedure_SetPayPerson(constvalue:TPerson);
protected
public
propertyM_Name:TBAStringread_Get_M_Name;
propertyM_Amount:TBACurrencyread_Get_M_Amount;
propertyM_HappenDate:TBADateread_Get_M_HappenDate;
propertyM_PayPerson:TBoldObjectReferenceread_Get_M_PayPerson;
propertyName:Stringread_GetNamewrite_SetName;
propertyAmount:Currencyread_GetAmountwrite_SetAmount;
propertyHappenDate:TDateread_GetHappenDatewrite_SetHappenDate;
propertyPayPerson:TPersonread_GetPayPersonwrite_SetPayPerson;
end;
TPerson=class(TMoneyModelRoot)
private
function_Get_M_Name:TBAString;
function_GetName:String;
procedure_SetName(constNewValue:String);
function_GetPay:TAcctItemList;
protected
public
propertyM_Name:TBAStringread_Get_M_Name;
propertyM_Pay:TAcctItemListread_GetPay;
propertyName:Stringread_GetNamewrite_SetName;
propertyPay:TAcctItemListread_GetPay;
end;
TMoneyModelRootList=class(TBoldObjectList)
protected
functionGetBoldObject(index:Integer):TMoneyModelRoot;
procedureSetBoldObject(index:Integer;NewObject:TMoneyModelRoot);
public
functionIncludes(anObject:TMoneyModelRoot):Boolean;
functionIndexOf(anObject:TMoneyModelRoot):Integer;
procedureAdd(NewObject:TMoneyModelRoot);
functionAddNew:TMoneyModelRoot;
procedureInsert(index:Integer;NewObject:TMoneyModelRoot);
propertyBoldObjects[index:Integer]:TMoneyModelRootreadGetBoldObjectwriteSetBoldObject;default;
end;
TAcctItemList=class(TMoneyModelRootList)
protected
functionGetBoldObject(index:Integer):TAcctItem;
procedureSetBoldObject(index:Integer;NewObject:TAcctItem);
public
functionIncludes(anObject:TAcctItem):Boolean;
functionIndexOf(anObject:TAcctItem):Integer;
procedureAdd(NewObject:TAcctItem);
functionAddNew:TAcctItem;
procedureInsert(index:Integer;NewObject:TAcctItem);
propertyBoldObjects[index:Integer]:TAcctItemreadGetBoldObjectwriteSetBoldObject;default;
end;
TPersonList=class(TMoneyModelRootList)
protected
functionGetBoldObject(index:Integer):TPerson;
procedureSetBoldObject(index:Integer;NewObject:TPerson);
public
functionIncludes(anObject:TPerson):Boolean;
functionIndexOf(anObject:TPerson):Integer;
procedureAdd(NewObject:TPerson);
functionAddNew:TPerson;
procedureInsert(index:Integer;NewObject:TPerson);
propertyBoldObjects[index:Integer]:TPersonreadGetBoldObjectwriteSetBoldObject;default;
end;
Bold生成的代码中定义了TMoneyModelRoot、TAcctItem、TPerson等模型中定义了的类,以及TMoneyModelRootList、TAcctItemList、TPersonList等类型安全的容器类,用于保存模型中类的多个实例,因为容器类中的元素都是强类型的对象TPerson、TAcctItem等,因此编译器会保证对对象操作的正确性,避免对象类型转型的错误。生成的类的关系图示意如下:
接下来,修改DataModule中bsthMoney组件的UseGeneratedCode属性为true,表示使用生成的代码的类定义作为元数据。
比如下面我想在显示账目信息时,所有账目金额大于100的账目金额,都用红色显示,而其它的都用蓝色显示。这需要编写bgAcct网格的OnDrawCell处理事件,同时设定Amount列的UserDraw属性为true使自绘画的功能生效,代码实现如下:
procedureTFormMain.bgAcctDrawCell(Sender:TObject;Canvas:TCanvas;ACol,
ARow:Integer;Rect:TRect;State:TGridDrawState);
var
AcctItem:TAcctItem;
begin
ifbgAcct.BoldList.Elements[ARow-1]isTAcctItemthen
begin
AcctItem:=TAcctItem(bgAcct.BoldList.Elements[ARow-1]);
Canvas.FillRect(rect);
ifAcctItem.Amount>100then
Canvas.Font.Color:=clred
else
Canvas.Font.Color:=clblue;
Canvas.TextOut(Rect.Left,Rect.Top,FloatTostr(AcctItem.Amount));
end;
end;
代码首先从网格的BoldList中获取当前行对应的Bold对象,如果BoldList中的元素是TAcctItem的话,则判断金额数目,进而输出不同颜色的金额。类似的功能如果不使用类型判断和转化的话,是很难实现的,由此可以见使用生成代码定义的类的元数据是十分必要的。
派生属性
迄今为止,模型中的Person和AcctItem类的属性都属于比较简单的属性类型,如字符串、货币类型等。但是现实生活中的对象都具有很多比较复杂的属性,那么如何使用Bold来表达呢?在数据库开发中,我们经常需要使用一些计算字段在运行时获得一些基本属性组合运算的结果,对于每个人员来说,我还想显示他们每个人的总的花费,那么这个总的花费需要对该人员的所有的账目进行汇总才能得到,同时每发生一笔新的费用,都需要更新总的花费属性,对于这种对象属性,可以通过Bold的派生属性来实现。
再次打开UMLEditor,为Person对象添加一个属性TotalAmount,设定属性为派生属性
同时设定DerivationOCL的表达式为pay.amount->sum表示对账目的情况进行汇总。
设定完该属性后,执行菜单命令Tools|GenerateCode重新生成代码,另外注意重新设定bsthMoney的UseGeneratedCode为true,同时重新获得人员的blhPerson的Columns定义,运行后效果显示如下:
派生属性比一般的计算字段更为优越的地方在于,计算字段只能是只读的,而派生属性可以被设计成可写的属性。比如下面假设要为人员增加一个属性出生年份BirthYear,同时还需要增加一个年龄属性,很显然年龄=当前年份-出生年份。因此年龄属性应该定义为一个派生属性,但是用户可能会修正年龄,因此年龄应该是一个可写的属性,每当修改年龄的时候,应该同步更新出生年份=当前年份-年龄。
接下来,为Person添加一个BirthYear的整数属性,同时增加一个可写的派生属性Age,如下图所示意:
这里注意要设置Age的Reversederive属性表示Age是一个可写的派生属性,同时要去掉Persistent选项,表示Age属性无须写入数据库实现可持续性。设定完属性后重新生成代码,这回Bold生成了另外一个文件MoneyClasses.Inc文件,代码如下:
(*****************************************************)
(* *)
(* BoldforDelphiStubFile *)
(* *)
(* Autogeneratedfileformethodimplementations *)
(* *)
(*****************************************************)
//
{$INCLUDEMoneyClasses_Interface.inc}
procedureTPerson._Age_DeriveAndSubscribe(DerivedObject:TObject;Subscriber:TBoldSubscriber);
//var
// Result:Integer;
begin
//CalculatevalueintoResultandplacetherequiredsubscriptions
//Result:=<<formula>>
//M_Age.AsInteger:=Result;
end;
procedureTPerson._Age_ReverseDerive(DerivedObject:TObject);
begin
end;
我们需要重载上面的两个方法,实现对Age属性的读写。要注意的是前面的TotalAmount派生属性并没有生成这两个方法是因为TotalAmount属性指定了OCL表达式,无须手工实现属性的读写。
先来看一下变更后的TPerson的接口定义:
TPerson=class(TMoneyModelRoot)
private
function_Get_M_BirthYear:TBAInteger;
function_GetBirthYear:Integer;
procedure_SetBirthYear(constNewValue:Integer);
function_Get_M_Age:TBAInteger;
function_GetAge:Integer;
procedure_SetAge(constNewValue:Integer);
…
protected
procedure_Age_DeriveAndSubscribe(DerivedObject:TObject;Subscriber:
TBoldSubscriber);virtual;
procedure_Age_ReverseDerive(DerivedObject:TObject);virtual;
…
public
propertyM_BirthYear:TBAIntegerread_Get_M_BirthYear;
propertyM_Age:TBAIntegerread_Get_M_Age;
propertyBirthYear:Integerread_GetBirthYearwrite_SetBirthYear;
propertyAge:Integerread_GetAgewrite_SetAge;
…
end;
其中BirthYear和Age属性对应于出生年份和年龄属性,同时Bold还创建带有M_前缀的类似属性。这些M打头的属性会返回特殊类型的表示对象属性基本类型的Bold对象。Bold定义这些对象的目的是:
-当属性值发生变化时,通知订阅了属性值变化通知的属性进行相关的属性变更。
-对对象的属性值进行缓存,以便对象值没有变化时,无须更新数据库中的内容。
-允许在对象属性级别上对对象的可持续性进行更好的控制。
-同时为了支持乐观锁机制。
现在实现上面两个方法,其中_Age_DeriveAndSubscribe是当读取Age属性时被调用
procedureTPerson._Age_DeriveAndSubscribe(DerivedObject:TObject;Subscriber:TBoldSubscriber);
begin
M_Age.AsInteger:=CurrentYear-BirthYear;
M_BirthYear.DefaultSubscribe(Subscriber);
end;
首先设定缓存对象M_Age的值为当前年份-出生年份,同时调用M_BirthYear的DefaultSubscribe方法订阅Age属性的变更,当Age属性变更时,将同步更新M_BirthYear对象的值。
当Age属性被设定时,会调用_Age_ReverseDerive方法,我们只要简单的设定BirthYear属性就可以了。
procedureTPerson._Age_ReverseDerive(DerivedObject:TObject);
begin
BirthYear:=CurrentYear-Age;
end;
定义操作
到目前为止,模型中定义的对象仍然非常简单,只有属性,而没有定义任何的方法,但是现实的项目中,业务域对象可能会定义有很多的方法。接下来,我们就来看看如何使用Bold定义业务域对象的方法,比如对于人员来说,我不仅关心他们各自总的花费,我也关心他们不同月份的花费,因此有必要定义一个方法,根据指定的年度和月份,返回当月的花费。
现在,在UMLEditor中选中Person节点,执行右键菜单的NewOperation命令,创建一个GetMonthAmount操作,然后执行NewParameter命令,添加AMount,AYear,AMonth三个Integer和Currency类型的参数,分别对应于返回的账目总和,查询的年份,月份。见下图:
再次执行GenerateCode命令,将生成GetMonthAmount的定义:
functionTPerson.GetMonthAmount(constAYear:Integer;constAMonth:Integer):
Currency;
begin
end;
代码的实现非常简单,只要在遍历该人员所有的账目信息,判断月份和年度,将符合条件的账目计算总和就可以了。代码实现如下:
functionTPerson.GetMonthAmount(constAYear:Integer;constAMonth:Integer):Currency;
var
I:Integer;
AcctItem:TAcctItem;
begin
Result:=0;
forI:=0toSelf.Pay.Count-1do
begin
AcctItem:=Self.Pay.BoldObjects[I];
if(YearOf(AcctItem.HappenDate)=AYear)and(MonthOf(AcctItem.HappenDate)=AMonth)
then
Result:=Result+AcctItem.Amount;
end;
end;
然后在界面上增加一个按钮,用来获得2002年12月的花销,代码如下:
procedureTFormMain.btnAmountClick(Sender:TObject);
var
AMount:Currency;
begin
ifblhPerson.CurrentElementisTPersonthen
begin
AMount:=(blhPerson.CurrentElementasTPerson).GetMonthAmount(2002,12);
ShowMessage(floattostr(AMount));
end;
end;
转换到真实的数据库
现在我们的程序由于使用的可持续性存储介质还是XML,今后使用是必须切换到真实的数据库中,因为这样进行大量数据的基于Sql的查询时,才能满足性能的需要,在Bold中切换数据库平台是非常简单的。首先,将所有文件保存为
首先删除XML存取组件,从组件面板中选取TBoldPersistenceHandleDB组件后,添加到DataModule中,命名为bphdMoney。这个组件用于将数据通过DataBase适配器保存到不同的数据库中,接下来还要添加数据库适配器,Bold默认提供了使用Interbase,BDE,ADO,DBExpress等不同的数据库存取API的数据库适配器。这里我们使用Interbase作为数据库平台,这是因为Bold对Interbase支持的最好,另外要注意不要使用Access数据库,因为Bold对Access的支持比较差,经常会创建创建数据库失败的情况。添加一个TBoldDatabaseAdapterIB组件,命名为bdaiMoney,同时添加还要一个TIBDataBaseinterbase数据库连接组件,命名为ibdMoney。
设定bphdMoney的BoldModel为bmMoney,DataBaseAdapter属性为bdaiMoney,设定bdaiMoney组件的DataBase属性为ibdMoney。然后新建一个Interbase数据库文件Money.GDB,使用用户名Sysdba,密码为masterkey来建立同数据库的连接,并设定ibdMoney组件的LoginPrompt属性为false。完成后的组件关系图示意如下:
建立好连接之后,需要根据对象模型的元数据生成数据库的Schema,选中bphdMoney组件,在右键菜单中执行GenerateDatabaseSchema命令,将创建对应对象模型的各个数据库表。
下图就是Bold自动创建的库表结构。
接下来运行程序,你会发现程序的运行效果和基于XML设计的程序是一模一样的,正是通过Adapter模式,Bold实现了不同数据库平台的无缝切换。
对于复杂属性的支持
现在我想为每个人员配备一张个人照片,一般数据库设计中对于照片这样的数据,通常都是以二进制字段形式保存的,而在面向对象开发框架Bold中,如何定义这样的二进制对象属性呢。
打开UMLEditor,选中Person节点,添加一个Photo的属性,如下图所示意:
设定属性的类型为TypedBlob类型,同时设定Allownull为true,允许该属性为空。修改完毕后,重新生成一下类定义代码,同时也重新生成一下DatabaseSchema。
为了显示人员的图像信息,需要在界面上添加一个BoldImage,然后设定其BoldHandle属性为blhPerson,其BoldProperties.Expression属性为photo。同时,为了让BoldImage组件可以粘贴剪贴中的图像,添加一个按钮,编写代码来实现图像粘贴。
procedureTFormMain.btnPasteClick(Sender:TObject);
begin
biPhoto.PasteFromClipboard;
end;
更多的时候图像是通过加载文件来实现的,因此添加一个按钮来加载图像,
procedureTFormMain.btnLoadClick(Sender:TObject);
begin
withdlgOpendo
begin
ifAssigned(biPhoto.Viewer)then
DefaultExt:=biPhoto.Viewer.DefaultExtension;
ifExecutethen
biPhoto.LoadFromFile(FileName);
end;
end;
运行后效果如下图示意:
报表打印
每个月我都想要统计本月支出的情况,但是现有的报表工具如QuickReport,Rave,FastReport,ReportBuilderPro等都主要通过连接数据集的方式来生成报表。而Bold是一个面向对象的开发框架,如何让现有的报表工具能够打印Bold对象的数据呢?
Bold提供了一个巧妙的办法来解决这个问题,它提供了一个TBoldDataset的组件,这个组件可以将Bold对象的属性映射为数据集的字段,这样就解决了报表打印的问题。下面我们就来看看如何使用TBoldDataset实现报表打印。
虽然Delphi7中提供了一个新的报表工具Rave,但是我不是很喜欢用,因此这里我仍然使用我熟悉的QuickReport3.62来实现报表功能,而基于其它报表平台打印Bold报表的实现应该也是大同小异的。
首先,新建一个TQuickRep组件,然后添加一个ColumnHeaderBand,一个TitleBand和一个DetailBand,以及一些标签,然后添加人员的聚合对象BlhPerson组件,设定它的RootHandle=DmMoney.bshMoney,Expression属性为Person.allInstances,获得全部人员对象的实例。然后添加一个TBoldDataset组件,设定组件的BoldHandle=blhPerson,Active和AutoOpen属性都为true。示意图如下:
然后双击BoldDataset组件的FieldDescriptions激活数据字段和类属性映射编辑界面。添加映射,并正确设定字段值和对象属性。如下图示意:
最后将DetailBand上的qrdbtext的字段和数据集指定为BoldDataset和映射的字段。
编译完报表后,在主界面上添加一个打印报表的按钮,输入打印语句:
procedureTFormMain.btnReportClick(Sender:TObject);
begin
//打印报表
ReportMoney.Preview;
end;
运行后,效果示意如下:
条件约束
一个好的应用程序一定具有很好的错误检查机制,现在我们的账本程序还不能说是很完善,比如多输入几个人员的信息后会发现,可以输入名称为空的人员,这显然不符合逻辑。在数据库开发中,我们可以通过指定数据字典来对数据进行约束,那么在面向对象开发中如何实现条件约束呢。
标准的UML不提供约束的功能,都是通过自然语言对约束条件进行描述,但是这些自然语言不容易转换为代码。为此,Bold为对象增加了Constraints这样的属性,通过OCL语言可以来精确的定义约束条件,在运行时Bold会自动根据这些约束条件对输入进行校验,可以极大的减轻我们的工作量。
打开UMLEditor后,执行菜单命令View|Advanced将视图切换到高级模式,可以看到同模式的视图相比,多了好多选项,
点击Constraints属性的编辑按钮,输入下面的约束OCL表达式:
约束名称不能为空。重新生成代码。为了能够查看不符合约束的输入,在界面上放上一个blhContraints的BoldListHandle,设定它的Expression为MoneyModelRoot.allInstances.constraints->select(c|notc),这句OCL语句意思是获取所有不满足约束的条件的对象实例,然后添加一个boldGrid显示这些不满足条件的输入。同时,设定bgPerson.BodlShowConstraints属性为true,这样BoldGrid会在那些不满足约束条件的对象前显示红灯。
运行程序,输入几个空的人员信息,可以看到每个不满足约束条件的信息都被列在了网格中,见下图:
另外可以看到一旦输入了名字信息后,相应的信息将从不满足约束输入网格中消失。但是现在的条件约束还不完备,因为虽然可以提示错误的输入,但是保存数据库时,仍然将这些不符合条件的对象保存到了数据库中了。
为了禁止保存不符合约束条件的对象,需要重载TBoldObject对象的MayUpdate和ReceiveQueryfromOwned方法。打开UMLEditor,选中所有对象的基类MoneyModelRoot,然后
如上图所示意,执行右键菜单命令OverrideFrameworkMethods的MayUpdate和ReceiveQueryfromOwned方法,重载这两个方法,然后重新GenerateCode,将在MoneyClasses.inc文件中生成两个方法的框架代码,重载这两个方法,代码示意如下:
functionTMoneyModelRoot.MayUpdate:Boolean;
var
message:string;
Failure:TBoldFailureReason;
begin
//MayUpdate方法在新建或者删除对象时会被调用,但是更新对象时不会被调用
result:=true;
ifBoldExistenceState=besExistingthen
begin
result:=EvaluateExpressionAsString('constraints->select(a|nota)->size=0',
brDefault)='Y';
ifnotresultthen
begin
//从第一条被违反的约束中获得信息
message:=EvaluateExpressionAsString('constraints->select(a|nota)->first',
10);
Failure:=TBoldFailureReason.Create(Message,self);
failure.MessageFormatStr:='有效性校验错误%s:%2:s';
SetBoldLastFailureReason(Failure);
end;
end;
result:=resultandinheritedMayUpdate;
end;
在MayUpdate方法中调用EvaluateExpressionAsString判断当前不满足约束条件的对象总数是否为0,如果不为零,则从第一个被违法的约束中获取信息,并创建TBoldFailureReason对象通知Bold发生了错误,停止更新对象。
functionTMoneyModelRoot.ReceiveQueryFromOwned(Originator:TObject;OriginalEvent:TBoldEvent;constArgs:arrayofconst;Subscriber:TBoldSubscriber):Boolean;
begin
result:=inheritedReceiveQueryFromOwned(Originator,OriginalEvent,Args,
Subscriber);
//截获对对象成员的更新
if(OriginalEvent=bqMayUpdate)and(OriginatorisTBoldMember)and
(TBoldmember(Originator).OwningObject=self)then
result:=resultandMayUpdate
end;
由于MayUpdate方法在添加或删除对象时会被调用,但更新对象属性时不会被调用,因此为了截获对对象属性的更新事件,需要重载ReceiveQueryFromOwned方法,来保证调用MayUpdate方法确保数据的有效性。
再次运行程序,输入一些不符合要求的内容,退出程序保存数据时,会显示下面提示框,并禁止更新数据:
总结
在本节,我们一起研究了一下Bold一些相对高级的用法,由于Bold的功能非常多,我无法在本书的一章内覆盖Bold的所有功能点,比如本文没有能够详细介绍OCL语言的使用,Bold同Rose和ModelMaker的配合,Sql查询等内容,我内心也挺遗憾的,只能是希望大家能够通过本文的入门指导,对Bold这个强大的面向对象开发框架有所了解,进而自己钻研,写出更好的软件了。