前言:
程序设计中,常常有这样的一个处理场景。需要批量处理一个列表的内容,但在列表条目的设计中,有基于条目的列表处理事件,这样的事件其实是重复覆盖的。
这种状态下,往往是需要屏蔽掉条目事件,进行列表整体处理完毕后,再恢复条目事件的。
举一个常规的例子,一般单据界面的设计模式,左边一个竖型单据号列表,右边是针对这个列表当前单据的内容明细,如下所示:
单据号列表 单据号: B002 单据描述信息: xxxxxxxxx
B001 单据内容: xxxxxxxxxxxxxxx
B002
B003
当这个单据列表发生滚动的时候,例如当前单据由B002改变为B003时,右边的整个内容都会随之改变。可是,在整个列表刷新的过程中,如果不断的触发右边内容
的刷新,就完全不必要了,不仅仅会占用系统大量资源,还可能让程序卡死。 这样的情况,在涉及二级表和三级表的滚动更新模式时,也是同样的问题。
那么我们今天就针对这个设计技巧来谈谈如何完美实现。 ------By Murphy
1,不优良的设计模式
a,增加一个是否刷新变量标识,例如FBusy : boolean;
假设,刷新某条单据的方法为RefreshBill( ABillNo : string); 刷新单据列表的方法是RefreshList;
那么,在刷新单据的过程中可以这么写:
procedure RefreshBill(ABillNo : string);
begin
if FBusy then
exit;
.... //这里是刷新某条单据内容的语句
end;
而在刷新单据列表的过程中,可以这么用:
procedure RefreshList;
begin
FBusy := True; //先更改刷新标记
... //列表滚动的过程
FBusy := False; //标记还原
RefreshBill(CurrentBill); //独立进行一次单据刷新
end;
这种设计,逻辑上是完整的。但这样的设计有什么问题呢?首先就是对标志位FBusy的判断十分繁琐,不仅仅是RefreshList中需要判断,往往在增删改存处理上,
都需要不断的判断调用。而当出现多级表的时候,例如一个单据内容有多个二级三级表的时候,就必须创建多个标记,这些标记在各种事件中的判断与更改,经过
叠加以后,会变得极为复杂,给设计带来不小的难度。
b,还有一种设计,与做是否刷新单据标记是同样的道理,是通过屏蔽事件来实现的。
例如刷新单据内容的过程是写在List的滚动事件中的:
procedure ListAfterScroll(Sender : TObject);
begin
...//这里是刷新某条单据内容的语句
end;
而调用刷新List的时候,需要做这样的处理:
procedure QueryList;
begin
List.OnAfterScroll := nil;
...//列表滚动的过程
List.OnAfterScroll := ListAfterScroll; //还原事件设置。
end;
这种设置跟设置刷新标记其实是一样的,一旦出现多表联动,就变得异常复杂。
并且在使用的时候,如果没注意处理好异常,很容易出现界面控件功能乱掉的情况。
2,高效稳定的处理方式:
a,我们先看看强大的VCL是如何处理这种情况的,就拿TStrings类处理UpdateState为例,下面是我从VCL中截取的一段源码,
无关的代码部分,我用省略号代替:
type
TStrings = class(TPersistent) private ... FUpdateCount: Integer; //定义一个计数器标记,由于是一个对象静态变量,所以系统初始化为0,没作额外初始化。 ...protected
...
property UpdateCount: Integer read FUpdateCount; //这里将计数器以只读属性的标识表达出来,如果大于0则表示正在更新中。
...
public ... procedure BeginUpdate; //TStrings列表更新开始 ... procedure EndUpdate; //TStrings列表更新结束 ... procedure SetUpdateState(Updating: Boolean); virtual; //更新的实际过程,系统给了一个虚方法,根据派生不同的LIST列表进行实化。 ...procedure TStrings.BeginUpdate;begin if FUpdateCount = 0 then SetUpdateState(True); //如果计数器为0,才进行刷新,系统这里明确区分出了刷新开始和刷新结束过程(即这里的参数True)。 Inc(FUpdateCount);end;...procedure TStrings.EndUpdate;begin Dec(FUpdateCount); //处理完计数器减1 if FUpdateCount = 0 then SetUpdateState(False); //作结尾的刷新处理,注意参数False。end;...procedure TStrings.SetUpdateState(Updating: Boolean); //对单条Item的刷新处理,这里还没有实化。beginend;以上的代码就是处理滚动更新的核心代码了,有这样的设置,就可以很好的避免多次调用SetUpdateState的【惨案】发生,下面是VCL中
一段利用这个机制的源码,这种情况下,即便多次嵌套,逻辑也是非常清晰的,只有所有潜逃层结束时,计数器才能为0,保证了执行次数。
procedure TStrings.AddStrings(const Strings: TArray<string>); //由于这个过程会不断的引起Strings的滚动,带来批量的更新效果,所以这里作特别处理
var I: Integer;begin BeginUpdate; //滚动之前增加开始标记 try for I := Low(Strings) to High(Strings) do Add(Strings[I]);finally EndUpdate; //滚动之后作结束标记 end;end;同样的设计,几乎贯穿了整个VCL控件处理,例如对数据集状态的控制,核心语句:TDataSet.BeginInsertAppend; 和 TDataSet.EndInsertAppend;
DoBeforeInsert,DoBeforeScroll在BeginXXX部分实现; DoAfterInsert,DoAfterScroll在EndXXX部分实现。
而各种新增操作,都以这个BeginXXX...EndXXX限定,如:TDataSet.AddRecord,TDataSet.Insert,TDataSet.Append,
避免了事件的重复调用。当然这个事例中的计数器就更为复杂些,但道理是一样的。
b,那么我们应该如何从VCL中汲取精华,进行我们的设计呢?同样的,我们还是以更新一个单据列表List,并避免滚动中出现重复刷新为例:
type
TForm1 =Class(TForm)
private
FiIndexListBusyCount : Integer; //数据集滚动计数器,由于没必要查看状态,所以开放为只读属性省了。 public procedure BeginScroll; //防止多次滚动 procedure EndScroll;procedure RefreshBill(ABillNo : string); //刷新右边单据区域数据
procedure RefreshList; //刷新左边的单据列表
... //以下是实现部分
procedure TForm1 .BeginScroll; //由于只需要在滚动结束后,再做刷新动作,所以这里省去了滚动前预处理动作。begin Inc(FiIndexListBusyCount); //计数器加1end;procedure TForm1 .EndScroll;
begin Dec(FiIndexListBusyCount); if FiIndexListBusyCount = 0 then RefreshBill(CurrentBill);end;procedure TForm1 .ListAfterScroll(DataSet: TDataSet); //这里引用一个事件来增加理解
begin BeginScroll; EndScroll; //在这里就有可能调用刷新单据内容的过程RefreshBill end;procedure TForm1 .RefreshBill(ABillNo : string);
begin
.... //这里是刷新某条单据内容的语句,例如QueryBill
end;
procedure TForm1 .RefreshList;
begin
BeginScroll;
try ...这里做实际刷新List的动作,例如QueryList。这个动作会引起多次ListAfterScroll事件finally EndScroll;end;end;
以上代码就完美的解决了滚动数据多次刷新的问题,并且非常好扩展。