Delphi 2010: New RTTI, setting propertyvalue to arbitary value - delphi

TRTTIProperty.SetValue( ) takes an TValue instance, but if the provided TValue instance is based on a different type then the property, things blow up.
E.g.
TMyObject = class
published
property StringValue: string read FStringValue write FStringValue;
end;
procedure SetProperty(obj: TMyObject);
var
context: TRTTIContext;
rtti: TRTTIType;
prop: TRTTIProperty;
value: TValue;
begin
context := TRTTIContext.Create;
rtti := context.GetType(TMyObject);
prop := rtti.GetProperty('StringValue');
value := 1000;
prop.SetValue(obj, value);
end;
Trying to cast the value to a string wont work either.
prop.SetValue(obj, value.AsString);
prop.SetValue(obj, value.Cast(prop.PropertyType.Handle));
Any ideas on how solve this?
UPDATE:
Some of you wonder why I want to assign an integer to an string, and I will try to explain.
(Actually, it's more likely that I want to assign a string to an integer, but that's not that relevant...)
What I'm trying to accomplish, is to make a general 'middle-man' between gui and model. I want to somehow hook a textedit field to an property. Instead of making such an middle man for each model that I have, I hoped that the new RTTI/TValue thing would work some magic for me.
I'm also new to generics, so I'm not sure how generics could have helped. Is it possible to instantiate a generic at runtime with a dynamically decided type, or do the compile need to know?
E.g.
TMyGeneric<T> = class
end;
procedure DoSomething( );
begin
prop := rtti.getProperty('StringValue');
mygen := TMyGeneric<prop.PropertyType>.Create;
//or
mygen := TMyGeneric<someModel.Class>.Create;
end;
Maybe the age of magic has yet to come... I guess I can manage with a couple of big case structures...

TValue is not a Variant. You can only read the datatype that "you" put into it.
TValue.Cast doesn't work because it has the same semantic that implicit type casts have. You cannot assign an integer to a string or vice versa. But you can assign an integer to a float, or you can assign an integer to a int64.

Can't try it right now, but I would have written:
value := '1000';
prop.SetValue(obj, value);

try
prop.SetValue(obj, value.ToString)
But for me it is same question as for François. Why you want to set the property with a value of the wrong datatype?

Related

How to assign an OleVariant with RTTI? // Convert an OleVariant or a Variant to a TValue with specific TTypeKind or TRTTIType in mind?

