0

Aim: Using reflection and generics pass in a VBA array of custom objects and return the same custom VBA COM object back after processing. Eg in the example removing duplicates from a VBA of any type array passed in as a variant containing an array.

For value types worked as expected returning an array of the same type passed in. See RemoveDuplicates

Issue: For custom VBA objects eg a Person array returns an array of variants containing person objects where expecting a Person array.

To remove duplicates for a custom object arrays RemoveDuplicates2 is used passing in the VBA array and an non-generic IEqualityComparer.

Apart from not returning the custom COM object array required is working as expected removing the duplicates within a Person array according to the IEqualityComparer supplied from VBA.

Note: For now named the RemoveDuplicates overloads in Array.cs uniquely to avoid any potiential issues requiring the knowlegde of expression trees to be investigated later.

SafeArraySingleton.cs

using GSystem = global::System;
using GArray = global::System.Array;
using GType = global::System.Type;
using GMethodInfo = global::System.Reflection.MethodInfo;

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Reflection;
using System.Collections;
using DotNetLib.System.Collections.Generic;

namespace DotNetLib.System
{
    [ComVisible(true)]
    [Description("Provides methods for creating, manipulating, searching, and sorting safe arrays.")]
    [Guid("37592E87-83CE-4D47-A510-324FE529BE27")]
    [ProgId("DotNetLib.System.SafeArraySingleton")]
    [ClassInterface(ClassInterfaceType.None)]
    [ComDefaultInterface(typeof(ISafeArraySingleton))]
    public class SafeArraySingleton : ISafeArraySingleton
    {
        const string RemoveDuplicatesMethod = "RemoveDuplicates";
        static GType _arrayType = typeof(Array);
        static  GMethodInfo _removeDuplicates = _arrayType.GetMethod("RemoveDuplicates",
                                                BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);


        static GMethodInfo _removeDuplicates2 = _arrayType.GetMethod("RemoveDuplicates2",
                                                BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
        static GType _genericComparerType = typeof(GenericEqualityComparer<>);

        public SafeArraySingleton() { }

        [return: MarshalAs(UnmanagedType.Struct, SafeArraySubType = VarEnum.VT_ARRAY)]
        public object RemoveDuplicates([MarshalAs(UnmanagedType.Struct, SafeArraySubType = VarEnum.VT_ARRAY)][In] ref object inputArray)
        {
            if ((inputArray == null) | !(inputArray is GArray))
            {
                throw new ArgumentException("Must be an array.", nameof(inputArray));
            }
            var result = _removeDuplicates.MakeGenericMethod(inputArray.GetType().GetElementType()).Invoke(null, new object[] { inputArray });
            return result;
        }

        [return: MarshalAs(UnmanagedType.Struct, SafeArraySubType = VarEnum.VT_ARRAY)]
        public object RemoveDuplicates2([MarshalAs(UnmanagedType.Struct, SafeArraySubType = VarEnum.VT_ARRAY)][In] ref object inputArray, IEqualityComparer nonGenericComparer)
        {
            if ((inputArray == null) | !(inputArray is GSystem.Array))
            {
                throw new ArgumentException("Must be an array.", nameof(inputArray));
            }

            GType elementType = inputArray.GetType().GetElementType();

            // Create a generic wrapper for the non-generic comparer using reflection
            var genericComparerType = typeof(GenericEqualityComparer<>);
            var genericType = genericComparerType.MakeGenericType(elementType);
            var genericComparer = Activator.CreateInstance(genericType, nonGenericComparer);

            var result = _removeDuplicates2.MakeGenericMethod(elementType).Invoke(null, new object[] { inputArray, genericComparer });
            return result;
        }

    }
}

ISafeArraySingleton.cs

using GCollections = global::System.Collections;
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;

namespace DotNetLib.System
{
    [ComVisible(true)]
    [Guid("5B473306-F5E2-4C83-92F7-CEF4A363E2DD")]
    [Description("Provides methods for creating, manipulating, searching, and sorting safe arrays.")]
    [InterfaceType(ComInterfaceType.InterfaceIsDual)]
    public interface ISafeArraySingleton
    {
        [Description("Removes duplicates from an array.")]
        [return: MarshalAs(UnmanagedType.Struct, SafeArraySubType = VarEnum.VT_ARRAY)]
        object RemoveDuplicates([MarshalAs(UnmanagedType.Struct, SafeArraySubType = VarEnum.VT_ARRAY)][In] ref object inputArray);

