我想监督一个班级的实例。每当该对象的属性发生变化时,我希望能够检查它,而不是自己实现该功能。特别是如果班级有很多属性。
我有一个这样的课程:
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 :(得分: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事件会很棒,但我们没有,所以你必须自己做一些事情。选项包括:
在每个属性的属性设置器过程中设置CHANGED布尔值。
使用RTTI保留所有属性数据的卷影副本,并与计时器上的副本进行比较,以设置CHANGED标志(如果不同)。
我很想知道更好的方法。