2012-03-30 371 views
3

我想监督一个类的实例。每当该对象的属性发生变化时,我希望能够检查该对象,而无需自己实现该功能。特别是如果班级有很多属性。监督对象属性值的更改

我有一类这样的:

TMyClass = class 
private 
    FTest1: Integer; 
    ... 
    FTestN: Integer; 
public 
    property Test1: Integer read FTest1 write FTest1; 
    ... 
    property TestN: Integer read FTest1 write FTest1; 
end. 

当使用这个类:

c := TMyClass.Create; 

这将是真棒有类似:

c.changed // -> false 
c.Test1 := 1; 
c.changed // -> true 

是有一个标准的方法来做到这一点?

+0

是什么你要求还不完全清楚。你是否正在寻找一种方法来检测对象的属性何时被修改而不需要为每个属性的getter触发事件? – 2012-03-30 17:21:48

+1

请注意,你的例子并不清楚,因为'changed'是一个时态属性:第二个'c.changed'后应该返回什么?假如只有一个客户检查变化或者它是否保持真实,它是否应该重置为假?另外,你是否愿意改变'TMyClass',或者你想从外部感知变化(这是不可能的)? – jpfollenius 2012-03-30 17:25:46

+0

请确定您的Delphi版本(添加适当的特定标签)! – menjaraz 2012-03-31 08:45:32

回答

1

有两种方法我知道这样做,但都不整齐。如果我们有一个OnProperyChanged事件会很好,但是我们不这样做,所以你必须自己做一些事情。选项有:

  1. 在属性设置程序中为每个属性设置一个CHANGED布尔值。

  2. 使用RTTI保留所有属性数据的影子副本,并与定时器上的副本进行比较以设置CHANGED标志(如果不同)。

我很想知道更好的方法。

+0

虚拟代理功能是另一种方式(请参阅我的答案) – whosrdaddy 2012-04-02 12:09:59

4

布赖恩放入选项#1时,使用的典型模式是属性中的setter方法。我想给你写一些示例代码,这样你就可以看到人们在做什么。

请注意,NameChanged是一个虚拟方法,因为我可能想要声明基类TPersonInfo,然后为TJanitorInfo创建子类,并且TJanitorInfo可能为NameChanged执行更复杂的实现。因此,处理属性值更改的一个级别是子类可以覆盖方法。但对于不是子类的东西,你在你的问题中建议将布尔标志设置为true。那就需要“重复检查该标志”(称为轮询)。这最终可能会比它的价值更多的工作。也许你需要的是以下所示的“事件”,也称为“回调”或“指向方法的指针”。在delphi中,这些属性以字On开头。 OnNameChanged就是这样一个事件。

type 
    TPersonInfo = class 
     private 
      FName:String; 
      FOnChangedProperty:TNotifyEvent; 
     protected 
      procedure SetName(aName:String); 
      procedure NameChanged; virtual; 
     published 
      property Name:String read fName write SetName; 

      property OnChangedProperty:TNotifyEvent read FOnChangedProperty write FOnChangedProperty; 

    end; 

... 
implementation 

    procedure TPersonInfo.SetName(aName:String); 
    begin 
     if aName<>FName then begin 
     aName := FName; 
     NameChanged; 
     end; 
    end; 

    procedure NameChanged; virtual; 
    begin 
     // option A: set a boolean flag. Exercise for reader: When does this turn off? 
     FNameChanged := true; 
     // option B: refresh visual control because a property changed: 
     Refresh; 
     // option C: something else (math or logic) might need to be notified 
     if Assigned(FOnChangedProperty) then 
       FOnChangedProperty(Self); 
    end; 
2

我做了关于这个问题的一些研究,并与TAspectWeaver demoDSharp project发挥来实现这一目标:

unit Aspects.ChangeDetection; 

interface 

uses 
    DSharp.Aspects, 
    Rtti, 
    SysUtils, 
    StrUtils; 

