Repeating procedure for every item in class - delphi

Data.XX.NewValue := Data.XX.SavedValue;
Data.XX.OldValue := Data.XX.SavedValue;
I need to do the above a large number of times, where XX represents the value in the class. Pretending there were 3 items in the list: Tim, Bob, Steve. Is there any way to do the above for all three people without typing out the above code three times?
(Data is a class containing a number of Objects, each type TList, which contain OldValue, NewValue and SavedValue)

What I'd do if I had to do something like this is put one more TList on Data, which holds a list of all the Objects on it. Fill it in the constructor, and then when you have to do something like this, use a loop to apply the same basic operation to each item in the list.

Maybe I'm not understanding it ok but...
Here is where Object Oriented shines. You define a procedure for the class and then apply for any instance you create.
TMyPropValue = class(TObject)
private
FNewValue: double;
FOldValue: double;
procedure SetValue(AValue: double);
public
procedure RestoreOldValue;
propety NewValue: double read FNewValue write SetValue; // Raed/write property (write using a procedure)
property OldValue: double read FOldValue; // Read only property
end;
TMyClass = class(TObject)
private
FProp1: TMyPropValue;
FProp2: TMyPropValue;
public
procedure RestoreValues;
end;
//....
var
MyObj1: TMyClass;
MyObj2: TMyclass;
procedure TMyPropValue.SetValue(AValue: double);
begin
FOldValue := FNewValue;
FNewValue := AValue;
end;
// Restore the Old value of this Prop
procedure TMyPropValue.RestoreOldValue;
begin
FNewValue := FOldValue;
end;
// Restore ald the Values of the class
procedure TMyClass.RestoreValues;
begin
FProp1.RestoreOldValue;
FProp2.RestoreOldValue;
end;
// -----------
// Creating and populating a couple of objects (instances)
procedure XXX;
begin
MyObj1 := TMyClass.Create;
MyObj1.Prop1.NewValue := 10.25:
MyObj1.Prop2.NewValue := 99.10:
MyObj2 := TMyClass.Create;
MyObj2.Prop1.NewValue := 75.25:
MyObj2.Prop2.NewValue := 60.30:
end;
// Changing values, the class internaly will save the OldValue
procedure yyyy;
begin
MyObj1.Prop1.NewValue := 85.26:
MyObj1.Prop2.NewValue := 61.20:
MyObj2.Prop1.NewValue := 99.20:
MyObj2.Prop2.NewValue := 55.23:
end;
// Using a procedure from the class
procedure zzzz;
begin
MyObj1.RestoreValues;
MyObj2.RestoreValues;
end;
Hope this help
Daniel

Judging from this post and this post, I would suggest the following :
unit MyAssignment;
interface
type
TValueKind = ( EconomicGrowth,
Inflation,
Unemployment,
CurrentAccountPosition,
AggregateSupply,
AggregateDemand,
ADGovernmentSpending,
ADConsumption,
ADInvestment,
ADNetExports,
OverallTaxation,
GovernmentSpending,
InterestRates,
IncomeTax,
Benefits,
TrainingEducationSpending );
TValue = record
NewValue,
OldValue,
SavedValue : Double;
procedure SetValue( aVal : Double );
procedure SaveValue();
procedure RestoreValue();
end;
TDataArray = array [TValueKind] of TValue;
var
Data : TDataArray;
implementation
{TValue}
procedure TValue.SetValue( aVal : Double );
begin
OldValue := NewValue;
NewValue := aVal;
end;
procedure TValue.SaveValue;
begin
SavedValue := NewValue;
end;
procedure TValue.RestoreValue;
begin
NewValue := SavedValue;
OldValue := SavedValue;
end;
end.
Now you can write this kind of code :
//accessing the values :
// Data[XX] instead of Data.XX
//examples :
ShowMessage(FloatToStr(Data[Inflation].SavedValue));
Data[AgregateSupply].SetValue( 10.0 );
Data[Benefits].SaveValue;
//writing loops :
procedure RestoreValues( var aData : TDataArray ); //the "var" keyword is important here : google "arguments by value" "arguments by reference"
var
lKind : TValueKind;
begin
for lKind := Low(TValueKind) to High(TValueKind) do
aData[lKind].RestoreValue;
end;
procedure SaveValues( var aData : TDataArray );
var
lKind : TValueKind;
begin
for lKind := Low(TValueKind) to High(TValueKind) do
aData[lKind].RestoreValue;
end;
//calling these functions :
SaveValues( Data );
RestoreValues( Data );
If you need more complex manipulations on the array, it would be a good idea to put it into a class - replace the fields you wrote with only on efield of type TDataArray - and write the functions to manipulate the data as methods of this class.

I would be careful here. I know the temptation is going to be to use a common interface and reflection, or some other automation that is more flexible and, frankly, more fun to write. Avoid this temptation. There is nothing wrong with listing every item in the list out according to your pattern. Patterns are good, and the code will be readable, easy to execute, and easy to modify any individual property that does not fit the pattern.
The low tech way to avoid typing everything out is to use our old friend Excel. Put all your properties in Column A, and then use this formula in column B:
= CONCATENATE("Data.", A1, ".NewValue := Data.", A1, ".SavedValue;", CHAR(10), "Data.", A1, ".OldValue := Data.", A1, ".SavedValue;", CHAR(10))

