How to set record values using RTTI in Delphi XE

delphi, delphi-xe, record, rtti, setvalue

Solution

`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;

Problem

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; ```

Original source

Related problems