Rebelss
Rebelss

Reputation: 502

Setting a value in a Nullable<T> record with RTTI

I'm working with Serialization/Deserialization using NEON library from Paolo Rossi.

i'm trying to populate this class using RTTI, with data that i get from a database. The properties on the class have the same name of the fields in the database.

In the library, i have this Nullable Record :

unit Neon.Core.Nullables;

interface

uses
  System.SysUtils, System.Variants, System.Classes, System.Generics.Defaults, System.Rtti,
  System.TypInfo, System.JSON;

type
  ENullableException = class(Exception);

  {$RTTI EXPLICIT FIELDS([vcPrivate]) METHODS([vcPrivate])}
  Nullable<T> = record
  private
    FValue: T;
    FHasValue: string;
    procedure Clear;
    function GetValueType: PTypeInfo;
    function GetValue: T;
    procedure SetValue(const AValue: T);
    function GetHasValue: Boolean;
  public
    constructor Create(const Value: T); overload;
    constructor Create(const Value: Variant); overload;
    function Equals(const Value: Nullable<T>): Boolean;
    function GetValueOrDefault: T; overload;
    function GetValueOrDefault(const Default: T): T; overload;

    property HasValue: Boolean read GetHasValue;
    function IsNull: Boolean;

    property Value: T read GetValue;

    class operator Implicit(const Value: Nullable<T>): T;
    class operator Implicit(const Value: Nullable<T>): Variant;
    class operator Implicit(const Value: Pointer): Nullable<T>;
    class operator Implicit(const Value: T): Nullable<T>;
    class operator Implicit(const Value: Variant): Nullable<T>;
    class operator Equal(const Left, Right: Nullable<T>): Boolean;
    class operator NotEqual(const Left, Right: Nullable<T>): Boolean;
  end;

  NullString = Nullable<string>;
  NullBoolean = Nullable<Boolean>;
  NullInteger = Nullable<Integer>;
  NullInt64 = Nullable<Int64>;
  NullDouble = Nullable<Double>;
  NullDateTime = Nullable<TDateTime>;

implementation

uses
  Neon.Core.Utils;

{ Nullable<T> }

constructor Nullable<T>.Create(const Value: T);
var
  a: TValue;
begin
  FValue := Value;
  FHasValue := DefaultTrueBoolStr;
end;

constructor Nullable<T>.Create(const Value: Variant);
begin
  if not VarIsNull(Value) and not VarIsEmpty(Value) then
    Create(TValue.FromVariant(Value).AsType<T>)
  else
    Clear;
end;

procedure Nullable<T>.Clear;
begin
  FValue := Default(T);
  FHasValue := '';
end;

function Nullable<T>.Equals(const Value: Nullable<T>): Boolean;
begin
  if HasValue and Value.HasValue then
    Result := TEqualityComparer<T>.Default.Equals(Self.Value, Value.Value)
  else
    Result := HasValue = Value.HasValue;
end;

function Nullable<T>.GetHasValue: Boolean;
begin
  Result := FHasValue <> '';
end;

function Nullable<T>.GetValueType: PTypeInfo;
begin
  Result := TypeInfo(T);
end;

function Nullable<T>.GetValue: T;
begin
  if not HasValue then
    raise ENullableException.Create('Nullable type has no value');
  Result := FValue;
end;

function Nullable<T>.GetValueOrDefault(const Default: T): T;
begin
  if HasValue then
    Result := FValue
  else
    Result := Default;
end;

function Nullable<T>.GetValueOrDefault: T;
begin
  Result := GetValueOrDefault(Default(T));
end;

class operator Nullable<T>.Implicit(const Value: Nullable<T>): T;
begin
  Result := Value.Value;
end;

class operator Nullable<T>.Implicit(const Value: Nullable<T>): Variant;
begin
  if Value.HasValue then
    Result := TValue.From<T>(Value.Value).AsVariant
  else
    Result := Null;
end;

class operator Nullable<T>.Implicit(const Value: Pointer): Nullable<T>;
begin
  if Value = nil then
    Result.Clear
  else
    Result := Nullable<T>.Create(T(Value^));
end;

class operator Nullable<T>.Implicit(const Value: T): Nullable<T>;
begin
  Result := Nullable<T>.Create(Value);
end;

class operator Nullable<T>.Implicit(const Value: Variant): Nullable<T>;
begin
  Result := Nullable<T>.Create(Value);
end;

function Nullable<T>.IsNull: Boolean;
begin
  Result := FHasValue = '';