Related

How to get value from field of superior record when it is setting property of subordinate record

In our project we have these structures and variable:
TPart = record
private
...
FSize: Integer;
procedure SetSize(const Value: Integer);
public
...
property Size : Integer read FSize write SetSize;
end;
TMain = record
...
material : Byte;
parts : array [1..10] of TPart;
end;
TAMain = array [1..200] of TMain;
var
whole : TAMain;
procedure TPart.SetSize(const Value: Integer);
begin
FSize := Value;
// need to know material current TMain
end;
Whenever procedure SetSize occurs
whole[x].parts[y].Size := ...;
we need to check value in material field of current TMain. (Because when the size is bigger than certain value, it is necessary to change material).
You need to have a pointer to the "main" record for each part. You can do it like this:
type
PMain = ^TMain;
TPart = record
private
//...
FSize : Integer;
FMain : PMain;
procedure SetSize(const Value: Integer);
public
//...
property Size : Integer read FSize write SetSize;
property Main : PMain read FMain write FMain;
end;
TMain = record
//...
material : Byte;
parts : array [1..10] of TPart;
end;
TAMain = array [1..200] of TMain;
procedure TPart.SetSize(const Value: Integer);
begin
FSize := Value;
// need to know material current TMain
if not Assigned(FMain) then
raise Exception.Create('Main not assigned for that part');
if FMain.material = 123 then begin
// Do something
end;
end;
For this to work, you have to assign TPart.Main property before it is needed. You didn't show how TPart records are created in your application. One way to do it is to add a method AddPart() in TMain. Then inside that method, it is easy to assign the "Main" property in the added part.
And by the way, using records is probably not the best design for this. Using classes as suggested Andreas Rejbrand if probably a better idea. The code is almost the same except there is no more explicit pointer. Just a reference to the main instance.

Storing the value of variable using TRadioGroup

I want to change the value of T according to a particular selection but it's not changing. Please have a look. The variable T has been declared along with Form1:TForm1 before 'implementation'. Basically, T should get assigned a linear or non linear equation depending upon the the selection of the respected radio buttons. I put a TEdit in the form so as to get an idea whether it is working or not. The last part is just a way to check by taking an example of Integer values.
Also, if I am not able to give a clear idea then just suggest me how to store a value of the concerned value using the Radiobuttons of the RadioGroup.
procedure TForm1.RadioGroup1Click(Sender: TObject);
begin
if RadioGroup1.Items[RadioGroup1.ItemIndex] = 'Linear Tension' then
T:= 5;
if RadioGroup1.Items[RadioGroup1.ItemIndex] = 'Non-Linear tension' then
T:= 10;
end;
procedure TForm1.Edit1Change(Sender: TObject);
var
code: Integer;
value: Real;
begin
Val(Edit1.Text,value,code);
Edit1.Text := formatfloat('#.0', T);
end;
end.
It's really not a good idea to use a textual comparison for RadioGroup items. It's much better to simply use the ItemIndex directly:
procedure TForm1.RadioGroup1Click(Sender: TObject);
begin
case RadioGroup1.ItemIndex of
0: T := 5;
1: T := 10;
else
raise Exception.Create('No item selected - should not get here');
end;
ShowMessage(FloatToStr(T));
end;
Do not compare the captions because you will have magic values in your code.
Declare a ValueObject containing the Value and the Name
type
TTensionValue = record
private
FValue : Integer;
FName : string;
public
constructor Create( AValue : Integer; const AName : string );
class function EMPTY : TTensionValue;
property Value : Integer read FValue;
property Name : string;
end;
TTensionValues = TList<TTensionValue>;
class function TTensionValue.EMPTY : TTensionValue;
begin
Result.FValue := 0;
Result.FName := '';
end;
constructor TTensionValue.Create( AValue : Integer; const AName : string );
begin
// Validation of AValue and AName
if AName = '' then
raise Exception.Create( 'AName' );
if AValue < 0 then
raise Exception.Create( 'AValue' );
FValue := AValue;
FName := AName;
end;
Prepare a List with valid entries
type
TForm1 = class( TForm )
...
procedure RadioGroup1Click( Sender: TObject );
private
FTensions : TTensionValues;
procedure PopulateTensions( AStrings : TStrings );
public
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
end;
procedure TForm1.AfterConstruction;
begin
inherited;
FTensions := TTensionValues.Create;
FTensions.Add( TTensionValue.Create( 5, 'Linear Tension' ) );
FTensions.Add( TTensionValue.Create( 10, 'Non-Linear tension' ) );
end;
procedure TForm1.BeforeDestruction;
begin
FTenstions.Free;
inherited;
end;
Populate that list to the RadioGroup
procedure TForm1.PopulateTensions( AStrings : TStrings );
var
LValue : TTensionValue;
begin
AStrings.BeginUpdate;
try
AStrings.Clear;
for LValue in FTensions.Count - 1 do
AStrings.Add( LValue.Name );
finally
AStrings.EndUpdate;
end;
end;
procedure TForm1.FormShow( Sender.TObject );
begin
PopulateTensions( RadioGroup1.Items );
end;
Now you only ask the TensionList for the value
procedure TForm1.RadioGroup1Click( Sender: TObject );
begin
T := FTensions[RadioGroup1.ItemIndex].Value;
end;
The selected value now only rely on the chosen ItemIndex and not on the caption text.
From what I can tell, you're simply trying to change the value displayed on Edit1 when RadioGroup1 is clicked. To achieve this, all you'll need to do is move
Edit1.Text := formatfloat('#.0', T);
to the end of your RadioGroup1Click procedure.
I'm assuming Edit1Change is the onChange procedure of Edit1. If so, according to the documentation this procedure only gets called when the Text property already might have changed. So not only will this procedure not get called (how would delphi know you intend to use the value of T to change the text of Edit1?), when it does get called, it might result in a stack overflow, since changing the text value indirectly calls the onChange event. (though setting it to the same value it already had might not call it).
That being said, checking if a value is being changed properly, does not require a TEdit, a TLabel would be a better fit there. Though in your case, i would opt for simply placing a breakpoint and stepping through the code to see if the value get's changed correctly.
There are also some a lot of additional problems with your code, such as inconsistent formatting, magic values, bad naming conventions and lines of code that serve no purpose, I would suggest you read up on those before you get into bad habits.

