Implementing collection for VB script in C#

N

Nikolay Belyh

I have created a "collection" in C# like this:

namespace ClassLibrary1
{
public class X {}

public class Class1
{
public ArrayList objs
{
get
{
return new ArrayList(new object[] { new X(), new X(),
new X() } );
}
}
}
}

Then I am trying to use this collection from VB script like this:

Set c = CreateObject("ClassLibrary1.Class1")
Set x = c.objs.Item(1) ' works
Set x = c.objs(1) ' does not work - here is the problem
(*)

At the same time, similar thing works just fine with Excel (for
example):

Set e = CreateObject("Excel.Application")
e.Workbooks.Add
Set x = e.Worksheets(1) ' Works

How do I make (*) work?
Kind regards.
 
G

Greg

Did you try adding the 'this[ ]' indexer and setting that as your
default property by decorating it with DefaultPropertyAttribute?

On the other hand, I'm surprised this 'almost' worked since the
VBA.CreateObject() method call is explicitly used for COM objects, and
by the looks of your code, it doesn't appear to be exposed to COM. The
reason it works for Excel is because Excel is a COM object.

regards,

Greg
 
N

Nikolay Belyh

Greg, thank you for reply.
Did you try adding the 'this[ ]' indexer and setting that as your
default property by decorating it with DefaultPropertyAttribute?

I did not try DefaultPropertyAttribute, since AFAIK it is unrelated to
COM after all.. But now I did :)
Still no luck. What I did try before is trying to use [DispId(0)]
attribute on indexer. Withe the same (negative) result though..
On the other hand, I'm surprised this 'almost' worked since the
VBA.CreateObject() method call is explicitly used for COM objects, and
by the looks of your code, it doesn't appear to be exposed to COM. The
reason it works for Excel is because Excel is a COM object.

The assembly is made visible for COM using project settings. I mean, 2
chekcboxes
"Make assembly COM-visible" and "Register for COM interop" in project
settings.

I have also tried to make the COM type library for the assembly look
exactly as Excel's one
in the part of enumerator implementation. Though it did not work
either, below is the code I tried:

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

namespace ClassLibrary2
{
public interface IMyColl
{
[DispId(-4)]
object _NewEnum
{
[return: MarshalAs(UnmanagedType.IUnknown)]
get;
}

[DispId(0)]
[IndexerName("_Default")]
object this[int index]
{
get;
}
}

public class X {}

[ComDefaultInterface(typeof(IMyColl))]
[ClassInterface(ClassInterfaceType.None)]
public class MyColl : IMyColl
{
ArrayList items;

public MyColl()
{
items = new ArrayList(new object[] { new X(), new X(), new
X() });
}

public object _NewEnum
{
get { return items.GetEnumerator(); }
}

public object this[int index]
{
get
{
return items[index];
}
}
}

public interface IMyClass
{
MyColl objs { get; }
}

[ComDefaultInterface(typeof(IMyClass))]
[ClassInterface(ClassInterfaceType.None)]
public class Class1 : IMyClass
{
public MyColl objs
{
get { return new MyColl(); }
}
}
}

This way it works when target environment sees the type library (from
Excel VBA for example)
But it still does not work from the script.
 
W

Willy Denoyette [MVP]

Greg, thank you for reply.
Did you try adding the 'this[ ]' indexer and setting that as your
default property by decorating it with DefaultPropertyAttribute?

I did not try DefaultPropertyAttribute, since AFAIK it is unrelated to
COM after all.. But now I did :)
Still no luck. What I did try before is trying to use [DispId(0)]
attribute on indexer. Withe the same (negative) result though..
On the other hand, I'm surprised this 'almost' worked since the
VBA.CreateObject() method call is explicitly used for COM objects, and
by the looks of your code, it doesn't appear to be exposed to COM. The
reason it works for Excel is because Excel is a COM object.

The assembly is made visible for COM using project settings. I mean, 2
chekcboxes
"Make assembly COM-visible" and "Register for COM interop" in project
settings.

I have also tried to make the COM type library for the assembly look
exactly as Excel's one
in the part of enumerator implementation. Though it did not work
either, below is the code I tried:

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

namespace ClassLibrary2
{
public interface IMyColl
{
[DispId(-4)]
object _NewEnum
{
[return: MarshalAs(UnmanagedType.IUnknown)]
get;
}

[DispId(0)]
[IndexerName("_Default")]
object this[int index]
{
get;
}
}

public class X {}

[ComDefaultInterface(typeof(IMyColl))]
[ClassInterface(ClassInterfaceType.None)]
public class MyColl : IMyColl
{
ArrayList items;

public MyColl()
{
items = new ArrayList(new object[] { new X(), new X(), new
X() });
}

public object _NewEnum
{
get { return items.GetEnumerator(); }
}

public object this[int index]
{
get
{
return items[index];
}
}
}

public interface IMyClass
{
MyColl objs { get; }
}

[ComDefaultInterface(typeof(IMyClass))]
[ClassInterface(ClassInterfaceType.None)]
public class Class1 : IMyClass
{
public MyColl objs
{
get { return new MyColl(); }
}
}
}

