Setting a "nullable" property on a .NET object

4

I have some .NET interop code where I've managed to load objects and read properties, however I am having trouble with setting a property on an object. Here's the relevant parts of the Delphi code:

uses
  mscorlib_TLB, Winapi.ActiveX;

type
  // Irrelevant parts of the code omitted
  TDotNetObject = class(TObject)
  private
    FTarget: OleVariant;
    FType: _Type;
  public
    procedure SetProperty(const APropertyName: string; const AValue: OleVariant; const AIndex: Integer = -1);
  end;

function VariantToPSafeArray(const AValue: Variant): PSafeArray;
begin
  Result := PSafeArray(VarArrayAsPSafeArray(AValue));
end;

procedure TDotNetObject.SetProperty(const APropertyName: string; const AValue: OleVariant; const AIndex: Integer = -1);
var
  LPropertyInfo: _PropertyInfo;
  LIndex: PSafeArray;
begin
  if AIndex >= 0 then
    LIndex := VariantToPSafeArray(VarArrayOf([AIndex]))
  else
    LIndex := nil;
  LPropertyInfo := FType.GetProperty(APropertyName, BindingFlags_Instance or BindingFlags_Public or BindingFlags_NonPublic);
  if LPropertyInfo <> nil then
    LPropertyInfo.SetValue(FTarget, AValue, LIndex);
end;

procedure UpdateDefectStatus(const ADefectID, AStatus: Integer);
var
  LObject: TDotNetObject;
begin
  // ** Code to obtain the object omitted ***
  LObject.SetProperty('Status', AStatus);
end;

The mscorlib_TLB unit comes from JCL in Project JEDI, here:

https://github.com/project-jedi/jcl/blob/master/jcl/source/windows/mscorlib_TLB.pas

An error is thrown when LPropertyInfo.SetValue is called in TDotNetObject.SetProperty:

Project TestProject.exe raised exception class EOleException with message 'Object of type 'System.Int32' cannot be converted to type 'System.Nullable`1[MTData.Transport.Tracking.DefectReporting.DefectStatus]''.

The DefectStatus property on the C# object is declared as:

public DefectStatus? Status

(i.e. it's nullable)

The Status property type in C# is declared:

public enum DefectStatus
{
    /// <summary>
    /// Defect Reported.
    /// </summary>
    Reported,
    /// <summary>
    /// Defect assessed.
    /// </summary>
    Assessed,
    /// <summary>
    /// Defect on work order.
    /// </summary>
    OnWorkOrder,
    /// <summary>
    /// Defect closed.
    /// </summary>
    Closed
}

I found a solution for how to handle this situation using C# here:

https://stackoverflow.com/a/13270302/3164070

However I'm a bit lost as to how to do the same in Delphi. Any ideas?

EDIT

Given Olivier's answer, I have attempted to write some Delphi code to do the equivalent, which is as follows:

procedure InvokeToObject;
var
  LType, LDefectStatusType: _Type;
  LInvokeFlags: TOleEnum;
  LArgs: PSafeArray;
  LValue: Integer;
  LResult: OleVariant;
  LRes: HRESULT;
  LResHex: string;
begin
  LType := MTDataClr.GetCoreType('System.Enum');
  LDefectStatusType := MTDataClr.GetType('MTData.Transport.Tracking.DefectReporting.DefectStatus');
  LInvokeFlags := BindingFlags_InvokeMethod or BindingFlags_Static;
  LValue := 1;
  LArgs := VariantToPSafeArray(VarArrayOf([LDefectStatusType, LValue]));
  LRes := LType.InvokeMember_2('ToObject', LInvokeFlags, nil, Null, LArgs, nil, LResult);
  LResHex := IntToHex(LRes);
end;

The goal with this piece of code is just to invoke the ToObject method of the Enum type. Obtaining LType and LDefectStatusType succeeds, however the call to InvokeMember_2 does not, with a return code of: 0x80131512, which apparently is a Missing Member exception. Any ideas on what I'm doing wrong?

c#
.net
delphi
asked on Stack Overflow Apr 21, 2020 by Dave Nottage • edited Apr 23, 2020 by Dave Nottage

1 Answer

1

The issue is that an enum is not an int, which means SetValue() would need to perform a double conversion (Int32 to DefectStatus and DefectStatus to DefectStatus?), which it cannot (it can only perform one).

Here's a C# code that reproduces what you're trying to do:

using System;
using System.Reflection;

public enum DefectStatus
{
    Reported,
    Assessed,
    OnWorkOrder,
    Closed
}

public class Defect
{
    public DefectStatus? Status {get; set;}
}

public class Test
{
    public static void Main()
    {
        Defect def = new Defect();

        PropertyInfo pi = typeof(Defect).GetProperty("Status");

        // This throws an ArgumentException
//      pi.SetValue(def, 1, null);

        // Retrieve the Assessed enum via its numeric value
        object assessed = Enum.ToObject(typeof(DefectStatus), 1);

        // This works as expected
        pi.SetValue(def, assessed, null);

        Console.WriteLine(def.Status);
    }
}

So you need to retrieve the enum in Delphi. For that you will need to play with the API to access the Enum type and call ToObject on it.

answered on Stack Overflow Apr 21, 2020 by Olivier • edited Apr 21, 2020 by Olivier

User contributions licensed under CC BY-SA 3.0