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?

+3


source to share


3 answers


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;

      

+4


source


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)

+2


source


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.

+1


source







All Articles