Monitor changes in object property values
I want to control instances of a class. Whenever a property of this object changed, I would like to be able to test it without having to do this function myself. Especially if the class has many attributes.
I have a class like this:
TMyClass = class
private
FTest1: Integer;
...
FTestN: Integer;
public
property Test1: Integer read FTest1 write FTest1;
...
property TestN: Integer read FTest1 write FTest1;
end.
And when using this class:
c := TMyClass.Create;
It would be great to have something like:
c.changed // -> false
c.Test1 := 1;
c.changed // -> true
Is there a standard way to do this?
source to share
The typical pattern used is the setter method on the property, as Brian puts in option # 1. I wanted to write you some sample code so you can see what people are doing.
Note that NameChanged is a virtual method because I might want to declare a base class TPersonInfo and then subclass TJanitorInfo later, while TJanitorInfo might have a more complex implementation for NameChanged
. Thus, one level of planning for handling property value changes is that subclasses can override methods. But for something that is not a subclass, you suggested in your question to set the boolean flag to true. Then this will require "repeat the check of this flag" (the so-called polling). In the end, it may be more work than it is worth. Perhaps you want what is shown below as "Event", also known as "callback" or "method pointer".In delphi, these properties start with a word On
.OnNameChanged
will be such an event.
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;
source to share
I did some research on this and played with a TAspectWeaver
demo from the DSharp project to achieve this:
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.
Using:
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.
Note that you need at least Delphi2010 to do this.
I prefer Warren's answer though (less magical), I just wanted to show that this is possible (with virtual function proxies)
source to share
There are two ways I know this, and they are not neat. It would be great if we had an OnProperyChanged event, but we don't, so you need to do something yourself. Possible options:
-
Set CHANGED Boolean inside the property setting procedure for each of your properties.
-
Use RTTI to keep a shadow copy of all your property data and compare to a copy on a timer to set the CHANGED flag if different.
I would be very interested to know the best way.
source to share