Search code examples
c#.netvbscriptcom-interop

Why is the indexer on my .NET component not always accessible from VBScript?


I have a .NET assembly which I am accessing from VBScript (classic ASP) via COM interop. One class has an indexer (a.k.a. default property) which I got working from VBScript by adding the following attribute to the indexer: [DispId(0)]. It works in most cases, but not when accessing the class as a member of another object.

How can I get it to work with the following syntax: Parent.Member("key") where Member has the indexer (similar to accessing the default property of the built-in Request.QueryString: Request.QueryString("key"))?

In my case, there is a parent class TestRequest with a QueryString property which returns an IRequestDictionary, which has the default indexer.

VBScript example:

Dim testRequest, testQueryString
Set testRequest = Server.CreateObject("AspObjects.TestRequest")
Set testQueryString = testRequest.QueryString
testQueryString("key") = "value"

The following line causes an error instead of printing "value". This is the syntax I would like to get working:

Response.Write(testRequest.QueryString("key"))

Microsoft VBScript runtime (0x800A01C2)
Wrong number of arguments or invalid property assignment: 'QueryString'

However, the following lines do work without error and output the expected "value" (note that the first line accesses the default indexer on a temporary variable):

Response.Write(testQueryString("key"))
Response.Write(testRequest.QueryString.Item("key"))

Below are the simplified interfaces and classes in C# 2.0. They have been registered via RegAsm.exe /path/to/AspObjects.dll /codebase /tlb:

[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IRequest {
    IRequestDictionary QueryString { get; }
}

[ClassInterface(ClassInterfaceType.None)]
public class TestRequest : IRequest {
    private IRequestDictionary _queryString = new RequestDictionary();

    public IRequestDictionary QueryString {
        get { return _queryString; }
    }
}

[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IRequestDictionary : IEnumerable {
    [DispId(0)]
    object this[object key] {
        [DispId(0)] get;
        [DispId(0)] set;
    }
}

[ClassInterface(ClassInterfaceType.None)]
public class RequestDictionary : IRequestDictionary {
    private Hashtable _dictionary = new Hashtable();

    public object this[object key] {
        get { return _dictionary[key]; }
        set { _dictionary[key] = value; }
    }
}

I've tried researching and experimenting with various options but have not yet found a solution. Any help would be appreciated to figure out why the testRequest.QueryString("key") syntax is not working and how to get it working.

Note: This is a followup to Exposing the indexer / default property via COM Interop.

Update: Here is some the generated IDL from the type library (using oleview):

[
  uuid(C6EDF8BC-6C8B-3AB2-92AA-BBF4D29C376E),
  version(1.0),
  custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, AspObjects.IRequest)

]
dispinterface IRequest {
    properties:
    methods:
        [id(0x60020000), propget]
        IRequestDictionary* QueryString();
};

[
  uuid(8A494CF3-1D9E-35AE-AFA7-E7B200465426),
  version(1.0),
  custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, AspObjects.IRequestDictionary)

]
dispinterface IRequestDictionary {
    properties:
    methods:
        [id(00000000), propget]
        VARIANT Item([in] VARIANT key);
        [id(00000000), propputref]
        void Item(
                        [in] VARIANT key, 
                        [in] VARIANT rhs);
};

Solution

  • I stumbled upon this exact problem a few days ago. I couldn't find a reasonable explanation as to why it doesn't work.

    After spending long hours trying different workarounds, I think I finally found something that seems to work, and is not so dirty. What I did is implement the accessor to the collection in the container object as a method, instead of a property. This method receives one argument, the key. If the key is "missing" or null, then the method returns the collection (this handles expressions like "testRequest.QueryString.Count" in VbScript). Otherwise, the method returns the specific item from the collection.

    The dirty part with this approach is that this method returns an object (because sometimes the return reference is the collection, and sometimes an item of the collection), so using it from managed code needs castings everywhere. To avoid this, I created another property (this time a proper property) in the container that exposes the collection. This property is NOT exposed to COM. From C#/managed code I use this property, and from COM/VbScript/unmanaged code I use the method.

    Here is an implementation of the above workaround using the example of this thread:

      [ComVisible(true)]
      [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
      public interface IRequest
      {
        IRequestDictionary ManagedQueryString { get; } // Property to use form managed code
        object QueryString(object key); // Property to use from COM or unmanaged code
      }
    
      [ComVisible(true)]
      [ClassInterface(ClassInterfaceType.None)]
      public class TestRequest : IRequest
      {
        private IRequestDictionary _queryString = new RequestDictionary();
    
        public IRequestDictionary ManagedQueryString
        {
          get { return _queryString; }
        }
    
        public object QueryString(object key)
        {
          if (key is System.Reflection.Missing || key == null)
            return _queryString;
          else
            return _queryString[key];
        }
      }
    
      [ComVisible(true)]
      [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
      public interface IRequestDictionary : IEnumerable
      {
        [DispId(0)]
        object this[object key]
        {
          [DispId(0)]
          get;
          [DispId(0)]
          set;
        }
    
        int Count { get; }
      }
    
      [ComVisible(true)]
      [ClassInterface(ClassInterfaceType.None)]
      public class RequestDictionary : IRequestDictionary
      {
        private Hashtable _dictionary = new Hashtable();
    
        public object this[object key]
        {
          get { return _dictionary[key]; }
          set { _dictionary[key] = value; }
        }
    
        public int Count { get { return _dictionary.Count; } }
    
        #region IEnumerable Members
    
        public IEnumerator GetEnumerator()
        {
          throw new NotImplementedException();
        }
    
        #endregion
      }