Accessing Arrays, Lists and Dictionaries with ComArray

One of the trickiest aspects of COM Interop from FoxPro and .NET is on how to deal with Arrays and Collections. COM is not well suited to marshal these structures to FoxPro and the problem excaberated by the fact that most collection structures in .NET are Generic types (ie. List<T> or Dictionary<string, Person>) which simply can't be marshaled over COM at all.

To help with this wwDotnetBridge provides a ComArray helper that:

  • Wraps a .NET Array, List, Dictionary or Collection
  • Keeps the Instance inside of .NET
  • Provides Proxy methods to retrieve, add, remove, update and iterate the collection
  • Automatically creates ComArray instances for collection results from InvokeMethod() and GetProperty()
  • Lets you create and pass collections from FoxPro to .NET

Samples can be found in Samples/wwDotnetBridge/CollectionTests.prg

Common Tasks for collections

Let's take a look at a few common tasks that you might do with collections. Keep in mind that there are many different types of collections in .NET:

  • Arrays - basic arrays use index lookups (string[] Strings)
  • Lists - Generic Lists use index lookups (List<T> Persons)
  • Dictionaries - Key Value use Key Lookups(Dictionary<string, Person> Persons)
  • HashSets - Collections use value lookups (HashSet<Person> Persons)
  • Custom Collections

This is not a complete list - there are many custom collection types in .NET but most implement at least ICollection, IList, IDictionary or at the lowest level IEnumerable.

In .NET all of these structures have natural language constructs, but for Interop this is tricky because COM doesn't support these types directly. ComArray can help here, as it allows access to many features via Proxy.

Retrieving a Collection

The first thing is to figure out how to access a Collection in .NET. In most cases in order to retrieve a collections as a result to a method call or via property access, you'll want to use InvokeMethod() or GetProperty().

First understand that the following does not work on generic collections and even on types like Arrays while it may work, you can't easily manipulate and access the array.

The following does not work

loList = loNet.GetGenericList()  && returns List<T>
? loList   && (Object)
? loList.Count && fails - any access to loList does

This fails because the list is generic and generic types, value types, inherited types etc. don't work over COM. For collections of any kind it's easier to use the indirect access methods which return a ComArray instance instead.

The following does work:

*** Returns a ComArray instance
loList = loBridge.InvokeMethod(loNet,"GetGenericList")
? loList.Count  && 2
? loList.Item(0)   && (Object)

*** Returns a ComArray instance
loList = loBridge.GetProperty(loNet,"PersonList")
? loList.Count  && 2

This applies to most collection types.

Arrays kinda sorta work without but not really

Although Array can be marshalled to FoxPro as a Fox array, the array does not stay a .NET array, which means you can't update the array itself by adding, removing etc. items. You also cannot send the array back to .NET for updates because it no longer is a .NET array.

So to retrieve or send collections and arrays it's recommended to always using InvokeMethod(), GetProperty() and work with the ComArray that is retrieved instead.

Array and List<T> Access and Iteration

Lists are indexed lists that can be accessed by numeric indexes - an integer. This includes Array, ArrayList, List<T> and any IList interface instances in .NET.

Let's start with a List although the following code also works with arrays - or anything that requires an numeric integer index:

loBridge = GetwwDotnetBridge()
loNet = loBridge.Createinstance("Westwind.WebConnection.TypePassingTests")

***  Retrieve a List<T> from .NET - returns ComArray instance
loList = loBridge.InvokeMethod(loNet,"GetGenericList")
? loList.Count  && 2

*** First Item
loPerson = loList.Item(0)
? loPerson.Company

*** Iterate the entire list
FOR lnX = 0 to loList.Count
    loPerson = loList.Item(lnX)
    ? loPerson.Company
ENDIF

Dictionary Access and Iteration

Dictionaries are key value pairs that have a key that lets you look up an item with, and a value. This applies to any type that implements IDictionary or IDictionary<TKey,TValue>.

Dictionaries by default access values via a key:

*** Retrieve a Dictionary<string, Person> - returns ComArray
loList = loBridge.InvokeMethod(loNet,"GetDictionary")
? loBridge.ToString(loList)
? loList.Count  &&  2

*** Return Item by Key
loCust =  loList.Item("Item1")   && Retrieve item by Key
? loCust.Company

*** You can also retrieve an item by int index if the key is not int
loItem = loList.Item(0)
? loItem.Company

*** This allows Dictionary iteration via numeric index
FOR lnX = 0 TO loList.Count -1
   loItem = lolist.Item(lnX)
   ? loItem.Company
