3
votes

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;
3
Your code works fine, new value is set ok. Why do you think it does not work?kludg
Serg, I added a showmessage to my code at the end. The code compiles, but the values shown are the values set in the constructor. I'd expect them to be 5.0 higher.deColaman

3 Answers

1
votes

MyVal.GetReferenceToRawData does not return a pointer to the FSize field, that is why FSize field value does not change.

I am not sure that my code is what are you trying to write, but I hope it will help:

uses rtti, typinfo;

type
  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;
  MyVal: TValue;
  NewSize: TSize;

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
      MyVal := MyField.GetValue(Self);
      NewSize:= MyVal.AsType<TSize>;
      NewSize.X:= NewSize.X + 5;
      NewSize.Y:= NewSize.Y + 5;
      TValue.Make(@NewSize, TypeInfo(TSize), MyVal);
      MyField.SetValue(Self, MyVal);
    end;
  ShowMessage(Format('X=%f Y=%f', [FSize.X, FSize.Y]));
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  Test: TMyTest;

begin
  Test:= TMyTest.Create;
  Test.DoStuff;
  Test.Free;
end;
1
votes

I had the same difficulty, but in my case I was using properties and not fields and it was quite complicated because TRttiProperty has no property Offset to solve the problem after searching lot and run several tests, I found a solution and I am sharing here.

procedure TMyTest.DoStuff;
var
  MyContext: TRttiContext;
  MyProp: TRttiProperty;
  MySizeField: TRttiField;
  NewVal: TValue;
  dMyVal: double;
  MyPointer:Pointer;
begin
  for MyProp in MyContext.GetType(ClassType).GetProperties do
    if MyProp.Name = 'Size' then
    begin
      MyPointer := TRttiInstanceProperty(MyProp).PropInfo^.GetProc;
      for MySizeField in MyProp.PropertyType.GetFields do
      begin
        dMyVal := MySizeField.GetValue(PByte(Self) + Smallint(MyPointer)).AsExtended;
        NewVal := TValue.From(dMyVal + 5.1);
        try
          MySizeField.SetValue(PByte(Self) + Smallint(MyPointer), 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;
-2
votes

RTTI works best on Published properties. So if you add

published property size: TSize read fSize write fSize;

to your class I think you will have better results.