How do I use RTTI to set an enumerated field's value ?
I.e.
type
TCPIFileStatus= (fsUnknown, fsProcessed);
TTest = class
FStatus: TCPIFileStatus;
end;
...
var
Data: TTest;
Ctx: TRttiContext;
Status : TCPIFileStatus;
begin
Data := TTest.Create;
Status := fsProcessed;
Ctx.GetType(Data.ClassType).GetField('FStatus').SetValue(Data, Status);
end;
I get "Invalid class typecast."
NB:I need to use RTTI because I will not always know the object type or field name at design time.
you must pass a TValue to the SetValue method try using this code :
{$APPTYPE CONSOLE}
uses
Rtti,
SysUtils;
type
TCPIFileStatus= (fsUnknown, fsProcessed);
TTest = class
FStatus: TCPIFileStatus;
end;
var
Data : TTest;
Ctx : TRttiContext;
Status : TCPIFileStatus;
v : TValue;
begin
try
Data := TTest.Create;
try
Status := fsProcessed;
v:= v.From(status);
Ctx.GetType(Data.ClassType).GetField('FStatus').SetValue(Data, v);
// do your stuff
finally
Data.Free;
end;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
Another solution to this problem, in the case you don't know the exact enum type on your function but instead it's TypeInfo, is to use TValue's Make procedure.
procedure Make(AValue: NativeInt; ATypeInfo: PTypeInfo; out Result: TValue); overload; static;
Here is an example (From an XML config parser):
This is later used for a TRTTIField/TRTTIProperty.SetValue()
function EnumNameToTValue(Name: string; EnumType: PTypeInfo): TValue;
var
V: integer;
begin
V:= GetEnumValue(EnumType, Name);
TValue.Make(V, EnumType, Result);
end;
Hope this helps you.
Use TValue.From generic method to obtain a compatible TValue value to pass to the SetValue method...
mmm... it's hard to get from words, better code:
type
TCPIFileStatus= (fsUnknown, fsProcessed);
TTest = class
FStatus: TCPIFileStatus;
end;
procedure TForm2.Button1Click(Sender: TObject);
var
Data: TTest;
Ctx: TRttiContext;
Status : TCPIFileStatus;
AValue: TValue;
begin
Data := TTest.Create;
try
Status := fsProcessed;
Ctx.GetType(Data.ClassType).GetField('FStatus').SetValue(Data, TValue.From(Status));
Assert(Data.FStatus = Status, 'Something wrong on assigning status trough RTTI!');
finally
Data.Free;
end;
end;
Related
I am using a generic class to allow me to access a named property of a generic type and read/write its value. I am getting an EAccessViolation error when trying to access the result from a call to GetValue from a RTTIProperty record and also when setting a value using SetValue. When running a trace it seems both errors are being thrown when access the TValue. I have included a sample console app below that highlights the issue.
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
System.RTTI;
Type
TTestClass = class
private
FItem: string;
public
Property Item: string read FItem write FItem;
end;
TAccessData<T> = class
Function GetTValue(AItem : T; AField : string) : TValue;
Procedure SetTValue(AItem : T; Afield : string; AValue : TValue);
end;
{ TAccessData<T> }
function TAccessData<T>.GetTValue(AItem: T; AField: string): TValue;
var
LContext : TRTTIContext;
LType : TRttiType;
LProperty : TRttiProperty;
begin
result := nil;
LType := LContext.GetType(Typeinfo(T));
LProperty := LType.GetProperty(Afield);
if LProperty <> nil then
Result := LProperty.GetValue(#AItem);
end;
var
LTestObj : TTestClass;
LAccessOBj : TAccessData<TTestClass>;
AValue : TValue;
procedure TAccessData<T>.SetTValue(AItem: T; Afield: string; AValue: TValue);
var
LContext : TRTTIContext;
LType : TRttiType;
LProperty : TRttiProperty;
begin
LType := LContext.GetType(Typeinfo(T));
LProperty := LType.GetProperty(Afield);
if LProperty <> nil then
LProperty.SetValue(#AItem, AValue);
end;
begin
try
LTestObj := TTestClass.Create;
LTestObj.Item := 'Hello';
Writeln(LTestObj.Item);
LAccessOBj := TAccessData<TTestClass>.Create;
AValue := LAccessObj.GetTValue(LTestObj, 'Item');
Writeln(AValue.TypeInfo^.Name);
if AValue.TypeInfo.Kind <> tkString then
Writeln('Not string');
Writeln(AValue.ToString); // <--- This results in a EAccessViolation
LAccessOBj.SetTValue(LTestObj,'Item','World'); // <--- This results in a EAccessViolation
Writeln(LTestObj.Item);
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
I suspect I am missing something in the way I am accessing the properties of the generic types but have hit a wall as to why I am getting this behaviour. I have not made a constraint on the generic parameter as I need this to work with record types as well.
Using Tokyo update 1
Your code in GetTValue and SetTValue is defect as it passes #AItem to TRttiProperty.SetValue and GetValue. It needs to be PPointer(#AItem)^ or constrain T to class so you can directly hardcast with Pointer(AItem).
Due to the wrong passed AInstance the TValue contains some garbage memory which you can see if you introduce a string variable and assign the result of the ToString call to it before trying to pass it to Writeln. And the code in Writeln is then producing the AV.
I'm trying to generalize the content validation of visual components with the Text-property using RTTI but when I try to pass a string value into TRttiMethod.Invoke, I get the Message "Invalid Typecast". (Actually "Ungültige Typumwandlung" but I guess, that was a fitting translation.)
The code below is stripped of all security measures, assertions and so on, assuming all passed objects are just perfect.
procedure ValidateTextFieldAndSetFocus(const Field: TObject; const Validator: TObject; const errorStates: array of TStringValidationResult; const sErrorMessage: string);
var
context : TRttiContext;
objField : TRttiType;
objValid : TRttiType;
prop : TRttiProperty;
execute : TRttiMethod;
I : Integer;
validResult : TStringValidationResult;
value : TValue;
begin
context := TRttiContext.Create;
objField := context.GetType(Field.ClassInfo);
objValid := context.GetType(Validator.ClassInfo);
prop := objField.GetProperty('Text');
value := prop.GetValue(Field);
execute := objValid.GetMethod('Execute');
for I := 0 to High(errorStates) do
if execute.Invoke(Validator,[value]).TryAsType<TStringValidationResult>(validResult) then
if validResult = errorStates[I] then
begin
SetFocusIfCan(Field);
raise Exception.Create(sErrorMessage);
end;
end;
The Validator's Execute only has one string-Parameter. I've seen examples where strings were passed directly into the array of TValue, but then I get the same typecast error.
edit:
The actual error appears in execute.Invoke(Validator,[value]).
Example
TNoSemicolonNullValidator = class
class function Execute(const aStr: string): TStringValidationResult;
end;
procedure TestValidation;
var
Validator : TNoSemicolonNullValidator;
begin
Validator := TNoSemicolonNullValidator.Create;
try
ValidateTextFieldAndSetFocus(Edit1,Validator,[svInvalid],'Edit1 is invalid!');
finally
Validator.Free;
end;
end;
You are calling a class function here but you are passing a TObject as first parameter (which is the hidden Self argument of non static methods). On a class method the Self parameter must not be an instance but the class of it. So the correct call would be:
execute.Invoke(validator.ClassType, [value]);
Here is a minimal example to prove that:
program Project1;
{$APPTYPE CONSOLE}
uses
Rtti,
SysUtils;
type
TValidator = class
class function Execute(const s: string): Boolean;
end;
class function TValidator.Execute(const s: string): Boolean;
begin
Writeln(s);
end;
var
ctx: TRttiContext;
v: TValidator;
begin
v := TValidator.Create;
try
ctx.GetType(TValidator).GetMethod('Execute').Invoke(v, ['test']);
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
try
ctx.GetType(TValidator).GetMethod('Execute').Invoke(v.ClassType, ['test']);
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
I'm trying to use TVirtualInterface. I've mostly tried to follow the examples at the Embarcadero doc wiki and at Nick Hodges' blog.
However, What I'm trying to do is a little bit different from the standard examples.
I have simplified the following sample code as much as I can to illustrate what I am trying to do. I have left out obvious validation and error handling code.
program VirtualInterfaceTest;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.Generics.Collections,
System.Rtti,
System.SysUtils,
System.TypInfo;
type
ITestData = interface(IInvokable)
['{6042BB6F-F30C-4C07-8D3B-C123CF1FF60F}']
function GetComment: string;
procedure SetComment(const Value: string);
property Comment: string read GetComment write SetComment;
end;
IMoreData = interface(IInvokable)
['{1D2262CE-09F4-45EC-ACD8-3EEE6B2F1548}']
function GetSuccess: Boolean;
procedure SetSuccess(const Value: Boolean);
property Success: Boolean read GetSuccess write SetSuccess;
end;
TDataHolder = class
private
FTestData: ITestData;
FMoreData: IMoreData;
public
property TestData: ITestData read FTestData write FTestData;
property MoreData: IMoreData read FMoreData write FMoreData;
end;
TVirtualData = class(TVirtualInterface)
private
FData: TDictionary<string, TValue>;
procedure DoInvoke(Method: TRttiMethod;
const Args: TArray<TValue>;
out Result: TValue);
public
constructor Create(PIID: PTypeInfo);
destructor Destroy; override;
end;
constructor TVirtualData.Create(PIID: PTypeInfo);
begin
inherited Create(PIID, DoInvoke);
FData := TDictionary<string, TValue>.Create;
end;
destructor TVirtualData.Destroy;
begin
FData.Free;
inherited Destroy;
end;
procedure TVirtualData.DoInvoke(Method: TRttiMethod;
const Args: TArray<TValue>;
out Result: TValue);
var
key: string;
begin
if (Pos('Get', Method.Name) = 1) then
begin
key := Copy(Method.Name, 4, MaxInt);
FData.TryGetValue(key, Result);
end;
if (Pos('Set', Method.Name) = 1) then
begin
key := Copy(Method.Name, 4, MaxInt);
FData.AddOrSetValue(key, Args[1]);
end;
end;
procedure InstantiateData(obj: TObject);
var
rttiContext: TRttiContext;
rttiType: TRttiType;
rttiProperty: TRttiProperty;
propertyType: PTypeInfo;
data: IInterface;
value: TValue;
begin
rttiContext := TRttiContext.Create;
try
rttiType := rttiContext.GetType(obj.ClassType);
for rttiProperty in rttiType.GetProperties do
begin
propertyType := rttiProperty.PropertyType.Handle;
data := TVirtualData.Create(propertyType) as IInterface;
value := TValue.From<IInterface>(data);
// TValueData(value).FTypeInfo := propertyType;
rttiProperty.SetValue(obj, value); // <<==== EInvalidCast
end;
finally
rttiContext.Free;
end;
end;
procedure Test_UsingDirectInstantiation;
var
dataHolder: TDataHolder;
begin
dataHolder := TDataHolder.Create;
try
dataHolder.TestData := TVirtualData.Create(TypeInfo(ITestData)) as ITestData;
dataHolder.MoreData := TVirtualData.Create(TypeInfo(IMoreData)) as IMoreData;
dataHolder.TestData.Comment := 'Hello World!';
dataHolder.MoreData.Success := True;
Writeln('Comment: ', dataHolder.TestData.Comment);
Writeln('Success: ', dataHolder.MoreData.Success);
finally
dataHolder.Free;
end;
end;
procedure Test_UsingIndirectInstantiation;
var
dataHolder: TDataHolder;
begin
dataHolder := TDataHolder.Create;
try
InstantiateData(dataHolder); // <<====
dataHolder.TestData.Comment := 'Hello World!';
dataHolder.MoreData.Success := False;
Writeln('Comment: ', dataHolder.TestData.Comment);
Writeln('Success: ', dataHolder.MoreData.Success);
finally
dataHolder.Free;
end;
end;
begin
try
Test_UsingDirectInstantiation;
Test_UsingIndirectInstantiation;
except on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
I have some arbitrary interfaces with read/write properties, ITestData and IMoreData, and a class that holds references to these interfaces, IDataHolder.
I have created a class, TVirtualData, that inherits from TVirtualInterface, following Nick Hodges' examples. And when I use this class the way I see it in all the examples, as in Test_UsingDirectInstantiation, it works swell.
What my code needs to do, however, is instantiate the interfaces in a more indirect manner, as in Test_UsingIndirectInstantiation.
The InstantiateData method uses RTTI, and works well up until the SetValue call which throws an EInvalidCast exception ("Invalid class typecast").
I added in the commented line (which I saw in some sample code from "Delphi Sorcery") to try to cast the data object to the appropriate interface. This allowed the SetValue call to run cleanly, but when I tried to access the interface property (i.e. dataHolder.TestData.Comment) it threw a EAccessViolation exception ("Access violation at address 00000000. Read of address 00000000").
For fun I replace IInterface in the InstantiateData method with ITestData, and for the first property it worked fine, but naturally, it didn't work for the second property.
Question: Is there a way to dynamically cast this TVirtualInterface object to the appropriate interface using TypeInfo or RTTI (or something else) so that the InstantiateData method has the same effect as setting the properties directly?
First you have to cast the instance to the correct interface and not IInterface. You can still store it in an IInterface variable though but it really containes the reference to the correct interface type.
Then you have to put that into a TValue with the correct type and not IInterface (RTTI is very strict about types)
The commented line you added was just to work around the second but as it was really containing the IInterface reference (and not a ITestData or TMoreData references) it resulted on the AV.
procedure InstantiateData(obj: TObject);
var
rttiContext: TRttiContext;
rttiType: TRttiType;
rttiProperty: TRttiProperty;
propertyType: PTypeInfo;
data: IInterface;
value: TValue;
begin
rttiType := rttiContext.GetType(obj.ClassType);
for rttiProperty in rttiType.GetProperties do
begin
propertyType := rttiProperty.PropertyType.Handle;
Supports(TVirtualData.Create(propertyType), TRttiInterfaceType(rttiProperty.PropertyType).GUID, data);
TValue.Make(#data, rttiProperty.PropertyType.Handle, value);
rttiProperty.SetValue(obj, value);
end;
end;
Using Delphi 2010 and RTTI, I know how to get the class type of an object and how to get/set the value and type of an object's properties, but how do you determine which class in the inheritance chain a property came from? I want to use the properties of a base class differently than the main class.
Consider this code:
TClassBase = class(TObject)
published
property A: Integer;
end;
TClassDescendant = class(TClassBase)
published
property B: Integer;
end;
procedure CheckProperties(Obj: TObject);
var
ctx: TRttiContext;
objType: TRttiType;
Prop: TRttiProperty;
begin
ctx := TRttiContext.Create;
objType := ctx.GetType(Obj.ClassInfo);
for Prop in objType.GetProperties do begin
if Prop.GetClassType is TClassBase then
// do something special with base class properties
else
// standard functionality on all other properties
end;
end;
The problem is there is no GetClassType for the properties. ClassType just returns TRttiInstancePropertyEx instead of the name of the class to which the property belongs.
Another option is use the Parent property of the TRttiProperty, from here you can access to the class which the property is part of.
{$APPTYPE CONSOLE}
{$R *.res}
uses
Rtti,
SysUtils;
type
TClassBase = class(TObject)
private
FA: Integer;
published
property A: Integer read FA;
end;
TClassDescendant = class(TClassBase)
private
FB: Integer;
published
property B: Integer read FB;
end;
procedure CheckProperties(Obj: TObject);
var
ctx: TRttiContext;
objType: TRttiType;
Prop: TRttiProperty;
begin
ctx := TRttiContext.Create;
objType := ctx.GetType(Obj.ClassInfo);
for Prop in objType.GetProperties do
if TRttiInstanceType(Prop.Parent).MetaclassType=TClassBase then
Writeln(Format('The property %s is declarated in the TClassBase class',[Prop.Name]))
else
Writeln(Format('The property %s is not declarated in the TClassBase class',[Prop.Name]))
end;
begin
try
//CheckProperties(TClassBase.Create);
CheckProperties(TClassDescendant.Create);
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
I don't know if it's possible to get the class which a property was introduced, but you can solve your problem with regular RTTI:
begin
...
for Prop in objType.GetProperties do begin
if Assigned(GetPropInfo(TClassBase, Prop.Name)) then
// do something special with base class properties
else
// standard functionality on all other properties
end;
end;
You can use the GetDeclaredProperties method to get properties declarated in the current class and then compare against the values returned by the GetProperties method.
Try this sample.
{$APPTYPE CONSOLE}
{$R *.res}
uses
Rtti,
SysUtils;
type
TClassBase = class(TObject)
private
FA: Integer;
published
property A: Integer read FA;
end;
TClassDescendant = class(TClassBase)
private
FB: Integer;
published
property B: Integer read FB;
end;
procedure CheckProperties(Obj: TObject);
function ExistProp(const PropName:string; List:TArray<TRttiProperty>) : Boolean;
var
Prop: TRttiProperty;
begin
result:=False;
for Prop in List do
if SameText(PropName, Prop.Name) then
begin
Result:=True;
break;
end;
end;
var
ctx: TRttiContext;
objType: TRttiType;
Prop: TRttiProperty;
CurrentClassProps : TArray<TRttiProperty>;
begin
ctx := TRttiContext.Create;
objType := ctx.GetType(Obj.ClassInfo);
CurrentClassProps:=objType.GetDeclaredProperties;
for Prop in objType.GetProperties do
if ExistProp(Prop.Name, CurrentClassProps) then
Writeln(Format('The property %s is declarated in the current %s class',[Prop.Name, obj.ClassName]))
else
Writeln(Format('The property %s is declarated in the base class',[Prop.Name]))
end;
begin
try
//CheckProperties(TClassBase.Create);
CheckProperties(TClassDescendant.Create);
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
I have a Delphi generic class that exposes a function with an argument of the generic type. Inside this function, I need to pass an instance of the generic type on to another object expecting a Variant type. Similar to this:
type
IMyInterface = interface
DoStuff(Value: Variant);
end;
TMyClass<T> = class
FMyIntf: IMyInterface
procedure DoStuff(SomeValue: T);
end;
[...]
procedure MyClass<T>.DoStuff(SomeValue: T);
begin
FMyIntf.DoStuff((*convert SomeValue to Variant here*));
end;
I tried using Rtti.TValue.From(SomeValue).AsVariant. This worked for integral types, but blew up for Booleans. I don't quite see why, since normally I'd be able to assign a Boolean value to a Variant...
Is there a better way to make this conversion? I only need it to work for simple built-in types (excluding enumerations and records)
I think there is no direct way to convert generic type to variant because variant cannot hold all the possible types. You must write your specific conversion routine. E.g.:
interface
//...
type
TDemo = class
public
class function GetAsVariant<T>(const AValue: T): Variant;
end;
//...
implementation
uses
Rtti,
TypInfo;
//...
{ TDemo}
class function TDemo.GetAsVariant<T>(const AValue: T): Variant;
var
val: TValue;
bRes: Boolean;
begin
val := TValue.From<T>(AValue);
case val.Kind of
tkInteger: Result := val.AsInteger;
tkInt64: Result := val.AsInt64;
tkEnumeration:
begin
if val.TryAsType<Boolean>(bRes) then
Result := bRes
else
Result := val.AsOrdinal;
end;
tkFloat: Result := val.AsExtended;
tkString, tkChar, tkWChar, tkLString, tkWString, tkUString:
Result := val.AsString;
tkVariant: Result := val.AsVariant
else
begin
raise Exception.Create('Unsupported type');
end;
end;
end;
Because TValue.AsVariant handles most of the type conversions internally, this function can be simplified. I will handle enumerations in case you could need them later:
class function TDemo.GetAsVariant<T>(const AValue: T): Variant;
var
val: TValue;
begin
val := TValue.From<T>(AValue);
case val.Kind of
tkEnumeration:
begin
if val.TypeInfo = TypeInfo(Boolean) then
Result := val.AsBoolean
else
Result := val.AsOrdinal;
end
else
begin
Result := val.AsVariant;
end;
end;
Possible usage:
var
vValue: Variant;
begin
vValue := TDemo.GetAsVariant<Boolean>(True);
Assert(vValue = True); //now vValue is a correct Boolean
Looks like in my Delphi version 10.2 the Boolean problem is gone and TValue.From<T>(FValue).AsVariant is enough.
Here an example with some other helpful things like comparing the generic type:
TMyValue<T> = class(TPersistent)
private
FValue: T;
procedure SetValue(const AValue: T);
function GetAsVariant: Variant; override;
public
procedure Assign(Source: TPersistent); override;
property Value: T read FValue write SetValue;
property AsVariant: Variant read GetAsVariant;
end;
function TMyValue<T>.GetAsVariant: Variant;
begin
Result:= TValue.From<T>(FValue).AsVariant;
end;
procedure TMyValue<T>.SetValue(const AValue: T);
begin
if TEqualityComparer<T>.Default.Equals(AValue, FValue) then Exit;
FValue:= AValue;
//do something
end;
procedure TMyValue<T>.Assign(Source: TPersistent);
begin
if Source is TMyValue<T> then Value:= (Source as TMyValue<T>).Value
else inherited;
end;
Another way (tested XE10)
Var
old : variant;
val : TValue;
Begin
val := TValue.FromVariant(old);
End;