How to assign event handler to event property using RTTI? - delphi

I have a class that has a few event properties, and another class that contains the event handlers. At compile-time I don't know the structure of either class, at run-time I only get to know the match between event property and event handler using their names. Using RTTI, I'd like to assign the event handlers to the respective event properties, how can I do so?
I currently have something like this:
type
TMyEvent = reference to procedure(const AInput: TArray<string>; out AOutput: TArray<string>);
TMyBeforeEvent = reference to procedure(const AInput: TArray<string>; out AOutput: TArray<string>; out ACanContinue: boolean);
TMyClass = class
private
FOnBeforeEvent: TMyBeforeEvent;
FOnEvent: TMyEvent;
public
property OnBeforeEvent: TMyBeforeEvent read FOnBeforeEvent write FOnBeforeEvent;
property OnEvent: TMyEvent read FOnEvent write FOnEvent;
end;
TMyEventHandler = class
public
procedure DoBeforeEvent(const AInput: TArray<string>; out AOutput: TArray<string>; out ACanContinue: boolean);
procedure DoEvent(const AInput: TArray<string>; out AOutput: TArray<string>);
end;
procedure AssignEvent;
implementation
uses
Vcl.Dialogs, System.RTTI;
{ TMyEventHandler }
procedure TMyEventHandler.DoBeforeEvent(const AInput: TArray<string>;
out AOutput: TArray<string>; out ACanContinue: boolean);
begin
// do something...
end;
procedure TMyEventHandler.DoEvent(const AInput: TArray<string>;
out AOutput: TArray<string>);
begin
// do something...
end;
procedure AssignEvent;
var
LObj: TMyClass;
LEventHandlerObj: TMyEventHandler;
LContextObj, LContextHandler: TRttiContext;
LTypeObj, LTypeHandler: TRttiType;
LEventProp: TRttiProperty;
LMethod: TRttiMethod;
LNewEvent: TValue;
begin
LObj := TMyClass.Create;
LEventHandlerObj := TMyEventHandler.Create;
LContextObj := TRttiContext.Create;
LTypeObj := LContextObj.GetType(LObj.ClassType);
LEventProp := LTypeObj.GetProperty('OnBeforeEvent');
LContextHandler := TRttiContext.Create;
LTypeHandler := LContextHandler.GetType(LEventHandlerObj.ClassType);
LMethod := LTypeHandler.GetMethod('DoBeforeEvent');
LEventProp.SetValue(LObj, LNewEvent {--> what value should LNewEvent have?});
end;

A reference to procedure(...) is an anonymous method type. Under the hood, it is implemented as an interfaced object (ie, a class that implements the IInterface interface) with an Invoke() method that matches the parameters of the procedure.
So, can't use TMyEventHandler.DoBeforeEvent() directly with TMyClass.OnBeforeEvent, for instance, since TMyEventHandler does not match that criteria. But, you can wrap the call to DoBeforeEvent() inside of an actual anonymous procedure, eg:
procedure AssignEvent;
var
LObj: TMyClass;
LEventHandlerObj: TMyEventHandler;
LEventHandler: TMyBeforeEvent;
LContextObj: TRttiContext;
LMethod: TRttiMethod;
LEventProp: TRttiProperty;
begin
LObj := TMyClass.Create;
LEventHandlerObj := TMyEventHandler.Create;
LContextObj := TRttiContext.Create;
LMethod := LContextObj.GetType(LEventHandlerObj.ClassType).GetMethod('DoBeforeEvent');
LEventHandler := procedure(const AInput: TArray<string>; out AOutput: TArray<string>; out ACanContinue: boolean);
begin
// Note: I don't know if/how TRttiMethod.Invoke() can handle
// 'out' parameters, so this MAY require further tweaking...
LMethod.Invoke(LEventHandlerObj, [AInput, AOutput, ACanContinue]);
end;
LEventProp := LContextObj.GetType(LObj.ClassType).GetProperty('OnBeforeEvent');
LEventProp.SetValue(LObj, TValue.From(LEventHandler));
end;
Same with using TMyEventHandler.DoEvent() with TMyClass.OnEvent.
Alternatively, if you change the reference to procedure(...) into procedure(...) of object instead, you can then use the TMethod record to assign TMyEventHandler.DoBeforeEvent() directly to TMyClass.OnBeforeEvent, eg:
procedure AssignEvent;
var
LObj: TMyClass;
LEventHandlerObj: TMyEventHandler;
LEventHandler: TMyBeforeEvent;
LContextObj: TRttiContext;
LEventProp: TRttiProperty;
LMethod: TRttiMethod;
begin
LObj := TMyClass.Create;
LEventHandlerObj := TMyEventHandler.Create;
LContextObj := TRttiContext.Create;
LEventProp := LContextObj.GetType(LObj.ClassType).GetProperty('OnBeforeEvent');
LMethod := LContextObj.GetType(LEventHandlerObj.ClassType).GetMethod('DoBeforeEvent');
with TMethod(LEventHandler) do
begin
Code := LMethod.CodeAddress;
Data := LEventHandlerObj;
end;
LEventProp.SetValue(LObj, TValue.From(LEventHandler));
end;
Same with using TMyEventHandler.DoEvent() with TMyClass.OnEvent.

