Jump to content
dmitrybv

How to work with published properties of the Variant type correctly.

Recommended Posts

Good day.

 

I can't get Object Inspector to work correctly with a property of the Variant type.

I have a published property CheckedValue of the Variant type in my object with the following implementation.

When I try to set a new value for this property in Design-Time, I can't do it. In the Type drop-down list, the system doesn't let me select a new type.

When I enter a new value in the CheckedValue field, the error 'Could not convert variant' keeps popping up.

 

Embarcadero® RAD Studio 12 Version 29.0.53982.0329 


 

{ TTestComponent }

  TTestComponent = class(TComponent)
  private
    FCheckedValue: Variant;
    function GetCheckedValue: Variant;
    function IsCheckedValueStored: Boolean;
    procedure SetCheckedValue(const Value: Variant);
    procedure CheckedValueChanged();
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;


  published
    property CheckedValue: Variant read GetCheckedValue write SetCheckedValue stored IsCheckedValueStored;
  end;




{ TTestComponent }

constructor TTestComponent.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FCheckedValue := True;
end;

destructor TTestComponent.Destroy;
begin
  inherited Destroy;
end;

function TTestComponent.GetCheckedValue: Variant;
begin
  Result := FCheckedValue;
end;

procedure TTestComponent.SetCheckedValue(const Value: Variant);
begin
  if VarSameValue(FCheckedValue, Value) = False then
  begin
    FCheckedValue := Value;
    CheckedValueChanged();
  end;
end;

function TTestComponent.IsCheckedValueStored: Boolean;
begin
  if VarSameValue(FCheckedValue, True) then
    Result := False
  else
    Result := True;
end;

procedure TTestComponent.CheckedValueChanged;
begin
end;

 

 

Share this post


Link to post
  1. Apparently the standard design-time property editors doesn't fully support Variant.
    https://stackoverflow.com/questions/29979950/how-to-correctly-set-a-variant-published-property

    I've looked at the code of the property editor but I can't make sense of it so I can't tell if it should work or not. I would have to debug Delphi in Delphi to figure it out and... Nope.
     
  2. Don't compare against a boolean value. Use boolean logic:
    if not VarSameValue(FCheckedValue, Value)then
  3. Same with the test in IsCheckedValueStored. Just set result directly:
    Result := not VarSameValue(FCheckedValue, True);

     

Share this post


Link to post

When using VarSameValue(), you can get a conversion failure if the 2 Variants are not holding compatible types.  This is even stated in the documentation:

Quote

If A and B can't be compared, VarSameValue raises an exception.

So, you should first make sure the 2 Variants have the same type before comparing their values.  This is just logical to do anyway, since if you know they have different types then there is no point in wasting resources to compare the values.

 

When I make these small changes to TTestComponent, the "Could not convert variant" error goes away:

procedure TTestComponent.SetCheckedValue(const Value: Variant);
begin
  if (VarType(FCheckedValue) <> VarType(Value)) or // <-- ADD THIS!
     (not VarSameValue(FCheckedValue, Value)) then
  begin
    FCheckedValue := Value;
    CheckedValueChanged();
  end;
end;

function TTestComponent.IsCheckedValueStored: Boolean;
begin
  Result := (VarType(FCheckedValue) <> varBoolean) or // <-- ADD THIS!
            (not VarSameValue(FCheckedValue, True));
end;

Now, that just leaves the problem of the 'CheckedValue.Type' property displaying "Unknown" for string values.  That is indeed a bug in the default Variant property editor, which I have now reported to Embarcadero:

 

RSS-2844; TVariantTypeProperty is broken for string values

 

You can easily work around the bug, either directly in your component's property setter:

procedure TTestComponent.SetCheckedValue(const Value: Variant);
begin
  if (VarType(FCheckedValue) <> VarType(Value)) or
     (not VarSameValue(FCheckedValue, Value)) then
  begin
    FCheckedValue := Value;
    if VarType(FCheckedValue) = varString then
      FCheckedValue := VarToStr(Value); // <-- change the VarType to varUString
    CheckedValueChanged();
  end;
end;

Or by deriving a custom property editor in your component's design-time package (if you don't have one, make one) to fix the bug directly:

uses
  Variants, DesignIntf, DesignEditors, DesignConst;

//...

{ TMyVariantTypeProperty }

// unfortunately, TVariantTypeProperty is hidden in the implementation
// of the DesignEditors unit, so we have to copy the entire class just
// to change a couple of lines!

const
  VarTypeNames: array[varEmpty..varInt64] of string = (
    'Unassigned', // varEmpty
    'Null',       // varNull
    'Smallint',   // varSmallint
    'Integer',    // varInteger
    'Single',     // varSingle
    'Double',     // varDouble
    'Currency',   // varCurrency
    'Date',       // varDate
    'OleStr',     // varOleStr
    '',           // varDispatch
    '',           // varError
    'Boolean',    // varBoolean
    '',           // varVariant
    '',           // varUnknown
    '',           // [varDecimal]
    '',           // [undefined]
    'Shortint',   // varShortInt
    'Byte',       // varByte
    'Word',       // varWord
    'LongWord',   // varLongWord
    'Int64');     // varInt64