        [Description("Removes duplicates from an array.")]
        [return: MarshalAs(UnmanagedType.Struct, SafeArraySubType = VarEnum.VT_ARRAY)]
        object RemoveDuplicates2([MarshalAs(UnmanagedType.Struct, SafeArraySubType = VarEnum.VT_ARRAY)][In] ref object inputArray, GCollections.IEqualityComparer equalityComparer);

    }
}

From Array.cs

        public static T[] RemoveDuplicates<T>(T[] inputArray)
        {
            if (inputArray == null)
                throw new ArgumentNullException(nameof(inputArray));

            HashSet<T> set = new HashSet<T>(inputArray);
            return set.ToArray();
        }

        public static T[] RemoveDuplicates2<T>(T[] inputArray, IEqualityComparer<T> equalityComparer)
        {
            if (inputArray == null)
                throw new ArgumentNullException(nameof(inputArray));

            HashSet<T> set = new HashSet<T>(inputArray, equalityComparer);
            return set.ToArray();
        }

GenericEqualityComparer.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace DotNetLib.System.Collections.Generic
{
    [ComVisible(false)]
    // Generic wrapper for non-generic IEqualityComparer
    // GenericEqualityComparer<T> is a generic wrapper that delegates to the non-generic comparer.
    public class GenericEqualityComparer<T> : IEqualityComparer<T>
    {
        private readonly IEqualityComparer originalComparer;

        public GenericEqualityComparer(IEqualityComparer originalComparer)
        {
            this.originalComparer = originalComparer ?? throw new ArgumentNullException(nameof(originalComparer));
        }

        public bool Equals(T x, T y)
        {
            return originalComparer.Equals(x, y);
        }

        public int GetHashCode(T obj)
        {
            return originalComparer.GetHashCode(obj);
        }
    }
}

7
  • Do you have a fully compilable project available? Commented Mar 5, 2024 at 8:02
  • @SimonMourier The project working on is at github.com/MarkJohnstoneGitHub/VBA-DotNetLib thou the code displayed isn't in the current version. The RemoveDuplicates working on was a stepping stone to fix up a similar issue with Arrays and predicates. Probably will be a couple of days to sort through the mess created attempting to do reflection and generics. Commented Mar 5, 2024 at 8:35
  • Do have a "quick fix" solution on VBA side to pass in the outputArray eg declared as a Person array and attempt to copy the variant from the result. If it's an error (i.e. variant array when expecting person array copy to the output array. Value types appear fine it's just the VBA custom types would like to be able to get the object type if possible. I know it's bit more involved involving IDispatch and ITypeInfo. Commented Mar 5, 2024 at 8:39
  • 1
    Your project is a big thing and it doesn't really contains the code here, you don't have a smaller reproducible and compilable one? Commented Mar 5, 2024 at 18:58
  • @SimonMourier Could separate the SafeArray class working on and the array utility functions RemoveDuplicates. Atm trying to determine if the array element type is a custom VBA object i.e. a System.__COMObject, From there hopefully a method to obtain it's Type so can create an array of that type or instances dynamically. Might be hints in another project done (Refactor-COM-object-to-VBA-COM-wrapper-class) where utilized Rubberduck COM reflection components which use to help generate the VBA singleton classes from the DotNetLib.tlb. Commented Mar 5, 2024 at 21:34

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.