Good question!
I believe I have found a sound approach.
To illustrate it, create a new VCL application with a form (Form1) and a single button (Button1) on it. Give the form an OnClick handler:
procedure TForm1.FormClick(Sender: TObject);
begin
ShowMessage(Self.Caption);
end;
We will attempt to assign this handler to the button's OnClick property using only RTTI and property and method names (as strings).
The key thing to remember is that a method pointer is a pair (code pointer, object pointer), specifically, a TMethod record:
procedure TForm1.FormCreate(Sender: TObject);
var
C: TRttiContext;
b, f: TRttiType;
m: TRttiMethod;
bp: TRttiProperty;
handler: TMethod;
begin
C := TRttiContext.Create;
f := C.GetType(TForm1);
m := f.GetMethod('FormClick');
b := C.GetType(TButton);
bp := b.GetProperty('OnClick');
handler.Code := m.CodeAddress;
handler.Data := Form1;
bp.SetValue(Button1, TValue.From<TNotifyEvent>(TNotifyEvent(handler)));
end;

Related

How to access Tag property

How can I access HTTPRIO.Tag previously assigned from, for instance, the HTTPRIO.OnBeforeExecute event? Is it possible?
Let's say I have something like this:
procedure TForm1.HTTPRIO1BeforeExecute(const MethodName: string; SOAPRequest: TStream);
begin
if THHPRIO(Sender).Tag = 99 then
...some code...
end;
But I don't have a Sender on any of the THTTPRIO events.
Since you are assigning events to multiple THTTPRIO objects, but don't have access to a Sender parameter in the events (bad design on THTTPRIO's author), one workaround is to use the TMethod record to manipulate the event handler's Self pointer to point at the THTTPRIO object rather than the TForm1 object, eg:
procedure TForm1.FormCreate(Sender: TObject);
var
M: TBeforeExecuteEvent;
begin
M := HTTPRIOBeforeExecute;
TMethod(M).Data := HTTPRIO1;
HTTPRIO1.OnBeforeExecute := M;
M := HTTPRIOBeforeExecute;
TMethod(M).Data := HTTPRIO2;
HTTPRIO2.OnBeforeExecute := M;
// and so on ...
end;
And then you can type-cast Self inside the events, eg:
procedure TForm1.HTTPRIOBeforeExecute(const MethodName: string; SOAPRequest: TStream);
begin
if THTTPRIO(Self).Tag = 99 then
...some code...
end;
Alternatively, you can use a standalone procedure (or class static method) with an explicit parameter for the Self pointer, eg:
procedure HTTPRIOBeforeExecute(Sender: THTTPRIO; const MethodName: string; SOAPRequest: TStream);
begin
if Sender.Tag = 99 then
...some code...
end;
procedure TForm1.FormCreate(Sender: TObject);
var
M: TBeforeExecuteEvent;
begin
TMethod(M).Data := HTTPRIO1;
TMethod(M).Code := #HTTPRIOBeforeExecute;
HTTPRIO1.OnBeforeExecute := M;
TMethod(M).Data := HTTPRIO2;
TMethod(M).Code := #HTTPRIOBeforeExecute;
HTTPRIO2.OnBeforeExecute := M;
// and so on ...
end;
While Remy's solution works, leaving the code in TForm1 leaves you at risk of accidentally referencing fields from TForm1 in TForm1.HTTPRIOBeforeExecute, and since "Self" isn't a TForm1 anymore, that would crash/corrupt memory.
I would personally use the following approach :
THTTPRIOMethodHolder = class(THTTPRIO)
public
procedure HTTPRIOBeforeExecute(const MethodName: string; SOAPRequest: TStream);
end;
procedure THTTPRIOMethodHolder.HTTPRIOBeforeExecute(const MethodName: string; SOAPRequest: TStream);
begin
if {self.}Tag = 99 then
...some code...
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
HTTPRIO1.OnBeforeExecute := THTTPRIOMethodHolder(HTTPRIO1).HTTPRIOBeforeExecute;
HTTPRIO2.OnBeforeExecute := THTTPRIOMethodHolder(HTTPRIO2).HTTPRIOBeforeExecute;
end;
This should work as long as you don't add fields in THTTPRIOMethodHolder and HTTPRIOBeforeExecute is not made virtual.
Well, I actualy not leaving the code in TForm1.
I'm doing something like this:
type
THTTPRIO_EventHandlers_v2 = Class
procedure HTTPRIOBeforeExecute(const MethodName: String; SOAPRequest: TStream);
procedure HTTPRIOAfterExecute(const MethodName: string; SOAPResponse: TStream);
end;
type
THTTPRIOBeforeExecute = procedure(const MethodName: String; SPRequest: TStream) of Object;
THTTPRIOAfterExecute = procedure(const MethodName: string; SOAPResponse: TStream) of Object;
var
HTTPRIO: THTTPRIO;
procedure TForm1.Button1Click(Sender: TObject)
var
RIO_Events: THTTPRIO_EventHandlers_v2;
EvtHTTPRIOBeforeExecute: THTTPRIOBeforeExecute;
EvtHTTPRIOAfterExecute: THTTPRIOAfterExecute;
begin
HTTPRIO := THTTPRIO.Create(Application);
HTTPRIO.Tag := 99;
HTTPRIO.Converter.Encoding := 'utf-8';
RIO_Events := THTTPRIO_EventHandlers_v2.Create;
EvtHTTPRIOBeforeExecute := RIO_Events.HTTPRIOBeforeExecute;
TMethod(EvtHTTPRIOBeforeExecute).Data := HTTPRIO;
HTTPRIO.OnBeforeExecute := EvtHTTPRIOBeforeExecute;
EvtHTTPRIOAfterExecute := RIO_Events.HTTPRIOAfterExecute;
TMethod(EvtHTTPRIOAfterExecute).Data := HTTPRIO;
HTTPRIO.OnAfterExecute := EvtHTTPRIOAfterExecute;
RIO_Events.Free;
end;
And on the, for instance, HTTPRIOBeforeExecute procedure, I'm doing this:
procedure THTTPRIO_EventHandlers_v2.HTTPRIOBeforeExecute(const MethodName: String; SOAPRequest: TStream);
var
HttpRioTag: Integer;
begin
HttpRioTag := THTTPRIO(Self).Tag;
if HttpRioTag = 99 then
...some code...
end;
This is working as expected.
Am I doing it the right way?
Thanks