This way it works when target environment sees the type library (from
Excel VBA for example)
But it still does not work from the script.



You won't be able to use the indexer syntax on a .NET collection like you do
on a COM collection without some hand code interface trickery. Note that
..NET collections are not exactly like COM collections, for instance the
default lower bound is 0, while it's 1 for COM (try Worksheets(0)).

If you change your code a bit like this:

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

namespace ClassLibrary2
{
public interface IMyColl : IEnumerable // (1)
{
[DispId(-4)]
new IEnumerator GetEnumerator(); // (2)
[DispId(1)] // (3)
int Count
{
get;
}
[DispId(0)]
object this[int index]
{
get;
}
}
public class X { // (4)
int _val;
public int Val
{
get
{
return _val;
}
}
}

[ComDefaultInterface(typeof(IMyColl))]
[ClassInterface(ClassInterfaceType.None)]
public class MyColl : IMyColl
{
ArrayList items;

public MyColl()
{
items = new ArrayList(new object[] { new X(), new X(), new
X() });
}
public IEnumerator GetEnumerator()
{
return items.GetEnumerator();
}
public int Count
{
get { return items.Count;}
}
public object this[int index]
{
get
{
return items[index];
}
}
}

public interface IMyClass
{
MyColl objs { get; }
}

[ComDefaultInterface(typeof(IMyClass))]
[ClassInterface(ClassInterfaceType.None)]
public class Class1 : IMyClass
{
public MyColl objs
{
get { return new MyColl(); }
}
}
}


You will be able to use this class from VBscript like this:

Dim obj, x
Set obj = Wscript.CreateObject("ClassLibrary2.Class1")
Set coll = obj.objs
For Each v in coll
WScript.Echo v.Val
Next
WScript.Echo coll.Count
For i = 0 To coll.Count - 1 'lower bound is 0!!
WScript.Echo x(i).Val
Next

Note that I set coll to obj.objs which is exposed as an IEnumVARIANT by the
COM Interop layer(wrapped in a VARIANT by the scripting engine).
You can use indexing syntax on the coll object to get a specific object from
the collection, while you can also For ... Each over the collection

Willy.
 
N

Nikolay Belyh

Willy, thank you for reply.

My aim is to achieve the excel syntax, the rest of the collection
stuff is not a problem.
It seem to works just fine with first trivial example I have posted.
What I want is to be able to do is the folloiwng (i.e. the problem is
supporting this exactly syntax):

Set a = mycoll.objs(1)
--------------------------------

I.e. like Excel's:

Set doc = app.Documents(1)

Today I can access the items of my collection like that:

Set a = mycoll.objs.Item(1)

Or like that (works too):

Set coll = mycoll.objs
Set a = mycoll(1)

I can iterate over the collection like that:

For each a in mycoll.objs
...
Next

But I cannot do that "mycoll.objs(1)"

The VB script complains about "invalid number of arguments or property
assignment"
Maybe there is some trickery involved in Excel's dispatch
implementation that handles the above expression somehow different?
 
W

Willy Denoyette [MVP]

Nikolay Belyh said:
Willy, thank you for reply.

My aim is to achieve the excel syntax, the rest of the collection
stuff is not a problem.


Why? What don't you like about this pattern:
Set coll = mycoll.objs
Set entry = mycoll(1)
....

Keeping in mind that mycoll(1) does not refer to the first entry in the
collection like it's the case when calling into COM.

Anyway, it looks like none of the (2) CLR's internal IDispatch handlers
handle the indexer syntax correctly. All you can try to do about this is to
implement a custom IDispatch marshaler and use this one instead, but I'm not
sure that even then, you will be able to overcome this issue.

Willy.
 
N

Nikolay Belyh

Why? What don't you like about this pattern:
 Set coll = mycoll.objs
 Set entry = mycoll(1)

There is no particular reason, actually.
I was jsut curious, why that syntax did not work, and how to make it
work...
You know, that kind of curiosity when a kid breaks a toy into 17
pieces to see what's in there.. :)
Anyway, it looks like none of the (2) CLR's internal IDispatch handlers
handle the indexer syntax correctly. All you can try to do about this is to
implement a custom IDispatch marshaler and use this one instead, but I'm not
sure that even then, you will be able to overcome this issue.

But I have already bet $20 this can be done... So I just can't give up
that easily :)

Kind regards, Nikolay.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top