I have an OleVariant or a Variant value that, for example, was read with IXMLNode.GetAttributeNS, making it a "String" (varOleStr or varString), and I would like to write that value with, for example, TRTTIField.SetValue, requiring a TValue assignment-compatible to TRTTIField.FieldType: TRTTIType.
For the base types (along TVarType and TRTTIType.TypeKind: TTypeKind), instead of making each a single case: case VarType(Value) and varTypeMask of varXXXX: ... end, I am looking for a general way to convert from OleVariant or Variant to a TValue that then is assignment-compatible to a specific TRTTIType.
What is the way to transition values between the Variant and the RTTI world?
Also, the Spring4D library is part of the project, in case that helps.
Update:
Technically I am looking for Convert in the following code (converting in the Variant world):
var
Left: TRTTIField;
Right: OleVariant;
Temp: TValue;
Instance: Pointer;
begin
{ Examples: varOleStr --> varXXXX --> assignment-compatible TValue }
Right := 'False'; // varOleStr, as read with IXMLNode.GetAttributeNS
Right := Convert(Right, Left.FieldType); // making it possibly varBoolean
Temp := TValue.FromVariant(Right); // tkEnumeration, like Left.FieldType.TypeKind
Right := '2'; // varOleStr, as read with IXMLNode.GetAttributeNS
Right := Convert(Right, Left.FieldType); // making it possibly varInteger
Temp := TValue.FromVariant(Right); // tkInteger, like Left.FieldType.TypeKind
Right := '3.1415'; // varOleStr, as read with IXMLNode.GetAttributeNS
Right := Convert(Right, Left.FieldType); // making it possibly varDoiuble
Temp := TValue.FromVariant(Right); // tkFloat, like Left.FieldType.TypeKind
Right := 'Hello!'; // varOleStr, as read with IXMLNode.GetAttributeNS
Right := Convert(Right, Left.FieldType); // making it possibly varOleStr
Temp := TValue.FromVariant(Right); // tkUString, like Left.FieldType.TypeKind
{ ... and an assignment: }
Left.SetValue(Instance, Temp);
end;
I have found VariantChangeTypeEx, however, I do not know how to relate Left.FieldType to it to make the subsequent code work. -- I also would not mind to convert in the RTTI world and instead start out with Temp := TValue.FromVariant(Right) (tkUString) and then reach assignment compatibility somehow; so Temp.Kind would become tkEnumeration/Boolean, tkFloat,... as given by Left.FieldType.TypeKind.
How to assign a Variant with RTTI? Or, how to convert a Variant to a TValue to then assign it?
Note: RTTIField.SetValue will fail with an EInvalidCast if field type and value type differ in nature, as the RTTI will not attempt to change the value's nature. My difficulty here is to reach assignment compatibility.
Update: Given the answer, the following code sketches my solution:
procedure (const Value: Pointer; const RTTIField: TRTTIField; const XMLNode: IXMLNode);
var
Temp1: OLEVariant;
Temp2: TValue;
begin
Assert(XMLNode.HasAttribute(Ref, Namespace));
Temp1 := XMLNode.GetAttributeNS(Ref, Namespace);
Temp2 := TValue.FromVariant(Temp1);
Temp2 := Temp2.Convert(RTTIField.FieldType.Handle{, FormatSettings}); // in Spring.TValueHelper
RTTIField.SetValue(Value, Temp2);
end;
The built-in type casts in TValue will not help you here as they only allow those types that are explicitly compatible (i.e. assignable). Technically if you store the Variant inside the TValue without "unpacking" it which is what FromVariant does internally it should be able to cast the Variant to anything it usually can be cast/converted to. However there are is at least one issue with casting a Variant holding 'True' or 'False' to a Boolean (see https://quality.embarcadero.com/browse/RSP-20160)
However since you are already using Spring4D you can use its improved TValue type conversion feature.
Just use the Convert method from the TValueHelper in Spring.pas.
There you can pass a PTypeInfo (which would be Left.FieldType.Handle in your code) and optionally a TFormatSettings - by default it will use the current locale.
What is the way to transition values between the Variant and the RTTI world?
Use the built in class function conversion in System.RTTI.TValue:
myTValue := TValue.FromVariant(myVariant);
Builds a new TValue record from a Variant value.
FromVariant is a static method that can be used to build TValue records with a stored Variant value. The Value parameter contains the Variant that will be stored inside the built TValue record.

Delphi OpenTools API get component property

I'm implementing a package to convert and auto-generate components in the delphi IDE. I'm aware that GExperts has a similar function but I need to customize some specific properties.
Right now I'm stuck on accessing the TADOQuery.SQL property, which is an instance of TStrings:
var
aVal : TValue;
aSqlS : TStrings;
begin
[...]
if (mycomp.GetComponentType = 'TADOQuery') then
if mycomp.GetPropValueByName('SQL', aVal) then
begin
aSqlS := TStrings(aVal.AsClass);
if Assigned(aSqlS) then <----- problem is here
ShowMessage(aSqlS.Text); <----- problem is here
end;
end;
I'm not really sure whether using TValue from RTTI is the correct way to go.
Thanks
Assuming GetPropValueByName() is returning a valid TValue (you did not show that code), then using aVal.AsClass is wrong since the SQL property getter does not return a metaclass type. It returns an object pointer, so use aVal.AsObject instead, or even aVal.AsType<TStrings>.
Update If comp is actually IOTAComponent than TValue is definitely wrong to use at all. The output of IOTAComponent.GetPropValueByName() is an untyped var that receives the raw data of the property value, or an IOTAComponent for TPersistent-derived objects:
var
aVal: IOTAComponent;
aSqlS : TStrings;
begin
[...]
if (mycomp.GetComponentType = 'TADOQuery') then
if mycomp.PropValueByName('SQL', aVal) then
ShowMessage(TStrings(aVal.GetComponentHandle).Text);
end;
However, a better option would be to access the actual TADOQuery object instead:
if (mycomp.GetComponentType = 'TADOQuery') then
ShowMessage(TADOQuery(comp.GetComponentHandle).SQL.Text);

Creating property to store two values

Using Delphi 10 I have two values work_start and work_finish of type TTime that I need to read and write from database table so I though to create a property for each one like that
private
fWorkStart: TTime;
function GetWS: TTime;
procedure SetWS(const Value: TTime);
Public
property WorkStart: TTime read GetWS write SetWS;
....
procedure MyClass.SetWS(const Value: TTime);
begin
fWorkStart := value;
mydataset.Edit;
mydataset.FieldByName('work_start').AsDateTime := fWorkStart;
mydataset.Post;
end;
function MyClass.GetWS: TTime;
begin
if mydataset.FieldByName('work_start').IsNull then
fWorkStart := encodetime(6,0,0,0)
else
fWorkStart := mydataset.FieldByName('work_start').AsDateTime;
result := fWorkStart;
end;
WorkFinish property is the same. So is there a way to create one property for both times or my code is fine ?
Craig's answer demonstrates record properties, which means you have a single property that gets set as a unit; you can't set the start and finish times independently. Dawood's answer demonstrates an array property, which allows independent accesses, but imposes cumbersome bracket notation on the consumer. Kobik's comment improves the semantics, but we can do even better using index specifiers.
First, define an enum to represent the two kinds of times:
type
TWorkTime = (wtStart, wtFinish);
Use those values in your property declarations, and provide an extra parameter to your property accessors to represent the index:
private
FWorkTime: :array[TWorkTime] of TTime;
function GetWT(Index: TWorkTime): TTime;
procedure SetWT(Index: TWorkTime; const Value: TTime);
public
property WorkStart: TTime index wsStart read GetWT write SetWT;
property WorkFinish: TTime index wsFinish read GetWT write SetWT;
To reduce the bloat Craig warns about in your accessors, you can define another array with the corresponding fields names, which lets you avoid duplicating code for your different fields:
const
FieldNames: array[TWorkTime] of string = (
'work_start',
'work_finish'
);
function MyClass.GetWT(Index: TWorkTime): TTime;
begin
if mydataset.FieldByName(FieldName[Index]).IsNull then
FWorkTime[Index] := EncodeTime(6, 0, 0, 0)
else
FWorkTime[Index] := mydataset.FieldByName(FieldNames[Index]).AsDateTime;
Result := FWorkTime[Index];
end;
It is possible:
//Define a record to hold both
type
TTimeRange = record
StartTime: TTime;
EndTime: TTime;
end;
//And have your property use the record
property WorkHours: TTimeRange read GetWorkHours write SetWorkHours;
However, this would force clients of your class to interact using the record structure. Basically the complications you'd encounter outweigh the small benefit you'd gain.
So I don't recommend it.
(Although it's worth remembering the technique because in other scenarios it may prove more useful.)
As for your code:
Handling of properties is fine. Although in the code you've presented fWorkStart is redundant.
I'd caution against Edit and Post within your property writer. Apart from the fact that updating 1 field at a time in the Db would be highly inefficient, your method has unexpected side-effects. (And can you always assume edit is the right choice and not insert?)
In your property reader, assuming NULL == 6:00 is not a good idea. NULL has very specific meaning that the value is unknown/unassigned. Defaulting it in the wrong place leads to being unable to tell the difference between 6:00 and NULL. (I'm not saying never default a null; just understand the implications.)
yes you can use indexed properties
property WorkTime[IsStart: Boolean]: TDataTime read GetWorkTime write SetWorkTime;
procedure MyClass.SetWorkTime(IsStart: Boolean;const value: TDataTime);
begin
mydataset.Edit;
if IsStart then
mydataset.FieldByName('work_start').AsDateTime := value else
mydataset.FieldByName('work_Finish').AsDateTime := value;
mydataset.Post;
end;
function MyClass.GetWorkTime(IsStart: Boolean): TTime;
begin
if IsStart then
Begin
if mydataset.FieldByName('work_start').IsNull then
fWorkStart := encodetime(6,0,0,0)
else
fWorkStart := mydataset.FieldByName('work_start').AsDateTime;
result := fWorkStart;
end else
begin
if mydataset.FieldByName('work_finish').IsNull then
fWorkfinish := encodetime(6,0,0,0)
else
fWorkfinish := mydataset.FieldByName('work_finish').AsDateTime;
result := fWorkfinish;
end
end;

Writing a generic TList of records

I am trying to write a generic TList that contains records of a specific type. Starting from David's answer on this question, I have written this class:
Type
TMERecordList<T> = Class(TList<T>)
Public Type
P = ^T;
Private
Function GetItem(Index: Integer): P;
Public
Procedure Assign(Source: TMERecordList<T>); Virtual;
Function First: P; Inline;
Function Last: P; Inline;
Property Items[Index: Integer]: P Read GetItem;
End;
Procedure TMERecordList<T>.Assign(Source: TMERecordList<T>);
Var
SrcItem: T;
Begin
Clear;
For SrcItem In Source Do
Add(SrcItem);
End;
Function TMERecordList<T>.First: P;
Begin
Result := Items[0];
End;
Function TMERecordList<T>.GetItem(Index: Integer): P;
Begin
If (Index < 0) Or (Index >= Count) Then
Raise EArgumentOutOfRangeException.CreateRes(#SArgumentOutOfRange);
Result := #List[Index];
End;
Function TMERecordList<T>.Last: P;
Begin
Result := Items[Count - 1];
End;
Having methods that return a pointer to the record works well (not perfectly) as pointers to records can be used as if they were records in most use cases. Using a record with properties and setters, these test cases work as expected:
TMETestRecord = Record
Private
FID: Word;
FText: String;
FValues: TIntegers;
Procedure SetID(Const Value: Word);
Procedure SetText(Const Value: String);
Procedure SetValues(Const Value: TIntegers);
Public
Property ID: Word Read FID Write SetID;
Property Text: String Read FText Write SetText;
Property Values: TIntegers Read FValues Write SetValues;
End;
// TestSetItem1
rl2[0] := rl1[0];
// TestSetItem2
r.ID := 9;
r.Text := 'XXXX';
r.Values := [9, 99, 999, 9999];
rl1[0] := r;
// TestAssignEmpty (rl0 is empty... after assign so should rl2)
rl2.Assign(rl0);
// TestAssignDeepCopies (modifications after assign should not affect both records)
rl2.Assign(rl1);
r.ID := 9;
r.Text := 'XXXX';
r.Values := [9, 99, 999, 9999];
rl1[0] := r;
Problem 1 - modifying a contained record
... this test case compiles and runs but does not work as desired:
// TestSetItemFields
rl1[0].ID := 9;
rl1[0].Text := 'XXXX';
rl1[0].Values := [9, 99, 999, 9999];
Modifications are applied to temporary copy of the record and not to the one stored in the list. I know this is a known and expected behaviour, as documented in other questions.
But... is there a way around it? I was thinking that maybe if the TMERecordList<>.Items property had a setter the compiler could maybe do what is actually desired. Could it? I know David has got a solution, as hinted at in this question... but I can't seem to find it on my own.
This would really be nice to have, as it would allow me to have a way of using the list identical (or almost) to that of a TList of objects. Having the same interface means I could easily change from objects to records and viceversa, when the need arises.
Problem 2 - interface ambiguity
Having the TList<> return a record pointer does pose some interface ambiguity problems. Some TList<> methods accept T parameters, and we know that being records, these are going to be passed by value. So what should these methods do? Should I rethink them? I'm talking specifically about these sets of methods:
Remove and RemoveItem
Extract and ExtractItem
Contains IndexOf, IndexOfItem and LastIndexOf
There is some ambiguity as to how these should test contained items to see if they match the parameter record value. The list could very well contain identical records and this could become a source of bugs in user code.
I tried not deriving it from TList<>, so as not to have these methods, but it was a mess. I couldn't write a class similar to TList without also writing my own TListHelper. Unfortunately System.Generics.Collections's one has some needed fields that are private, like FCount, and cannot be used outside the unit.
Problem 1
Your Items property is not marked as being default. Hence your erroneous code is picking up the base class default property. Add the default keyword to your Items property:
property Items[Index: Integer]: P read GetItem; default;
Problem 2
This is really a consequence of deriving from TList<T>. I would not recommend doing that. Encapsulate an instance of TList<T> and therefore define the interface explicitly rather than inheriting it. Or implement the list functionality directly in your code. After all, it's not much more than a wrapper around a dynamic array.
For what it is worth my classes don't use TList<T> at all which is a decision that I was very happy with when Emba broke the class in a recent release.
In the recent versions TList<T> in System.Generics.Collections contains a List property that gives you direct access to the backing array of the list. You can use that to manipulate the records inside the list.

How can I test if an unknown Delphi RTTI TValue reflects an object that is ANY type of generic TList<> (or at least TEnumerable<>)?

In Delphi, if I have a TValue instance reflecting an unknown object, how can I test if this object is an instance of ANY kind of generic TEnumerable<> (or even better, also which specific generic enumerable type it is an instance of, e.g. TList<>)?
NOTE: I already know how to easily check its exact type, i.e. with the .BaseType property of the corresponding TRttiType of the TValue, resulting in for example TList<string>, but what I want to test is rather if it is a TList<> of any sub-item type.
To exemplify how this hypothetical code "IsAnyKindOfGenericEnumerable()" would work, here is some example code:
var
LContext : TRttiContext;
obj_1_rtti_value : TValue;
obj_2_rtti_value : TValue;
obj_3_rtti_value : TValue;
obj_1_rtti_type : TRttiType;
obj_2_rtti_type : TRttiType;
obj_3_rtti_type : TRttiType;
LContext := TRttiContext.Create();
{
...
obj_1_rtti_value is set to a TValue reflection of a TList<string> object here
obj_2_rtti_value is set to a TValue reflection of a plain TObject object here
obj_3_rtti_value is set to a TValue reflection of a TQueue<integer> object here
...
}
obj_1_rtti_type := LContext.GetType(obj_1_rtti_value.TypeInfo);
obj_2_rtti_type := LContext.GetType(obj_2_rtti_value.TypeInfo);
obj_3_rtti_type := LContext.GetType(obj_3_rtti_value.TypeInfo);
IsAnyKindOfGenericEnumerable(obj_1_rtti_type); //Would return true
IsAnyKindOfGenericEnumerable(obj_2_rtti_type); //Would return false
IsAnyKindOfGenericEnumerable(obj_3_rtti_type); //Would return true
And again, the very best thing would be if I could also detect which kind of TEnumerable<> type it is, like for example:
IsAnyKindOfGenericEnumerable(obj_1_rtti_type); //Will return true + `TList<>`
IsAnyKindOfGenericEnumerable(obj_2_rtti_type); //Will return false
IsAnyKindOfGenericEnumerable(obj_3_rtti_type); //Will return true + `TQueue<>`
I have tried:
if obj_1_rtti_type is TRttiEnumerationType then
begin
//...
end;
but for some reason this evaluates to false, which I'm completely at loss as to why that is? The expression value_type.BaseType.Name does indeed evaluate to 'TEnumerable<System.string>' in this case, but there really has to be some other way than to manually parse this string in order to accomplish my objective, right?
Finally, the goal must be accomplished solely using the RTTI info, that is, any "cheating" by referring to the real object behind the TValue is not permitted (for reasons outside the scope of this question).
There is no RTTI generated for Generic types themselves (they don't exist at runtime), and each specific instantiation (like TList<string>) is a distinct class type with its own distinct RTTI. You would have to check for each individual type, it is not possible to test for any Generic type. Parsing class names is the only way to detect Generic types.
use TRttiType.Name to get the class name as a string ('TList<System.string>').
parse it to detect the presence of angle brackets ('<>').
extract the substring between the brackets ('System.string')
walk the ancestor tree looking for an ancestor whose TRttiType.Name is 'TEnumerable<...>', where ... is the extracted substring ('TEnumerable<System.string>').
However, this approach fails for class types that derive from TEnumerable<T> but do not have Generics parameters themselves, eg:
type
TMyClass = class(TEnumerable<string>)
end;
To account for that, ignore steps 1-3 and jump right to step 4 by itself, ignoring whatever value appears between the brackets, eg:
function IsAnyKindOfGenericEnumerable(AType: TRttiType): Boolean;
begin
Result := False;
while AType <> nil do
begin
Result := StartsText('TEnumerable<', AType.Name);
if Result then Exit;
AType := AType.BaseType;
end;
end;
As for TRttiEnumerationType, it represents enumerated types (ie: type typeName = (val1, ...,valn);). It has nothing to do with TEnumerable<T>. That is why the is operator is always returning False for you - none of the RTTI types you are testing represent enums.

Resources