Rtti accessing fields, properties and invoke method in record structures

Rtti accessing fields, properties and invoke method in record structures.
I use the following record types, is from site
type
Nullable<T> = record
public
FValue: T;
FHasValue: boolean;
procedure Clear;
function GetHasValue: boolean;
function GetValue: T;
constructor Create(AValue: T);
property HasValue: boolean read GetHasValue;
property Value: T read GetValue;
class operator Implicit(Value: Nullable<T>): T;
class operator Implicit(Value: T): Nullable<T>;
end;
type
TIntEx = Nullable<integer>;
TSmallintEx = Nullable<smallint>;
implementation
constructor Nullable<T>.Create(AValue: T);
begin
FValue := AValue;
FHasValue := false;
end;
function Nullable<T>.GetHasValue: boolean;
begin
Result := FHasValue;
end;
function Nullable<T>.GetValue: T;
begin
Result := FValue;
end;
class operator Nullable<T>.Implicit(Value: Nullable<T>): T;
begin
Result := Value.Value;
end;
class operator Nullable<T>.Implicit(Value: T): Nullable<T>;
begin
Result := Nullable<T>.Create(Value);
end;
But with a record this code doesn't work
type
[TableName('Record')]
TMyrecord = class(TPersistent)
private
FRecno: TIntEx;
FName: TStringEx;
protected
public
constructor Create();
destructor Destoy();
function GetSqlInsert(): string;
[SqlFieldName('recno')]
property Recno: TIntEx read FRecno write FRecno;
[SqlFieldName('Name')]
property Name: TStringEx read FName write FName;
end;
implementation
{ TMyrecord }
function TMyrecord.GetSqlInsert(): string;
var
vCtx: TRttiContext;
vType: TRttiType;
vProp: TRttiProperty;
vAttr: TCustomAttribute;
vPropValue: TValue;
vRecord: TRttiRecordType;
M: TRttiMethod;
tmpStr: String;
val: TValue;
begin
result := '';
vCtx := TRttiContext.Create;
try
vType := vCtx.GetType(self);
for vProp in vType.GetProperties do
for vAttr in vProp.GetAttributes do
if vAttr is SqlFieldName then
begin
if (vProp.IsReadable) and (vProp.IsWritable) and
(vProp.PropertyType.TypeKind = tkRecord) then
begin
vRecord := vCtx.GetType(vProp.GetValue(self).TypeInfo).AsRecord;
M := vRecord.GetMethod('GetValue');
if Assigned(M) then
vPropValue := (M.Invoke(vPropValue, []));
tmpStr := val.ToString;
end;
end;
finally
freeandnil(vCtx);
end;
end;
I studied all the examples on the internet but in vain.
vType := vCtx.GetType(self);
The GetType method expects to be pass a pointer to type info, or a class, but you pass an instance. Instead you should pass the class like this:
vType := vCtx.GetType(ClassType);
You must not pass a TRttiContext to FreeAndNil. The TRttiContext type is a record. You don't need to call Create on that type. You don't need to call Free.
Further more, your code to invoke the method is just wrong.
Your function might look like this:
function TMyrecord.GetSqlInsert(): string;
var
vCtx: TRttiContext;
vType: TRttiType;
vProp: TRttiProperty;
vAttr: TCustomAttribute;
vRecord: TValue;
M: TRttiMethod;
begin
vType := vCtx.GetType(ClassType);
for vProp in vType.GetProperties do
for vAttr in vProp.GetAttributes do
if vAttr is SqlFieldNameAttribute then
begin
if (vProp.IsReadable) and (vProp.IsWritable) and
(vProp.PropertyType.TypeKind = tkRecord) then
begin
vRecord := vProp.GetValue(self);
M := vProp.PropertyType.GetMethod('GetValue');
if Assigned(M) then
begin
Result := M.Invoke(vRecord, []).ToString;
exit;
end;
end;
end;
Result := '';
end;
That code does at least call the method and retrieve the returned value. I'll let you take it from there.

