监督对象属性值的更改

时间:2012-03-30 15:27:45

标签: delphi

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

我有一个这样的课程:

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

有没有一种标准方法可以做到这一点?

3 个答案:

答案 0 :(得分:4)

使用的典型模式是属性中的setter方法,正如Brian在选项#1中所做的那样。我想给你写一些示例代码,这样你就可以看到人们做了什么。

请注意,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;

答案 1 :(得分:2)

我对此主题进行了一些研究,并使用TAspectWeaver demo中的DSharp 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。

我更喜欢Warren的答案(不那么神奇),我只是想表明它是可能的(使用虚函数代理)

答案 2 :(得分:1)

我知道有两种方法可以做到这一点而且都不是很整洁。如果我们有一个OnProperyChanged事件会很棒,但我们没有,所以你必须自己做一些事情。选项包括:

  1. 在每个属性的属性设置器过程中设置CHANGED布尔值。

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

  3. 我很想知道更好的方法。