end;

class operator Nullable<T>.Equal(const Left, Right: Nullable<T>): Boolean;
begin
  Result := Left.Equals(Right);
end;

class operator Nullable<T>.NotEqual(const Left, Right: Nullable<T>): Boolean;
begin
  Result := not Left.Equals(Right);
end;

procedure Nullable<T>.SetValue(const AValue: T);
begin
  FValue := AValue;
  FHasValue := DefaultTrueBoolStr;
end;

end.

Here's the model class :

type
  TMyClass = class(TPersistent)
  private
    FMyIntegerProp: Nullable<Integer>;
    procedure SetMyIntegerProp(const Value: Nullable<Integer>);
  published
    Property MyIntegerProp: Nullable<Integer> read FMyIntegerProp write SetMyIntegerProp;
  end;

implementation

{ TMyClass }

procedure TMyClass.SetMyIntegerProp(const Value: Nullable<Integer>);
begin
  FMyIntegerProp := Value;
end;

And my code so far :

procedure DatasetToObject(AObject: TObject; AQuery: TFDQuery);
var
  n: Integer;
  LRttiContext: TRttiContext;
  LRttiType: TRttiType;
  LRttiProperty: TRttiProperty;
  LFieldName: string;
  Value: TValue;

  LValue: TValue;
  LRttiMethod : TRttiMethod;
begin
  LRttiContext := TRttiContext.Create;
  try
    LRttiType := LRttiContext.GetType(AObject.ClassType);

    for n := 0 to AQuery.FieldCount - 1 do
    begin
      LRttiProperty := LRttiType.GetProperty(AQuery.Fields[n].FieldName);

      if (LRttiProperty <> nil) and (LRttiProperty.PropertyType.TypeKind = tkRecord) then
      begin
        LValue := LRttiProperty.GetValue(AObject);
        LRttiMethod := LRttiContext.GetType(LValue.TypeInfo).GetMethod('SetValue');

        if (LRttiProperty.PropertyType.Name = 'Nullable<System.Integer>') then
          LRttiMethod.Invoke(LValue, [AQuery.Fields[n].AsInteger]).AsInteger;
      end;
    end;
  finally
    LRttiContext.Free;
  end;
end;

but no success so far, any help will be appreciated.

Upvotes: 1

Views: 934

Answers (1)

Remy Lebeau
Remy Lebeau

Reputation: 595961

Nullable.SetValue() does not have a return value, but you are trying to read one when you call AsInteger on the TValue that TRttiMethod.Invoke() returns. That will cause an exception to be raised at runtime.

Also, when you read the value of the TMyClass.MyIntegerProp property, you will end up with a copy of its Nullable record, so Invoke()'ing SetValue() on that copy is not going to update the MyIntegerProp property. You will have to assign the modified Nullable back to MyIntegerProp afterwards, eg:

LValue := LRttiProperty.GetValue(AObject);
LRttiMethod := LRttiContext.GetType(LValue.TypeInfo).GetMethod('SetValue');
LRttiMethod.Invoke(LValue, [AQuery.Fields[n].AsInteger]);
LRttiProperty.SetValue(AObject, LValue); // <-- add this!

That being said, the Nullable.Value property is read-only, but Nullable has a SetValue() method, so I would suggest changing the Value property to be read-write instead, eg:

property Value: T read GetValue write SetValue;

Then you can set the Value property via RTTI instead of Invoke()'ing the SetValue() method directly:

var
  ...
  //LRttiMethod: TRttiMethod;
  LRttiValueProp: TRttiProperty;
  ...

...

LRttiProperty := LRttiType.GetProperty(AQuery.Fields[n].FieldName);

if (LRttiProperty <> nil) and
   (LRttiProperty.PropertyType.TypeKind = tkRecord) and
   (LRttiProperty.PropertyType.Name = 'Nullable<System.Integer>') then
begin
  LValue := LRttiProperty.GetValue(AObject);
  {
  LRttiMethod := LRttiContext.GetType(LValue.TypeInfo).GetMethod('SetValue');
  LRttiMethod.Invoke(LValue, [AQuery.Fields[n].AsInteger]);
  }
  LRttiValueProp := LRttiContext.GetType(LValue.TypeInfo).GetProperty('Value');
  LRttiValueProp.SetValue(LValue.GetReferenceToRawData, AQuery.Fields[n].AsInteger);
  LRttiProperty.SetValue(AObject, LValue);
end;

Upvotes: 3

Related Questions