Unable to invoke method declare in class implement generic interface method

Delphi support generic for IInterface. I have the follow construct using generic IInterface:
type
IVisitor<T> = interface
['{9C353AD4-6A3A-44FD-B924-39B86A4CB14D}']
procedure Visit(o: T);
end;
TMyVisitor = class(TInterfacedObject, IVisitor<TButton>, IVisitor<TEdit>)
procedure Visit(o: TButton); overload;
procedure Visit(o: TEdit); overload;
end;
implementation
procedure TMyVisitor.Visit(o: TButton);
begin
ShowMessage('Expected: TButton, Actual: ' + o.ClassName);
end;
procedure TMyVisitor.Visit(o: TEdit);
begin
ShowMessage('Expected: TEdit, Actual: ' + o.ClassName);
end;
TMyVisitor class implement two interface: IVisitor<TButton> and IVisitor<TEdit>.
I attempt invoke the methods:
procedure TForm6.Button1Click(Sender: TObject);
var V: IInterface;
begin
V := TMyVisitor.Create;
(V as IVisitor<TButton>).Visit(Button1);
(V as IVisitor<TEdit>).Visit(Edit1);
end;
The output I have is:
Expected: TEdit, Actual: TButton
Expected: TEdit, Actual: TEdit
Apparently, the code doesn't invoke procedure TMyVisitor.Visit(o: TButton) when execute (V as IVisitor<TButton>).Visit(Button1).
Is this a bug in Delphi or I should avoid implement multiple generic IInterface? All above codes have test in Delphi XE6.
as operator requires interface GUID to be able to tell which interface you are referring to. Since generic interfaces share same GUID as operator will not work with them. Basically, compiler cannot tell the difference between IVisitor<TButton> and IVisitor<TEdit> interfaces.
However, you can solve your problem using enhanced RTTI:
type
TCustomVisitor = class(TObject)
public
procedure Visit(Instance: TObject);
end;
TVisitor = class(TCustomVisitor)
public
procedure VisitButton(Instance: TButton); overload;
procedure VisitEdit(Instance: TEdit); overload;
end;
procedure TCustomVisitor.Visit(Instance: TObject);
var
Context: TRttiContext;
CurrentClass: TClass;
Params: TArray<TRttiParameter>;
ParamType: TRttiType;
SelfMethod: TRttiMethod;
s: string;
begin
Context := TRttiContext.Create;
CurrentClass := Instance.ClassType;
repeat
s := CurrentClass.ClassName;
Delete(s, 1, 1); // remove "T"
for SelfMethod in Context.GetType(Self.ClassType).GetMethods('Visit' + s) do
begin
Params := SelfMethod.GetParameters;
if (Length(Params) = 1) then
begin
ParamType := Params[0].ParamType;
if ParamType.IsInstance and (ParamType.AsInstance.MetaclassType = CurrentClass) then
begin
SelfMethod.Invoke(Self, [Instance]);
Exit;
end;
end;
end;
CurrentClass := CurrentClass.ClassParent;
until CurrentClass = nil;
end;
If you need to have Visitor interface you can change declarations to
type
IVisitor = interface
['{9C353AD4-6A3A-44FD-B924-39B86A4CB14D}']
procedure Visit(Instance: TObject);
end;
TCustomVisitor = class(TInterfacedObject, IVisitor)
public
procedure Visit(Instance: TObject);
end;
You can then use that in following manner, just by calling Visit and appropriate Visit method will be called.
procedure TForm6.Button1Click(Sender: TObject);
var V: IVisitor;
begin
V := TMyVisitor.Create;
V.Visit(Button1);
V.Visit(Edit1);
end;
Above code is based on Uwe Raabe's code and you can read more http://www.uweraabe.de/Blog/?s=visitor
And here is extended visitor interface and class that can operate on non-class types. I have implemented only calls for string, but implementation for other types consists only of copy-paste code with different typecast.
IVisitor = interface
['{9C353AD4-6A3A-44FD-B924-39B86A4CB14D}']
procedure Visit(const Instance; InstanceType: PTypeInfo);
procedure VisitObject(Instance: TObject);
end;
TCustomVisitor = class(TInterfacedObject, IVisitor)
public
procedure Visit(const Instance; InstanceType: PTypeInfo);
procedure VisitObject(Instance: TObject);
end;
procedure TCustomVisitor.Visit(const Instance; InstanceType: PTypeInfo);
var
Context: TRttiContext;
Params: TArray<TRttiParameter>;
ParamType: TRttiType;
SelfMethod: TRttiMethod;
begin
Context := TRttiContext.Create;
case InstanceType.Kind of
tkClass : VisitObject(TObject(Instance));
// template how to implement calls for non-class types
tkUString :
begin
for SelfMethod in Context.GetType(Self.ClassType).GetMethods('VisitString') do
begin
Params := SelfMethod.GetParameters;
if (Length(Params) = 1) then
begin
ParamType := Params[0].ParamType;
if ParamType.TypeKind = tkUString then
begin
SelfMethod.Invoke(Self, [string(Instance)]);
Exit;
end;
end;
end;
end;
end;
end;
procedure TCustomVisitor.VisitObject(Instance: TObject);
var
Context: TRttiContext;
CurrentClass: TClass;
Params: TArray<TRttiParameter>;
ParamType: TRttiType;
SelfMethod: TRttiMethod;
s: string;
begin
Context := TRttiContext.Create;
CurrentClass := Instance.ClassType;
repeat
s := CurrentClass.ClassName;
Delete(s, 1, 1); // remove "T"
for SelfMethod in Context.GetType(Self.ClassType).GetMethods('Visit' + s) do
begin
Params := SelfMethod.GetParameters;
if (Length(Params) = 1) then
begin
ParamType := Params[0].ParamType;
if ParamType.IsInstance and (ParamType.AsInstance.MetaclassType = CurrentClass) then
begin
SelfMethod.Invoke(Self, [Instance]);
Exit;
end;
end;
end;
CurrentClass := CurrentClass.ClassParent;
until CurrentClass = nil;
end;
Enhanced Visitor can be used like this:
TVisitor = class(TCustomVisitor)
public
procedure VisitButton(Instance: TButton); overload;
procedure VisitEdit(Instance: TEdit); overload;
procedure VisitString(Instance: string); overload;
end;
var
v: IVisitor;
s: string;
begin
s := 'this is string';
v := TVisitor.Create;
// class instances can be visited directly via VisitObject
v.VisitObject(Button1);
v.Visit(Edit1, TypeInfo(TEdit));
v.Visit(s, TypeInfo(string));
end;
This is a well known problem with generic interfaces. Here is yours:
type
IVisitor<T> = interface
['{9C353AD4-6A3A-44FD-B924-39B86A4CB14D}']
procedure Visit(o: T);
end;
Now, the as operator is implemented on top of the GUID that you specify for the interface. When you write:
(V as IVisitor<TButton>).Visit(Button1);
(V as IVisitor<TEdit>).Visit(Edit1);
how can the as operator distinguish between IVisitor<TButton> and IVisitor<TEdit>? You have only specified a single GUID. In fact when this happens, all instantiated types based on this generic interface share the same GUID. And so whilst the as operator compiles, and the code executes, the runtime behaviour is ill-defined. In effect you are defining multiple interfaces and giving them all the same GUID.
So, the fundamental issue here is that the as operator is not compatible with generic interfaces. You will have to find some other way to implement this. You might consider looking at the Spring4D project for inspiration.