ENDFOR

Notice that ComArray provides additional functionality to also access the dictionary via int keys in order to allow for iterating the list of values. Without this FoxPro wouldn't be able to access the Enumerator, but the index allows retrieving all values.

Updating values in the Collection

Collections are containers for object references typically, so you've already seen above how you can access individual items in a collection:

loList = loBridge.InvokeMethod(loNet,"GetGenericList")

*** First Item
loPerson = loList.Item(0)

*** Change a value - immediately visible in .NET (reference)
loPerson.Company = "New Company"

Since the person object lives in .NET as a reference making the change here immediately updates the object in .NET. If .NET uses the collection in other code it'll immediately see the change.

Updating the Collection itself

The same logic applies if you update the collection itself by updating the collection by adding, removing or clearing items on the collection.

***  Retrieve a List<T> from .NET - returns ComArray instance
loList = loBridge.InvokeMethod(loNet,"GetGenericList")
? loList.Count  && 2

*** First Item
loPerson = loList.Item(0)
? loPerson.Company

loPerson = loList.CreateItem()  && creates new Person object
loPerson.Company = "East Wind Technologies"
loList.AddItem(loPerson)
? loList.Count && 3

For a dictionary that looks a little different because you have to also provide the key:

loList = loBridge.InvokeMethod(loNet,"GetDictionary")
? loList.Count && 2

loCust =  loList.Item("Item1")   && Retrieve item by Key
? loCust.Company

loPerson = loList.CreateItem()  && creates new Person object
loPerson.Company = "North Wind Technologies"
loList.AddItem("Item3",loPerson)
? loList.Count && 3

Passing a Collection to .NET

Just like retrieving a collection, there are issues passing collections to .NET directly over COM. For the same reason you'll want to use InvokeMethod() and SetProperty() to pass and assign collections to .NET objects.

***  Retrieve a List<T> from .NET - returns ComArray instance
loList = loBridge.InvokeMethod(loNet,"GetGenericList")
? loList.Count  && 2

*** First Item
loPerson = loList.Item(0)
? loPerson.Company

loPerson = loList.CreateItem()  && creates new Person object
loPerson.Company = "East Wind Technologies"
loList.AddItem(loPerson)
? loList.Count && 3

*** Pass ComArray to method that expects `List<Person>` here
loBridge.InvokeMethod(loNet,"SetGenericList", loList)

Rather than passing a List<Person> to the method in question, we're passing the ComArray that **contains the Instance of List<Person>. InvokeMethod() and SetProperty() both pick up the Instance value from the COM Array and replace that as the parameter passed to .NET so that the method signature to the SetGenericList(List<Person> persons) is properly addressed.

Creating a new Collection and Passing to .NET

In the previous example I used a collection that already exists and passed it back to .NET.

But a more likely scenario is that your code needs to create a collections and then pass it to .NET. Here's how to do it with a list:

*** Create an ComArray instance without any data
loList  = loBridge.CreateArray() 

*** This also works, but obvious more verbose
*loList = loBridge.CreateInstance("Westwind.WebConnection.ComArray")

*** Create empty Instance of  List<T>
loList.CreateList("Westwind.WebConnection.TestCustomer")

*** Add 2 Items
loCust = loList.CreateItem()
? loCust
loCust.Company = "North Wind Traders"
loList.AddItem(loCust)
? lolist.Count  && 1

loCust = loList.CreateItem()
? loCust
loCust.Company = "East Wind Traders"
loList.AddItem(loCust)
? lolist.Count  && 2

? loBridge.InvokeMethod(loNet, "SetDictionary", loList)  && 2

Using a Dictionary looks a little different but similar except for he extra key added:

*** Create an unconfigured ComArray instance
loList  = loBridge.CreateArray()

*** Create Dictionary<TKey, TValue>
loList.CreateDictionary("System.String", "Westwind.WebConnection.TestCustomer")

*** Create a new Person instance (based on the Instance type)
loCust = loList.CreateItem()
? loCust
loCust.Company = "North Wind Traders"
loList.AddDictionaryItem("Item1" , loCust)
? lolist.Count

loCust = loList.CreateItem()
? loCust
loCust.Company = "East Wind Traders"
loList.AddDictionaryItem("Item2" , loCust)
? lolist.Count

? loBridge.InvokeMethod(loNet, "SetDictionary", loList)  && 2

© West Wind Technologies, 2023 • Updated: 10/21/22
Comment or report problem with topic