关于滚动更新的设计技巧

前言

程序设计中,常常有这样的一个处理场景。需要批量处理一个列表的内容,但在列表条目的设计中,有基于条目的列表处理事件,这样的事件其实是重复覆盖的。

这种状态下,往往是需要屏蔽掉条目事件,进行列表整体处理完毕后,再恢复条目事件的。

举一个常规的例子,一般单据界面的设计模式,左边一个竖型单据号列表,右边是针对这个列表当前单据的内容明细,如下所示:

单据号列表                         单据号: 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的刷新处理,这里还没有实化。
begin
end;

以上的代码就是处理滚动更新的核心代码了,有这样的设置,就可以很好的避免多次调用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);     //计数器加1
end;

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;

以上代码就完美的解决了滚动数据多次刷新的问题,并且非常好扩展。

原文地址:https://www.cnblogs.com/Murphieston/p/9928218.html