Keep values from the beforepost event to the afterpost event

I am writing this question for Delphi 2007, but I'm pretty sure that this is a common problem in all kind of languages.
So, I have a project where I need to keep informations about the old and new value of certain fields (which are given in the BeforePost event of the dataset I'm working with) and use them in the AfterPost event.
For now, I have been using global variables, but there is already so many of them in the project that this is becoming a real issue when it comes to managing documentation and/or comments.
Basically, I am asking if there is a better way (in Delphi 2007 or in general) to keep the informations from the BeforePost event of a Dataset and get them back in the AfterPost event.
first create a new Custom Data Source
TDataRecord = array of record
FieldName: string;
FieldValue: Variant;
end;
TMyDataSource = class(TDataSource)
private
LastValues: TDataRecord;
procedure MyDataSourceBeforePost(DataSet: TDataSet);
procedure SetDataSet(const Value: TDataSet);
function GetDataSet: TDataSet;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function GetLastValue(FieldName: string): Variant;
property MyDataSet: TDataSet read GetDataSet write SetDataSet;
end;
{ TMyDataSource }
constructor TMyDataSource.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
end;
destructor TMyDataSource.Destroy;
begin
SetLength(LastValues, 0);
inherited Destroy;
end;
function TMyDataSource.GetDataSet: TDataSet;
begin
Result := DataSet;
end;
procedure TMyDataSource.SetDataSet(const Value: TDataSet);
begin
DataSet := Value;
DataSet.BeforePost := MyDataSourceBeforePost;
end;
procedure TMyDataSource.MyDataSourceBeforePost(DataSet: TDataSet);
var
i: integer;
begin
SetLength(LastValues, DataSet.FieldCount);
for i:=0 to DataSet.FieldCount-1 do
begin
LastValues[i].FieldName := DataSet.Fields.Fields[i].FieldName;
LastValues[i].FieldValue := DataSet.Fields.Fields[i].OldValue;
end;
end;
function TMyDataSource.GetLastValue(FieldName: string): Variant;
var
i: integer;
begin
Result := Null;
for i:=0 to Length(LastValues)-1 do
if SameText(FieldName, LastValues[i].FieldName) then
begin
Result := LastValues[i].FieldValue;
break;
end;
end;
and after override application Data Source
TForm1 = class(TForm)
private
MyDataSource: TMyDataSource;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
ADOQuery1.Active := true;
MyDataSource := TMyDataSource.Create(Self);
MyDataSource.MyDataSet := ADOQuery1;
DBGrid1.DataSource := MyDataSource;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
MyDataSource.Free;
end;
procedure TForm1.ADOQuery1AfterPost(DataSet: TDataSet);
var
AValue: Variant;
begin
AValue := MyDataSource.GetLastValue('cname');
if not VarIsNull(AValue) then;
end;

How to copy the properties of one class instance to another instance of the same class?

I want to duplicate a class. It is sufficient that I copy all properties of that class. Is it possible to:
loop thru all properties of a class?
assign each property to the other property, like a.prop := b.prop?
The getters and setters should take care of the underlying implementation details.
EDIT:
As Francois pointed out I did not word my question carefully enough. I hope the new wording of the question is better
SOLUTION:
Linas got the right solution. Find a small demo program below. Derived classes work as expected. I didn't know about the new RTTI possibilities until several people pointed me at it. Very useful information. Thank you all.
unit properties;
interface
uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls,
RTTI, TypInfo;
type
TForm1 = class(TForm)
Memo1: TMemo;
Button0: TButton;
Button1: TButton;
procedure Button0Click(Sender: TObject);
procedure Button1Click(Sender: TObject);
public
procedure GetObjectProperties (AObject: TObject; AList: TStrings);
procedure CopyObject<T: class>(ASourceObject, ATargetObject: T);
end;
TDemo = class (TObject)
private
FIntField: Int32;
function get_str_field: string;
procedure set_str_field (value: string);
public
constructor Create; virtual;
property IntField: Int32 read FIntField write FIntField;
property StrField: string read get_str_field write set_str_field;
end; // Class: TDemo //
TDerived = class (TDemo)
private
FList: TStringList;
function get_items: string;
procedure set_items (value: string);
public
constructor Create; override;
destructor Destroy; override;
procedure add_string (text: string);
property Items: string read get_items write set_items;
end;
var Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.GetObjectProperties(AObject: TObject; AList: TStrings);
var ctx: TRttiContext;
rType: TRttiType;
rProp: TRttiProperty;
AValue: TValue;
sVal: string;
const SKIP_PROP_TYPES = [tkUnknown, tkInterface];
begin
if not Assigned(AObject) and not Assigned(AList) then Exit;
ctx := TRttiContext.Create;
rType := ctx.GetType(AObject.ClassInfo);
for rProp in rType.GetProperties do
begin
if (rProp.IsReadable) and not (rProp.PropertyType.TypeKind in SKIP_PROP_TYPES) then
begin
AValue := rProp.GetValue(AObject);
if AValue.IsEmpty then
begin
sVal := 'nil';
end else
begin
if AValue.Kind in [tkUString, tkString, tkWString, tkChar, tkWChar]
then sVal := QuotedStr(AValue.ToString)
else sVal := AValue.ToString;
end;
AList.Add(rProp.Name + '=' + sVal);
end;
end;
end;
procedure TForm1.CopyObject<T>(ASourceObject, ATargetObject: T);
const
SKIP_PROP_TYPES = [tkUnknown, tkInterface, tkClass, tkClassRef, tkPointer, tkProcedure];
var
ctx: TRttiContext;
rType: TRttiType;
rProp: TRttiProperty;
AValue, ASource, ATarget: TValue;
begin
Assert( Assigned(ASourceObject) and Assigned(ATargetObject) , 'Both objects must be assigned');
ctx := TRttiContext.Create;
rType := ctx.GetType(ASourceObject.ClassInfo);
ASource := TValue.From<T>(ASourceObject);
ATarget := TValue.From<T>(ATargetObject);
for rProp in rType.GetProperties do
begin
if (rProp.IsReadable) and (rProp.IsWritable) and not (rProp.PropertyType.TypeKind in SKIP_PROP_TYPES) then
begin
//when copying visual controls you must skip some properties or you will get some exceptions later
if SameText(rProp.Name, 'Name') or (SameText(rProp.Name, 'WindowProc')) then
Continue;
AValue := rProp.GetValue(ASource.AsObject);
rProp.SetValue(ATarget.AsObject, AValue);
end;
end;
end;
procedure TForm1.Button0Click(Sender: TObject);
var demo1, demo2: TDemo;
begin
demo1 := TDemo.Create;
demo2 := TDemo.Create;
demo1.StrField := '1023';
Memo1.Lines.Add ('---Demo1---');
GetObjectProperties (demo1, Memo1.Lines);
CopyObject<TDemo> (demo1, demo2);
Memo1.Lines.Add ('---Demo2---');
GetObjectProperties (demo2, Memo1.Lines);
end;
procedure TForm1.Button1Click(Sender: TObject);
var derivate1, derivate2: TDerived;
begin
derivate1 := TDerived.Create;
derivate2 := TDerived.Create;
derivate1.IntField := 432;
derivate1.add_string ('ien');
derivate1.add_string ('twa');
derivate1.add_string ('drei');
derivate1.add_string ('fjour');
Memo1.Lines.Add ('---derivate1---');
GetObjectProperties (derivate1, Memo1.Lines);
CopyObject<TDerived> (derivate1, derivate2);
Memo1.Lines.Add ('---derivate2---');
GetObjectProperties (derivate2, Memo1.Lines);
end;
constructor TDemo.Create;
begin
IntField := 321;
end; // Create //
function TDemo.get_str_field: string;
begin
Result := IntToStr (IntField);
end; // get_str_field //
procedure TDemo.set_str_field (value: string);
begin
IntField := StrToInt (value);
end; // set_str_field //
constructor TDerived.Create;
begin
inherited Create;
FList := TStringList.Create;
end; // Create //
destructor TDerived.Destroy;
begin
FList.Free;
inherited Destroy;
end; // Destroy //
procedure TDerived.add_string (text: string);
begin
FList.Add (text);
end; // add_string //
function TDerived.get_items: string;
begin
Result := FList.Text;
end; // get_items //
procedure TDerived.set_items (value: string);
begin
FList.Text := value;
end; // set_items //
end. // Unit: properties //
Try this code (but I won't advise copying properties of visual components because then you'll need to manually skip some properties):
uses
Rtti, TypInfo;
procedure CopyObject<T: class>(ASourceObject, ATargetObject: T);
procedure TForm1.CopyObject<T>(ASourceObject, ATargetObject: T);
const
SKIP_PROP_TYPES = [tkUnknown, tkInterface, tkClass, tkClassRef, tkPointer, tkProcedure];
var
ctx: TRttiContext;
rType: TRttiType;
rProp: TRttiProperty;
AValue, ASource, ATarget: TValue;
begin
Assert( Assigned(ASourceObject) and Assigned(ATargetObject) , 'Both objects must be assigned');
ctx := TRttiContext.Create;
rType := ctx.GetType(ASourceObject.ClassInfo);
ASource := TValue.From<T>(ASourceObject);
ATarget := TValue.From<T>(ATargetObject);
for rProp in rType.GetProperties do
begin
if (rProp.IsReadable) and (rProp.IsWritable) and not (rProp.PropertyType.TypeKind in SKIP_PROP_TYPES) then
begin
//when copying visual controls you must skip some properties or you will get some exceptions later
if SameText(rProp.Name, 'Name') or (SameText(rProp.Name, 'WindowProc')) then
Continue;
AValue := rProp.GetValue(ASource.AsObject);
rProp.SetValue(ATarget.AsObject, AValue);
end;
end;
end;
Usage example:
CopyObject<TDemoObj>(FObj1, FObj2);
Your question as it is does not make much sense to me.
Are you really trying to create a new class by copying an existing one?
Or are you trying to do a deep copy of an instance A of a class into another instance B of the same class?
In that case, see this discussion about cloning in another SO question.
You didn't mention your Delphi version, but here's a good start. You need to explore the Delphi RTTI which allows you to obtain runtime type information. You'd have to iterate your source class for types, then provide a method for assigning each type.
About RTTI
If you're designing your own simple classes, you could just override assign and do your own property assignments there.

Resources