type 
    TChangeDetectionAspect = class(TAspect) 
    private 
    class var IsChanged : Boolean; 
    public 
    class procedure DoAfter(Instance: TObject; Method: TRttiMethod; 
     const Args: TArray<TValue>; var Result: TValue); override; 
    class procedure DoBefore(Instance: TObject; Method: TRttiMethod; 
     const Args: TArray<TValue>; out DoInvoke: Boolean; 
     out Result: TValue); override; 
    class procedure DoException(Instance: TObject; Method: TRttiMethod; 
     const Args: TArray<TValue>; out RaiseException: Boolean; 
     Exception: Exception; out Result: TValue); override; 
    end; 

    ChangeDetectionAttribute = class(AspectAttribute) 
    public 
    constructor Create; 
    end; 

    [ChangeDetection] 
    IChangeable = interface 
    ['{59992EB4-62EB-4A9A-8216-1B14393B003B}'] 
    function GetChanged: Boolean; 
    procedure SetChanged(const Value: Boolean); 
    property Changed : boolean read GetChanged write SetChanged; 
    end; 

    TChangeable = class(TInterfacedObject, IChangeable) 
    private 
    FChanged : Boolean; 
    function GetChanged: Boolean; 
    procedure SetChanged(const Value: Boolean); 
    public 
    property Changed : boolean read GetChanged write SetChanged; 
    end; 


implementation 

{ TChangeDetectionAspect } 

class procedure TChangeDetectionAspect.DoAfter(Instance: TObject; Method: TRttiMethod; 
    const Args: TArray<TValue>; var Result: TValue); 

var ic : IChangeable; 

begin 
if Supports(Instance, IChangeable, ic) then 
    ic.Changed := IsChanged; 
end; 

class procedure TChangeDetectionAspect.DoBefore(Instance: TObject; Method: TRttiMethod; 
    const Args: TArray<TValue>; out DoInvoke: Boolean; out Result: TValue); 

var ctx : TRttiContext; 
    typ : TRttiType; 
    meth : TRttiMethod; 
    Res : TValue; 

begin 
IsChanged := False; 
if StartsText('set', Method.Name) then 
    begin 
    ctx := TRttiContext.Create; 
    typ := ctx.GetType(Instance.ClassType); 
    // call Getxxx counterpart 
    meth := typ.GetMethod('G'+ Copy(Method.Name, 2, Maxint)); 
    if Assigned(meth) then 
    try 
    Res := meth.Invoke(Instance, []); 
    IsChanged := Res.AsVariant <> Args[0].AsVariant; 
    except 
    end; 
    end; 
end; 

class procedure TChangeDetectionAspect.DoException(Instance: TObject; Method: TRttiMethod; 
    const Args: TArray<TValue>; out RaiseException: Boolean; Exception: Exception; 
    out Result: TValue); 
begin 

end; 

{ ChangeDetectionAttribute } 

constructor ChangeDetectionAttribute.Create; 
begin 
    inherited Create(TChangeDetectionAspect); 
end; 

{ TChangeable } 

function TChangeable.GetChanged: Boolean; 
begin 
Result := FChanged; 
end; 

procedure TChangeable.SetChanged(const Value: Boolean); 
begin 
FChanged := Value; 
end; 

end. 

用法:

unit u_frm_main; 

interface 

uses 
    Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, 
    Dialogs, Aspects.ChangeDetection, DSharp.Aspects.Weaver; 

type 
    TForm1 = class(TForm) 
    procedure FormCreate(Sender: TObject); 
    private 
    { Private declarations } 
    public 
    { Public declarations } 
    end; 

    IMyObject = interface(IChangeable) 
    function GetName: String; 
    procedure SetName(const Value: String); 
    property Name : String read GetName write SetName; 
    end; 

    TMyObject = class(TChangeable, IMyObject) 
    private 
    FName : String; 
    public 
    function GetName: String; 
    procedure SetName(const Value: String); virtual; 
    end; 

var 
    Form1: TForm1; 

implementation 

{$R *.dfm} 

{ TMyObject } 

function TMyObject.GetName: String; 
begin 
Result := FName; 
end; 

procedure TMyObject.SetName(const Value: String); 
begin 
FName := Value; 
end; 

procedure TForm1.FormCreate(Sender: TObject); 

var MyObject : IMyObject; 
begin 
MyObject := TMyObject.Create; 
MyObject.Changed := False; 
AspectWeaver.AddAspect(TMyObject, TChangeDetectionAspect, '^Set'); 
MyObject.Name := 'yee'; 
if MyObject.Changed then 
    ShowMessage('yep changed'); 
MyObject.Name := 'yee'; 
if MyObject.Changed then 
    ShowMessage('oops, not changed should not display'); 
MyObject.Name := 'yeea'; 
if MyObject.Changed then 
    ShowMessage('yep changed'); 
end; 

end. 

请注意,你应该有至少Delphi2010为此工作。

我喜欢沃伦的回答虽然(魔少),我只是想证明它是合法的(虚拟函数代理)