7

Can anyone instruct me on how to code a C# enumerable class such that the "for each" construct in Excel VBA works properly? I tried this out with a test class called People that implements IEnumerable and contains an array of Person objects. The "foreach" construct works fine in C#, but in VBA I am only able to loop the old fashioned way.

This VBA code works just fine:

Dim P As Person
Dim PP As New People

For i = 0 To PP.Count - 1
    Set P = PP(i)
    Debug.Print P.firstName + " " + P.lastName
Next i

But this fails at run time ("Object doesn't support this property or method"):

For Each P In PP
    Debug.Print P.firstName + " " + P.lastName
Next P

Here is the C# code (compiled COM visible in VS 2008 for use with Excel VBA - Office 2010):

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

public class Person
{
    public Person(string fName, string lName)
    {
        this.firstName = fName;
        this.lastName = lName;
    }
    public string firstName;
    public string lastName;
}

public class People : IEnumerable
{
    private Person[] _people;                           // array of people
    public Int32 Count() { return _people.Length; }     // method to return array size

    // indexer method to enable People[i] construct, or in VBA: People(i)
    public Person this[Int32 PersonNo] { get { return _people[PersonNo]; } }

    // constructor - hardcode to initialize w 3 people (for testing)
    public People()
    {
        _people = new Person[3]
        {
            new Person("John", "Smith"),
            new Person("Jim", "Johnson"),
            new Person("Sue", "Rabon"),
        };
    }

    // test method just to make sure the c# foreach construct works ok
    public void Test() 
    { 
        foreach (Person P in this) System.Diagnostics.Debug.WriteLine(P.firstName + " " + P.lastName); 
    }

    //implementation of basic GetEnumerator
    IEnumerator IEnumerable.GetEnumerator()
    {
        return (IEnumerator)GetEnumerator();
    }

    //implementation of People GetEnumerator
    public PeopleEnum GetEnumerator()
    {
        return new PeopleEnum(_people);
    }
}

// People Enumerator class definition
public class PeopleEnum : IEnumerator
{
    public Person[] _people;

    int position = -1;

    public PeopleEnum(Person[] list)
    {
        _people = list;
    }

    public bool MoveNext()
    {
        position++;
        return (position < _people.Length);
    }

    public void Reset()
    {
        position = -1;
    }

    object IEnumerator.Current
    {
        get
        {
            return Current;
        }
    }

    public Person Current
    {
        get
        {
            try
            {
                return _people[position];
            }
            catch (IndexOutOfRangeException)
            {
                throw new InvalidOperationException();
            }
        }
    }
}
tpascale
  • 2,516
  • 5
  • 25
  • 38
  • [related: but VBA based](http://stackoverflow.com/questions/19373081/how-to-use-the-implements-in-excel-vba/19379641#19379641) –  Jun 09 '14 at 07:45

2 Answers2

6

Try adding [DispId(-4)] to your GetEnumerator() method. This flags it to be the DISPID_NEWENUM member. In order for VBA to work with a collection using For Each, it needs to implement _newEnum via COM.

This can be done by implementing an Enumerator and attributing it with the proper DispId. This is typically done via implementing a custom interface with this specified, though there are other mechanisms available.

Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • 2
    Thanks for the fast reply ... I tried the quick-fix - i.e. adding [DispId(-4)] to GetEnumerator. Now the error msg is "Property let procedure not defined and property get procedure did not return an object". I tried adding a "set" method to the People indexer, but that didn't help. If there are any other quick things to try, let me know. Meanwhile I'm following your other suggested link (but that will take a little more time to digest). Thanks – tpascale Jan 25 '11 at 18:32
  • @tpascale: I think you need to make a COM visible interface, and have this implement it. It should include the GetEnumerator method with the [DispId(-4)] flagged on it. See that link for examples... – Reed Copsey Jan 25 '11 at 18:37
  • The project is already COM visible - and Excel VBA can already use these objects and loop using an index. The foreach construct works in C# too - just doesn't work in VBA even with the DispID(-4) trick. VBA collections are not .NET collections so the fact that it works ok in .NET but not in VBA is not terrribly surprising. Anyway, I very much appreciate the comments which clearly have pointed me in the right direction (even if I am still foggy on how to cross the goal line). – tpascale Jan 27 '11 at 18:30
0

I came across this thread in Feb 2020 whilst experiencing the same problem with a class I wrote as a wrapper for a C# Dictionary.

The get enumerator method in the class is

    public IEnumerator GetEnumerator()
    {
        foreach (KeyValuePair<dynamic, dynamic> myPair in MyKvp)
        {
            yield return new dynamic[] { myPair.Key, myPair.Value };
        }
    }

I found that the DispId(-4) was successful when I added the attribute to the signature in the Interface.

Before (in interface)

IEnumerator GetEnumerator();

Which prodeced an object does not know this methos error in VBA

After (in interface)

[DispId(-4)]
IEnumerator GetEnumerator();

This update allowed me to iterate using a For Each loop in VBA.

freeflow
  • 4,129
  • 3
  • 10
  • 18