I am trying to set the values of a record using RTTI in Delphi XE. I can get the value from the record using the GetValue method, but I can not set the value using the SetValue method.
Does anyone know how to do this / why it does not work?
Thanks in advance!
My context: Final goal is to write a component which will read any XML file and fill the application's data model with the data from the XML automagically. The datamodel will be annotated to detemine the XPaths for all elements. For objects and basic data types I already have it up and running.
TSize = record
X, Y: double;
end;
TMyTest = class
protected
FSize: TSize;
public
constructor Create;
procedure DoStuff;
end;
constructor TMyTest.Create;
begin
FSize.X := 2.7;
FSize.Y := 3.1;
end;
procedure TMyTest.DoStuff;
var
MyContext: TRttiContext;
MyField: TRttiField;
MySizeField: TRttiField;
MyVal: TValue;
MyRecord: TRttiRecordType;
NewVal: TValue;
begin
// Explicit Create of MyContext does not help (as expected)
for MyField in MyContext.GetType(ClassType).GetFields do
if MyField.Name = 'FSize' then //For debugging
begin
MyRecord := MyField.FieldType.AsRecord;
MyVal := MyField.GetValue(Self);
for MySizeField in MyRecord.GetFields do
begin
//This works
NewVal := MySizeField.GetValue(MyVal.GetReferenceToRawData).AsExtended;
NewVal := NewVal.AsExtended + 5.0;
try
// This does not work. (no feedback)
MySizeField.SetValue(MyVal.GetReferenceToRawData, NewVal);
// This however does work. Now to find out what the difference between the two is.
MySizeField.SetValue(@FSize, NewVal);
except
on e: Exception do //Never happens
ShowMessage('Oops!' + sLineBreak + e.Message);
end;
end;
end;
// Shows 'X=2.7 Y=3.1'
// Expected 'X=7.7 Y=8.1'
ShowMessage(Format('X=%f Y=%f', [FSize.X, FSize.Y]));
end;
If TSize is declared as class, then MyVal.GetReferenceToRawData
in the code should be replaced by TObject(MyVal.GetReferenceToRawData^)
. If you do this, it all works as expected. (Yes, MyVal.AsObject
would also do the trick in that case)
This leads me to a possible solution: Typecast MyVal.GetReferenceToRawData^
to the correct record type. How can this be done though?
I just tried using the @FSize directly in SetValue. This works, as you would expect. This triggers the question: What is different between @FSize and MyVal.GetReferenceToRawData
After lots of further investigations, I found out that MyVal was actually a copy of the record, so indeed the values were set correctly as Serg mentioned in his first answer, however, it was set in a copy. I feel so silly for not realizing this earlier...
Anyway, below is a code sample that does work. Also, if you are willing to use "fieldname paths" you can have a look at Barry Kelly's approach in this post, which put me on track. I just didn't like the paths needed for the Follow
routine.
procedure TMyTest.DoStuff;
var
MyContext: TRttiContext;
MyField: TRttiField;
MySizeField: TRttiField;
NewVal: TValue;
dMyVal: double;
begin
for MyField in MyContext.GetType(ClassType).GetFields do
if MyField.Name = 'FSize' then
begin
for MySizeField in MyField.FieldType.GetFields do
begin
dMyVal := MySizeField.GetValue(PByte(Self) + MyField.Offset).AsExtended;
NewVal := TValue.From(dMyVal + 5.1);
try
MySizeField.SetValue(PByte(Self) + MyField.Offset, NewVal);
except
on e: Exception do
ShowMessage('Oops!' + sLineBreak + e.Message);
end;
end;
end;
if FSize.X > 5.0 then
ShowMessage(Format('X=%f Y=%f', [FSize.X, FSize.Y]));
end;