How to loop all properties in a Class

I have a class in my Delphi app where I would like an easy and dynamic way of resetting all the string properties to '' and all the boolean properties to False
As far as I can see on the web it should be possible to make a loop of some sort, but how to do it isn't clear to me.
if you are an Delphi 2010 (and higher) user then there is a new RTTI unit (rtti.pas). you can use it to get runtime information about your class and its properties (public properties by default, but you can use {$RTTI} compiler directive to include protected and private fields information).
For example we have next test class with 3 public fields (1 boolean and 2 string fields (one of them is readonly)).
TTest = class(TObject)
strict private
FString1 : string;
FString2 : string;
FBool : boolean;
public
constructor Create();
procedure PrintValues();
property String1 : string read FString1 write FString1;
property String2 : string read FString2;
property BoolProp : boolean read FBool write FBool;
end;
constructor TTest.Create();
begin
FBool := true;
FString1 := 'test1';
FString2 := 'test2';
end;
procedure TTest.PrintValues();
begin
writeln('string1 : ', FString1);
writeln('string2 : ', FString2);
writeln('bool: ', BoolToStr(FBool, true));
end;
to enumerate all properties of object and set it values to default you can use something like code below.
First at all you have to init TRttiContext structure (it is not neccesary, because it is a record). Then you should get rtti information about your obejct, after that you can loop your properties and filter it (skip readonly properties and other than boolean and stirng). Take into account that there are few kind of strings : tkUString, tkString and others (take a look at TTypeKind in typinfo.pas)
TObjectReset = record
strict private
public
class procedure ResetObject(obj : TObject); static;
end;
{ TObjectReset }
class procedure TObjectReset.ResetObject(obj: TObject);
var ctx : TRttiContext;
rt : TRttiType;
prop : TRttiProperty;
value : TValue;
begin
ctx := TRttiContext.Create();
try
rt := ctx.GetType(obj.ClassType);
for prop in rt.GetProperties() do begin
if not prop.IsWritable then continue;
case prop.PropertyType.TypeKind of
tkEnumeration : value := false;
tkUString : value := '';
else continue;
end;
prop.SetValue(obj, value);
end;
finally
ctx.Free();
end;
end;
simple code to test:
var t : TTest;
begin
t := TTest.Create();
try
t.PrintValues();
writeln('reset values'#13#10);
TObjectReset.ResetObject(t);
t.PrintValues();
finally
readln;
t.Free();
end;
end.
and result is
string1 : test1
string2 : test2
bool: True
reset values
string1 :
string2 : test2
bool: False
also take a look at Attributes, imo it is good idea to mark properties (wich you need to reset) with some attribute, and may be with default value like:
[ResetTo('my initial value')]
property MyValue : string read FValue write FValue;
then you can filter only properties wich are marked with ResetToAttribute
Please note, the following code works only for published properties of a class! Also, the instance of a class passed to the function below must have at least published section defined!
Here is how to set the published string property values to an empty string and boolean values to False by using the old style RTTI.
If you have Delphi older than Delphi 2009 you might be missing the tkUString type. If so, simply removeit from the following code:
uses
TypInfo;
procedure ResetPropertyValues(const AObject: TObject);
var
PropIndex: Integer;
PropCount: Integer;
PropList: PPropList;
PropInfo: PPropInfo;
const
TypeKinds: TTypeKinds = [tkEnumeration, tkString, tkLString, tkWString,
tkUString];
begin
PropCount := GetPropList(AObject.ClassInfo, TypeKinds, nil);
GetMem(PropList, PropCount * SizeOf(PPropInfo));
try
GetPropList(AObject.ClassInfo, TypeKinds, PropList);
for PropIndex := 0 to PropCount - 1 do
begin
PropInfo := PropList^[PropIndex];
if Assigned(PropInfo^.SetProc) then
case PropInfo^.PropType^.Kind of
tkString, tkLString, tkUString, tkWString:
SetStrProp(AObject, PropInfo, '');
tkEnumeration:
if GetTypeData(PropInfo^.PropType^)^.BaseType^ = TypeInfo(Boolean) then
SetOrdProp(AObject, PropInfo, 0);
end;
end;
finally
FreeMem(PropList);
end;
end;
Here is a simple test code (note the properties must be published; if there are no published properties in the class, at least empty published section must be there):
type
TSampleClass = class(TObject)
private
FStringProp: string;
FBooleanProp: Boolean;
published
property StringProp: string read FStringProp write FStringProp;
property BooleanProp: Boolean read FBooleanProp write FBooleanProp;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
SampleClass: TSampleClass;
begin
SampleClass := TSampleClass.Create;
try
SampleClass.StringProp := 'This must be cleared';
SampleClass.BooleanProp := True;
ResetPropertyValues(SampleClass);
ShowMessage('StringProp = ' + SampleClass.StringProp + sLineBreak +
'BooleanProp = ' + BoolToStr(SampleClass.BooleanProp));
finally
SampleClass.Free;
end;
end;

Rtti accessing fields and properties in complex data structures

As already discussed in Rtti data manipulation and consistency in Delphi 2010 a consistency between the original data and rtti values can be reached by accessing members by using a pair of TRttiField and an instance pointer. This would be very easy in case of a simple class with only basic member types (like e.g. integers or strings).
But what if we have structured field types?
Here is an example:
TIntArray = array [0..1] of Integer;
TPointArray = array [0..1] of Point;
TExampleClass = class
private
FPoint : TPoint;
FAnotherClass : TAnotherClass;
FIntArray : TIntArray;
FPointArray : TPointArray;
public
property Point : TPoint read FPoint write FPoint;
//.... and so on
end;
For an easy access of Members I want to buil a tree of member-nodes, which provides an interface for getting and setting values, getting attributes, serializing/deserializing values and so on.
TMemberNode = class
private
FMember : TRttiMember;
FParent : TMemberNode;
FInstance : Pointer;
public
property Value : TValue read GetValue write SetValue; //uses FInstance
end;
So the most important thing is getting/setting the values, which is done - as stated before - by using the GetValue and SetValue functions of TRttiField.
So what is the Instance for FPoint members? Let's say Parent is the Node for TExample class, where the instance is known and the member is a field, then Instance would be:
FInstance := Pointer (Integer (Parent.Instance) + TRttiField (FMember).Offset);
But what if I want to know the Instance for a record property? There is no offset in this case. So is there a better solution to get a pointer to the data?
For the FAnotherClass member, the Instance would be:
FInstance := Parent.Value.AsObject;
So far the solution works, and data manipulation can be done by using rtti or the original types, without losing information.
But things get harder, when working with arrays. Especially the second array of Points. How can I get the instance for the members of points in this case?
TRttiField.GetValue where the field's type is a value type gets you a copy. This is by design. TValue.MakeWithoutCopy is for managing reference counts on things like interfaces and strings; it is not for avoiding this copy behaviour. TValue is intentionally not designed to mimic Variant's ByRef behaviour, where you can end up with references to (e.g.) stack objects inside a TValue, increasing the risk of stale pointers. It would also be counter-intuitive; when you say GetValue, you should expect a value, not a reference.
Probably the most efficient way to manipulate values of value types when they are stored inside other structures is to step back and add another level of indirection: by calculating offsets rather than working with TValue directly for all the intermediary value typed steps along the path to the item.
This can be encapsulated fairly trivially. I spent the past hour or so writing up a little TLocation record which uses RTTI to do this:
type
TLocation = record
Addr: Pointer;
Typ: TRttiType;
class function FromValue(C: TRttiContext; const AValue: TValue): TLocation; static;
function GetValue: TValue;
procedure SetValue(const AValue: TValue);
function Follow(const APath: string): TLocation;
procedure Dereference;
procedure Index(n: Integer);
procedure FieldRef(const name: string);
end;
function GetPathLocation(const APath: string; ARoot: TLocation): TLocation; forward;
{ TLocation }
type
PPByte = ^PByte;
procedure TLocation.Dereference;
begin
if not (Typ is TRttiPointerType) then
raise Exception.CreateFmt('^ applied to non-pointer type %s', [Typ.Name]);
Addr := PPointer(Addr)^;
Typ := TRttiPointerType(Typ).ReferredType;
end;
procedure TLocation.FieldRef(const name: string);
var
f: TRttiField;
begin
if Typ is TRttiRecordType then
begin
f := Typ.GetField(name);
Addr := PByte(Addr) + f.Offset;
Typ := f.FieldType;
end
else if Typ is TRttiInstanceType then
begin
f := Typ.GetField(name);
Addr := PPByte(Addr)^ + f.Offset;
Typ := f.FieldType;
end
else
raise Exception.CreateFmt('. applied to type %s, which is not a record or class',
[Typ.Name]);
end;
function TLocation.Follow(const APath: string): TLocation;
begin
Result := GetPathLocation(APath, Self);
end;
class function TLocation.FromValue(C: TRttiContext; const AValue: TValue): TLocation;
begin
Result.Typ := C.GetType(AValue.TypeInfo);
Result.Addr := AValue.GetReferenceToRawData;
end;
function TLocation.GetValue: TValue;
begin
TValue.Make(Addr, Typ.Handle, Result);
end;
procedure TLocation.Index(n: Integer);
var
sa: TRttiArrayType;
da: TRttiDynamicArrayType;
begin
if Typ is TRttiArrayType then
begin
// extending this to work with multi-dimensional arrays and non-zero
// based arrays is left as an exercise for the reader ... :)
sa := TRttiArrayType(Typ);
Addr := PByte(Addr) + sa.ElementType.TypeSize * n;
Typ := sa.ElementType;
end
else if Typ is TRttiDynamicArrayType then
begin
da := TRttiDynamicArrayType(Typ);
Addr := PPByte(Addr)^ + da.ElementType.TypeSize * n;
Typ := da.ElementType;
end
else
raise Exception.CreateFmt('[] applied to non-array type %s', [Typ.Name]);
end;
procedure TLocation.SetValue(const AValue: TValue);
begin
AValue.Cast(Typ.Handle).ExtractRawData(Addr);
end;
This type can be used to navigate locations within values using RTTI. To make it slightly easier to use, and slightly more fun for me to write, I also wrote a parser - the Follow method:
function GetPathLocation(const APath: string; ARoot: TLocation): TLocation;
{ Lexer }
function SkipWhite(p: PChar): PChar;
begin
while IsWhiteSpace(p^) do
Inc(p);
Result := p;
end;
function ScanName(p: PChar; out s: string): PChar;
begin
Result := p;
while IsLetterOrDigit(Result^) do
Inc(Result);
SetString(s, p, Result - p);
end;
function ScanNumber(p: PChar; out n: Integer): PChar;
var
v: Integer;
begin
v := 0;
while (p >= '0') and (p <= '9') do
begin
v := v * 10 + Ord(p^) - Ord('0');
Inc(p);
end;
n := v;
Result := p;
end;
const
tkEof = #0;
tkNumber = #1;
tkName = #2;
tkDot = '.';
tkLBracket = '[';
tkRBracket = ']';
var
cp: PChar;
currToken: Char;
nameToken: string;
numToken: Integer;
function NextToken: Char;
function SetToken(p: PChar): PChar;
begin
currToken := p^;
Result := p + 1;
end;
var
p: PChar;
begin
p := cp;
p := SkipWhite(p);
if p^ = #0 then
begin
cp := p;
currToken := tkEof;
Exit(currToken);
end;
case p^ of
'0'..'9':
begin
cp := ScanNumber(p, numToken);
currToken := tkNumber;
end;
'^', '[', ']', '.': cp := SetToken(p);
else
cp := ScanName(p, nameToken);
if nameToken = '' then
raise Exception.Create('Invalid path - expected a name');
currToken := tkName;
end;
Result := currToken;
end;
function Describe(tok: Char): string;
begin
case tok of
tkEof: Result := 'end of string';
tkNumber: Result := 'number';
tkName: Result := 'name';
else
Result := '''' + tok + '''';
end;
end;
procedure Expect(tok: Char);
begin
if tok <> currToken then
raise Exception.CreateFmt('Expected %s but got %s',
[Describe(tok), Describe(currToken)]);
end;
{ Semantic actions are methods on TLocation }
var
loc: TLocation;
{ Driver and parser }
begin
cp := PChar(APath);
NextToken;
loc := ARoot;
// Syntax:
// path ::= ( '.' <name> | '[' <num> ']' | '^' )+ ;;
// Semantics:
// '<name>' are field names, '[]' is array indexing, '^' is pointer
// indirection.
// Parser continuously calculates the address of the value in question,
// starting from the root.
// When we see a name, we look that up as a field on the current type,
// then add its offset to our current location if the current location is
// a value type, or indirect (PPointer(x)^) the current location before
// adding the offset if the current location is a reference type. If not
// a record or class type, then it's an error.
// When we see an indexing, we expect the current location to be an array
// and we update the location to the address of the element inside the array.
// All dimensions are flattened (multiplied out) and zero-based.
// When we see indirection, we expect the current location to be a pointer,
// and dereference it.
while True do
begin
case currToken of
tkEof: Break;
'.':
begin
NextToken;
Expect(tkName);
loc.FieldRef(nameToken);
NextToken;
end;
'[':
begin
NextToken;
Expect(tkNumber);
loc.Index(numToken);
NextToken;
Expect(']');
NextToken;
end;
'^':
begin
loc.Dereference;
NextToken;
end;
else
raise Exception.Create('Invalid path syntax: expected ".", "[" or "^"');
end;
end;
Result := loc;
end;
Here's an example type, and a routine (P) that manipulates it:
type
TPoint = record
X, Y: Integer;
end;
TArr = array[0..9] of TPoint;
TFoo = class
private
FArr: TArr;
constructor Create;
function ToString: string; override;
end;
{ TFoo }
constructor TFoo.Create;
var
i: Integer;
begin
for i := Low(FArr) to High(FArr) do
begin
FArr[i].X := i;
FArr[i].Y := -i;
end;
end;
function TFoo.ToString: string;
var
i: Integer;
begin
Result := '';
for i := Low(FArr) to High(FArr) do
Result := Result + Format('(%d, %d) ', [FArr[i].X, FArr[i].Y]);
end;
procedure P;
var
obj: TFoo;
loc: TLocation;
ctx: TRttiContext;
begin
obj := TFoo.Create;
Writeln(obj.ToString);
ctx := TRttiContext.Create;
loc := TLocation.FromValue(ctx, obj);
Writeln(loc.Follow('.FArr[2].X').GetValue.ToString);
Writeln(obj.FArr[2].X);
loc.Follow('.FArr[2].X').SetValue(42);
Writeln(obj.FArr[2].X); // observe value changed
// alternate syntax, not using path parser, but location destructive updates
loc.FieldRef('FArr');
loc.Index(2);
loc.FieldRef('X');
loc.SetValue(24);
Writeln(obj.FArr[2].X); // observe value changed again
Writeln(obj.ToString);
end;
The principle can be extended to other types and Delphi expression syntax, or TLocation may be changed to return new TLocation instances rather than destructive self-updates, or non-flat array indexing may be supported, etc.
You're touching a few concepts and problems with this question. First of all you've mixed in some record types and some properties, and I'd like to handle this first. Then I'll give you some short info on how to read the "Left" and "Top" fields of a record when that record is part of an field in a class... Then I'll give you suggestions on how to make this work generically. I'm probably going to explain a bit more then it's required, but it's midnight over here and I can't sleep!
Example:
TPoint = record
Top: Integer;
Left: Integer;
end;
TMyClass = class
protected
function GetMyPoint: TPoint;
procedure SetMyPoint(Value:TPoint);
public
AnPoint: TPoint;
property MyPoint: TPoint read GetMyPoint write SetMyPoint;
end;
function TMyClass.GetMyPoint:Tpoint;
begin
Result := AnPoint;
end;
procedure TMyClass.SetMyPoint(Value:TPoint);
begin
AnPoint := Value;
end;
Here's the deal. If you write this code, at runtime it will do what it seems to be doing:
var X:TMyClass;
x.AnPoint.Left := 7;
But this code will not work the same:
var X:TMyClass;
x.MyPoint.Left := 7;
Because that code is equivalent to:
var X:TMyClass;
var tmp:TPoint;
tmp := X.GetMyPoint;
tmp.Left := 7;
The way to fix this is to do something like this:
var X:TMyClass;
var P:TPoint;
P := X.MyPoint;
P.Left := 7;
X.MyPoint := P;
Moving on, you want to do the same with RTTI. You may get RTTI for both the "AnPoint:TPoint" field and for the "MyPoint:TPoint" field. Because using RTTI you're essentially using a function to get the value, you'll need do use the "Make local copy, change, write back" technique with both (the same kind of code as for the X.MyPoint example).
When doing it with RTTI we'll always start from the "root" (a TExampleClass instance, or a TMyClass instance) and use nothing but a series of Rtti GetValue and SetValue methods to get the value of the deep field or set the value of the same deep field.
We'll assume we have the following:
AnPointFieldRtti: TRttiField; // This is RTTI for the AnPoint field in the TMyClass class
LeftFieldRtti: TRttiField; // This is RTTI for the Left field of the TPoint record
We want to emulate this:
var X:TMyClass;
begin
X.AnPoint.Left := 7;
end;
We'll brake that into steps, we're aiming for this:
var X:TMyClass;
V:TPoint;
begin
V := X.AnPoint;
V.Left := 7;
X.AnPoint := V;
end;
Because we want to do it with RTTI, and we want it to work with anything, we will not use the "TPoint" type. So as expected we first do this:
var X:TMyClass;
V:TValue; // This will hide a TPoint value, but we'll pretend we don't know
begin
V := AnPointFieldRtti.GetValue(X);
end;
For the next step we'll use the GetReferenceToRawData to get a pointer to the TPoint record hidden in the V:TValue (you know, the one we pretend we know nothing about - except the fact it's a RECORD). Once we get a pointer to that record, we can call the SetValue method to move that "7" inside the record.
LeftFieldRtti.SetValue(V.GetReferenceToRawData, 7);
This is allmost it. Now we just need to move the TValue back into X:TMyClass:
AnPointFieldRtti.SetValue(X, V)
From head-to-tail it would look like this:
var X:TMyClass;
V:TPoint;
begin
V := AnPointFieldRtti.GetValue(X);
LeftFieldRtti.SetValue(V.GetReferenceToRawData, 7);
AnPointFieldRtti.SetValue(X, V);
end;
This can obviously be expanded to handle structures of any depth. Just remember that you need to do it step-by-step: The first GetValue uses the "root" instance, then the next GetValue uses an Instance that's extracted from the previous GetValue result. For records we may use TValue.GetReferenceToRawData, for objects we can use TValue.AsObject!
The next tricky bit is doing this in a generic way, so you can implement your bi-directional tree-like structure. For that, I'd recommend storing the path from "root" to your field in the form of an TRttiMember array (casting will then be used to find the actual runtype type, so we can call GetValue and SetValue). An node would look something like this:
TMemberNode = class
private
FMember : array of TRttiMember; // path from root
RootInstance:Pointer;
public
function GetValue:TValue;
procedure SetValue(Value:TValue);
end;
The implementation of GetValue is very simple:
function TMemberNode.GetValue:TValue;
var i:Integer;
begin
Result := FMember[0].GetValue(RootInstance);
for i:=1 to High(FMember) do
if FMember[i-1].FieldType.IsRecord then
Result := FMember[i].GetValue(Result.GetReferenceToRawData)
else
Result := FMember[i].GetValue(Result.AsObject);
end;
The implementation of SetValue would be a tiny little bit more involved. Because of those (pesky?) records we'll need to do everything the GetValue routine does (because we need the Instance pointer for the very last FMember element), then we'll be able to call SetValue, but we might need to call SetValue for it's parent, and then for it's parent's parent, and so on... This obviously means we need to KEEP all the intermediary TValue's intact, just in case we need them. So here we go:
procedure TMemberNode.SetValue(Value:TValue);
var Values:array of TValue;
i:Integer;
begin
if Length(FMember) = 1 then
FMember[0].SetValue(RootInstance, Value) // this is the trivial case
else
begin
// We've got an strucutred case! Let the fun begin.
SetLength(Values, Length(FMember)-1); // We don't need space for the last FMember
// Initialization. The first is being read from the RootInstance
Values[0] := FMember[0].GetValue(RootInstance);
// Starting from the second path element, but stoping short of the last
// path element, we read the next value
for i:=1 to Length(FMember)-2 do // we'll stop before the last FMember element
if FMember[i-1].FieldType.IsRecord then
Values[i] := FMember[i].GetValue(Values[i-1].GetReferenceToRawData)
else
Values[i] := FMember[i].GetValue(Values[i-1].AsObject);
// We now know the instance to use for the last element in the path
// so we can start calling SetValue.
if FMember[High(FMember)-1].FieldType.IsRecord then
FMember[High(FMember)].SetValue(Values[High(FMember)-1].GetReferenceToRawData, Value)
else
FMember[High(FMember)].SetValue(Values[High(FMember)-1].AsObject, Value);
// Any records along the way? Since we're dealing with classes or records, if
// something is not a record then it's a instance. If we reach a "instance" then
// we can stop processing.
i := High(FMember)-1;
while (i >= 0) and FMember[i].FieldType.IsRecord do
begin
if i = 0 then
FMember[0].SetValue(RootInstance, Values[0])
else
if FMember[i-1].FieldType.IsRecord then
FMember[i].SetValue(FMember[i-1].GetReferenceToRawData, Values[i])
else
FMember[i].SetValue(FMember[i-1].AsObject, Values[i]);
// Up one level (closer to the root):
Dec(i)
end;
end;
end;
... And this should be it. Now some warnings:
DON'T expect this to compile! I actually wrote every single bit of code in this post in the web browser. For technical reasons I had access to the Rtti.pas source file to look up method and field names, but I don't have access to an compiler.
I'd be VERY careful with this code, especially if PROPERTIES are involved. A property can be implemented without an backing field, the setter procedure might not do what you expect. You might run into circular references!
You seem to be misunderstanding the way an instance pointer works. You don't store a pointer to the field, you store a pointer to the class or the record that it's a field of. Object references are pointers already, so no casting is needed there. For records, you need to obtain a pointer to them with the # symbol.
Once you have your pointer, and a TRttiField object that refers to that field, you can call SetValue or GetValue on the TRttiField, and pass in your instance pointer, and it takes care of all the offset calculations for you.
In the specific case of arrays, GetValue it will give you a TValue that represents an array. You can test this by calling TValue.IsArray if you want. When you have a TValue that represents an array, you can get the length of the array with TValue.GetArrayLength and retrieve the individual elements with TValue.GetArrayElement.
EDIT: Here's how to deal with record members in a class.
Records are types too, and they have RTTI of their own. You can modify them without doing "GetValue, modify, SetValue" like this:
procedure ModifyPoint(example: TExampleClass; newXValue, newYValue: integer);
var
context: TRttiContext;
value: TValue;
field: TRttiField;
instance: pointer;
recordType: TRttiRecordType;
begin
field := context.GetType(TExampleClass).GetField('FPoint');
//TValue that references the TPoint
value := field.GetValue(example);
//Extract the instance pointer to the TPoint within your object
instance := value.GetReferenceToRawData;
//RTTI for the TPoint type
recordType := context.GetType(value.TypeInfo) as TRttiRecordType;
//Access the individual members of the TPoint
recordType.GetField('X').SetValue(instance, newXValue);
recordType.GetField('Y').SetValue(instance, newYValue);
end;
It looks like the part you didn't know about is TValue.GetReferenceToRawData. That will give you a pointer to the field, without you needing to worry about calculating offsets and casting pointers to integers.