type
  TMyVariantTypeProperty = class(TNestedProperty)
  public
    function AllEqual: Boolean; override;
    function GetAttributes: TPropertyAttributes; override;
    function GetName: string; override;
    function GetValue: string; override;
    procedure GetValues(Proc: TGetStrProc); override;
    procedure SetValue(const Value: string); override;
   end;

function TMyVariantTypeProperty.AllEqual: Boolean;
var
  i: Integer;
  V1, V2: Variant;
begin
  Result := False;
  if PropCount > 1 then
  begin
    V1 := GetVarValue;
    for i := 1 to PropCount - 1 do
    begin
      V2 := GetVarValueAt(i);
      if VarType(V1) <> VarType(V2) then Exit;
    end;
  end;
  Result := True;
end;

function TMyVariantTypeProperty.GetAttributes: TPropertyAttributes;
begin
  Result := [paMultiSelect, paValueList, paSortList];
end;

function TMyVariantTypeProperty.GetName: string;
begin
  Result := 'Type';
end;

function TMyVariantTypeProperty.GetValue: string;
begin
  case VarType(GetVarValue) and varTypeMask of
    Low(VarTypeNames)..High(VarTypeNames):
      Result := VarTypeNames[VarType(GetVarValue) and varTypeMask];
    varString,varUString: // <-- FIX HERE!
      Result := SString;
  else
    Result := SUnknown;
  end;
end;

procedure TMyVariantTypeProperty.GetValues(Proc: TGetStrProc);
var
  i: Integer;
begin
  for i := 0 to High(VarTypeNames) do
    if VarTypeNames[i] <> '' then
      Proc(VarTypeNames[i]);
  Proc(SString);
end;

procedure TMyVariantTypeProperty.SetValue(const Value: string);

  function GetSelectedType: Integer;
  var
    i: Integer;
  begin
    Result := -1;
    for i := 0 to High(VarTypeNames) do
      if VarTypeNames[i] = Value then
      begin
        Result := i;
        break;
      end;
    if (Result = -1) and (Value = SString) then
      Result := varUString; // <-- FIX HERE!
  end;

var
  NewType: Integer;
  V: Variant;
begin
  NewType := GetSelectedType;
  case NewType of
    varEmpty: VarClear(V);
    varNull: V := NULL;
    -1: raise EDesignPropertyError.CreateRes(@SUnknownType);
  else
    V := GetVarValue; // <-- move here for good measure...
    try
      VarCast(V, V, NewType);
    except
      { If it cannot cast, clear it and then cast again. }
      VarClear(V);
      VarCast(V, V, NewType);
    end;
  end;
  SetVarValue(V);
end;

{ TMyVariantProperty }

// fortunately, TVariantProperty is public in the DesignEditors unit,
// so we need to override only 1 method in it...

type
  TMyVariantProperty = class(TVariantProperty)
    procedure GetProperties(Proc: TGetPropProc); override;
  end;

procedure TMyVariantProperty.GetProperties(Proc: TGetPropProc);
begin
  Proc(TMyVariantTypeProperty.Create(Self));
end;

procedure Register;
begin
  //...
  // change the 2nd and 3rd properties if you want to reuse this editor for all Variant properties, eg:
  // RegisterPropertyEditor(TypeInfo(Variant), nil, '', TMyVariantProperty);
  RegisterPropertyEditor(TypeInfo(Variant), TTestComponent, 'CheckedValue', TMyVariantProperty);
end;

 

Edited by Remy Lebeau
  • Like 4

Share this post


Link to post

Thanks for the detailed answer.

 

I used the option:

 

if VarType(FCheckedValue) = varString then
FCheckedValue := VarToStr(Value); // <-- change the VarType to varUString

Almost everything works except if you specify the Date type and enter the value 1111111111 then an error is displayed that does not allow you to close the window with the error and you have to kill the bds.exe process.

Share this post


Link to post
33 minutes ago, dmitrybv said:

Almost everything works except if you specify the Date type and enter the value 1111111111 then an error is displayed that does not allow you to close the window with the error and you have to kill the bds.exe process.

You landed on the holy grail of bugs !

 

If you can make smallest demo to reproduce this bug then it is great for reporting, these Variant handling exception/bugs/AV are fatal in the IDE and debugger and there quite few of them, not all do show error messages, many leads to silent IDE crash or just freeze, there is bugs is many places but it could shed light on this Variant mishandle in IDE/Debugger in general.

Share this post


Link to post
51 minutes ago, dmitrybv said:

Thanks for the detailed answer.

Is there any way to train an AI model with Remy's brain?

  • Like 1
  • Haha 2

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×