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?

Dave Nottage
  • 3,411
  • 1
  • 20
  • 57
  • Do you have the possibility to modify the C# code? – Olivier Apr 21 '20 at 09:23
  • Unfortunately, no. – Dave Nottage Apr 21 '20 at 09:24
  • How do you do the interop exactly? Where are the classes `_Type` and `_PropertyInfo` coming from? – Olivier Apr 21 '20 at 09:47
  • @Olivier They're from the mscorlib_TLB unit, here: https://github.com/project-jedi/jcl/blob/master/jcl/source/windows/mscorlib_TLB.pas – Dave Nottage Apr 21 '20 at 10:12
  • Maybe add a `BindingFlags_Public`? – Olivier Apr 23 '20 at 07:58
  • @MartynA Not entirely, especially since my test code does not work. I need a solution in Delphi. – Dave Nottage Apr 23 '20 at 08:35
  • @Olivier Adding `BindingFlags_Public` does not help, sadly – Dave Nottage Apr 23 '20 at 08:39
  • @DaveNottage, oh ok, comment deleted. – MartynA Apr 23 '20 at 08:51
  • I just did a test in C# (reproducing what you're trying to do) and I confirm that `BindingFlags_Public` is mandatory (otherwise you get a `MissingMethodException`). I notice that you mixed `nil` and `Null` in the parameters. Did you try using only `nil` or only `Null`? – Olivier Apr 23 '20 at 09:02
  • What flags (other than `BindingFlags_Public`) are you using? – Dave Nottage Apr 23 '20 at 09:54
  • The same as you: `object o = typeof(Enum).InvokeMember("ToObject", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new object[]{typeof(DefectStatus), 1}, null);` – Olivier Apr 23 '20 at 10:40
  • You could try to retrieve the method first: `MethodInfo mi = typeof(Enum).GetMethod("ToObject", new Type[]{typeof(Type), typeof(Int32)});` (it's called `GetMethod_5()` in Delphi) – Olivier Apr 23 '20 at 10:54
  • D'Oh! I had only 2 flags (`BindingFlags.InvokeMethod | BindingFlags.Public`, i.e. I was missing `BindingFlags.Static`). Now the trick will be to fit it into the SetProperty routine (which is generic).. – Dave Nottage Apr 23 '20 at 11:01
  • Good news if it finally works! I guess you should add a `SetEnumProperty()` method. – Olivier Apr 23 '20 at 11:14
  • I still need to get the correct `_Type` from the `_PropertyInfo` to pass to the `ToObject` method. – Dave Nottage Apr 23 '20 at 11:40
  • If you define `SetEnumProperty()`, you can explicitly pass the enum type (`'MTData.Transport.Tracking.DefectReporting.DefectStatus'` in your case). If you want to find it automatically, it's doable but requires more work because you have to detect the nullable type, extract its underlying type, and check if that type is an enum. – Olivier Apr 23 '20 at 12:38
  • @Olivier Thanks for the additional tip. I've now posted this question: https://stackoverflow.com/questions/61514990/setting-an-enum-property-on-a-net-object – Dave Nottage Apr 30 '20 at 02:38

1 Answers1

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.

Olivier
  • 13,283
  • 1
  • 8
  • 24