Delphi: how to set the length of a RTTI-accessed dynamic array using DynArraySetLength?

I'd like to set the length of a dynamic array, as suggested in this post. I have two classes TMyClass and the related TChildClass defined as
TChildClass = class
private
FField1: string;
FField2: string;
end;
TMyClass = class
private
FField1: TChildClass;
FField2: Array of TChildClass;
end;
The array augmentation is implemented as
var
RContext: TRttiContext;
RType: TRttiType;
Val: TValue; // Contains the TMyClass instance
RField: TRttiField; // A field in the TMyClass instance
RElementType: TRttiType; // The kind of elements in the dyn array
DynArr: TRttiDynamicArrayType;
Value: TValue; // Holding an instance as referenced by an array element
ArrPointer: Pointer;
ArrValue: TValue;
ArrLength: LongInt;
i: integer;
begin
RContext := TRTTIContext.Create;
try
RType := RContext.GetType(TMyClass.ClassInfo);
Val := RType.GetMethod('Create').Invoke(RType.AsInstance.MetaclassType, []);
RField := RType.GetField('FField2');
if (RField.FieldType is TRttiDynamicArrayType) then begin
DynArr := (RField.FieldType as TRttiDynamicArrayType);
RElementType := DynArr.ElementType;
// Set the new length of the array
ArrValue := RField.GetValue(Val.AsObject);
ArrLength := 3; // Three seems like a nice number
ArrPointer := ArrValue.GetReferenceToRawData;
DynArraySetLength(ArrPointer, ArrValue.TypeInfo, 1, #ArrLength);
{ TODO : Fix 'Index out of bounds' }
WriteLn(ArrValue.IsArray, ' ', ArrValue.GetArrayLength);
if RElementType.IsInstance then begin
for i := 0 to ArrLength - 1 do begin
Value := RElementType.GetMethod('Create').Invoke(RElementType.AsInstance.MetaclassType, []);
ArrValue.SetArrayElement(i, Value);
// This is just a test, so let's clean up immediatly
Value.Free;
end;
end;
end;
ReadLn;
Val.AsObject.Free;
finally
RContext.Free;
end;
end.
Being new to D2010 RTTI, I suspected the error could depend on getting ArrValue from the class instance, but the subsequent WriteLn prints "TRUE", so I've ruled that out. Disappointingly, however, the same WriteLn reports that the size of ArrValue is 0, which is confirmed by the "Index out of bounds"-exception I get when trying to set any of the elements in the array (through ArrValue.SetArrayElement(i, Value);). Do anyone know what I'm doing wrong here? (Or perhaps there is a better way to do this?) TIA!
Dynamic arrays are kind of tricky to work with. They're reference counted, and the following comment inside DynArraySetLength should shed some light on the problem:
// If the heap object isn't shared (ref count = 1), just resize it. Otherwise, we make a copy
Your object is holding one reference to it, and so is the TValue. Also, GetReferenceToRawData gives you a pointer to the array. You need to say PPointer(GetReferenceToRawData)^ to get the actual array to pass to DynArraySetLength.
Once you've got that, you can resize it, but you're left with a copy. Then you have to set it back onto the original array.
TValue.Make(#ArrPointer, dynArr.Handle, ArrValue);
RField.SetValue(val.AsObject, arrValue);
All in all, it's probably a lot simpler to just use a list instead of an array. With D2010 you've got Generics.Collections available, which means you can make a TList<TChildClass> or TObjectList<TChildClass> and have all the benefits of a list class without losing type safety.
I think you should define the array as a separate type:
TMyArray = array of TMyClass;
and use that.
From an old RTTI based XML serializer I know the general method that you use should work (D7..2009 tested):
procedure TXMLImpl.ReadArray(const Name: string; TypeInfo: TArrayInformation; Data: Pointer; IO: TParameterInputOutput);
var
P: PChar;
L, D: Integer;
BT: TTypeInformation;
begin
FArrayType := '';
FArraySize := -1;
ComplexTypePrefix(Name, '');
try
// Get the element type info.
BT := TypeInfo.BaseType;
if not Assigned(BT) then RaiseSerializationReadError; // Not a supported datatype!
// Typecheck the array specifier.
if (FArrayType <> '') and (FArrayType <> GetTypeName(BT)) then RaiseSerializationReadError;
// Do we have a fixed size array or a dynamically sized array?
L := FArraySize;
if L >= 0 then begin
// Set the array
DynArraySetLength(PPointer(Data)^,TypeInfo.TypeInformation,1,#L);
// And restore he elements
D := TypeInfo.ElementSize;
P := PPointer(Data)^;
while L > 0 do begin
ReadElement(''{ArrayItemName},BT,P,IO); // we allow any array item name.
Inc(P,D);
Dec(L);
end;
end else begin
RaiseNotSupported;
end;
finally
ComplexTypePostfix;
end;
end;
Hope this helps..

Resources