Question

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);
};
Was it helpful?

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
  }

OTHER TIPS

Results of my investigation on this subject:

The problem is relative to IDispatch implementation the common language runtime uses when exposing dual interfaces and dispinterfaces to COM.

Scripting language like VBScript (ASP) use OLE Automation IDispatch implementation when accessing to COM Object.

Despite it seems to work, I want to keep the property as a property and don't want to have a function (workaround explained above).

You have 2 possible solutions :

1 - Use the deprecated IDispatchImplAttribute with IDispatchImplType.CompatibleImpl.

    [ClassInterface(ClassInterfaceType.None)]
    [IDispatchImpl(IDispatchImplType.CompatibleImpl)]
    public class TestRequest : IRequest
    {
        private IRequestDictionary _queryString = new RequestDictionary();
        public IRequestDictionary QueryString
        {
            get { return _queryString; }
        }
    }

As said in MSDN, this attribute is deprecated but still working with .Net 2.0, 3.0, 3.5, 4.0. You have to decide if the fact that it is "deprecated" could be a problem for you...

2 - Or implement IReflect as a custom IDispatch in your class TesRequest or create a generic class that implement IReflect and make your class inherits this new created one.

Generic class sample (the interresting part is in the InvokeMember Method):

[ComVisible(false)]
public class CustomDispatch : IReflect
{
    // Called by CLR to get DISPIDs and names for properties
    PropertyInfo[] IReflect.GetProperties(BindingFlags bindingAttr)
    {
        return this.GetType().GetProperties(bindingAttr);
    }

    // Called by CLR to get DISPIDs and names for fields
    FieldInfo[] IReflect.GetFields(BindingFlags bindingAttr)
    {
        return this.GetType().GetFields(bindingAttr);
    }

    // Called by CLR to get DISPIDs and names for methods
    MethodInfo[] IReflect.GetMethods(BindingFlags bindingAttr)
    {
        return this.GetType().GetMethods(bindingAttr);
    }

    // Called by CLR to invoke a member
    object IReflect.InvokeMember(string name, BindingFlags invokeAttr, Binder binder, object target, object[] args, ParameterModifier[] modifiers, System.Globalization.CultureInfo culture, string[] namedParameters)
    {
        try
        {
            // Test if it is an indexed Property
            if (name != "Item" && (invokeAttr & BindingFlags.GetProperty) == BindingFlags.GetProperty && args.Length > 0 && this.GetType().GetProperty(name) != null)
            {
                object IndexedProperty = this.GetType().InvokeMember(name, invokeAttr, binder, target, null, modifiers, culture, namedParameters);
                return IndexedProperty.GetType().InvokeMember("Item", invokeAttr, binder, IndexedProperty, args, modifiers, culture, namedParameters);
            }
            // default InvokeMember
            return this.GetType().InvokeMember(name, invokeAttr, binder, target, args, modifiers, culture, namedParameters);
        }
        catch (MissingMemberException ex)
        {
            // Well-known HRESULT returned by IDispatch.Invoke:
            const int DISP_E_MEMBERNOTFOUND = unchecked((int)0x80020003);
            throw new COMException(ex.Message, DISP_E_MEMBERNOTFOUND);
        }
    }

    FieldInfo IReflect.GetField(string name, BindingFlags bindingAttr)
    {
        return this.GetType().GetField(name, bindingAttr);
    }

    MemberInfo[] IReflect.GetMember(string name, BindingFlags bindingAttr)
    {
        return this.GetType().GetMember(name, bindingAttr);
    }

    MemberInfo[] IReflect.GetMembers(BindingFlags bindingAttr)
    {
        return this.GetType().GetMembers(bindingAttr);
    }

    MethodInfo IReflect.GetMethod(string name, BindingFlags bindingAttr)
    {
        return this.GetType().GetMethod(name, bindingAttr);
    }

    MethodInfo IReflect.GetMethod(string name, BindingFlags bindingAttr,
    Binder binder, Type[] types, ParameterModifier[] modifiers)
    {
        return this.GetType().GetMethod(name, bindingAttr, binder, types, modifiers);
    }

    PropertyInfo IReflect.GetProperty(string name, BindingFlags bindingAttr,
    Binder binder, Type returnType, Type[] types,
    ParameterModifier[] modifiers)
    {
        return this.GetType().GetProperty(name, bindingAttr, binder,
        returnType, types, modifiers);
    }

    PropertyInfo IReflect.GetProperty(string name, BindingFlags bindingAttr)
    {
        return this.GetType().GetProperty(name, bindingAttr);
    }

    Type IReflect.UnderlyingSystemType
    {
        get { return this.GetType().UnderlyingSystemType; }
    }
}

and for Mike's code:

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

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

I David Porcher solution works for me.

But the code that he had posted handle the Get part of the indexer, so I updated his code to handle also the Set part of the indexer

Here is the updated code:

   // Called by CLR to invoke a member
    object IReflect.InvokeMember(string name, BindingFlags invokeAttr, Binder binder, object target, object[] args, ParameterModifier[] modifiers, System.Globalization.CultureInfo culture, string[] namedParameters)
    {
        try
        {
            // Test if it is an indexed Property - Getter
            if (name != "Item" && (invokeAttr & BindingFlags.GetProperty) == BindingFlags.GetProperty && args.Length > 0 && this.GetType().GetProperty(name) != null)
            {
                object IndexedProperty = this.GetType().InvokeMember(name, invokeAttr, binder, target, null, modifiers, culture, namedParameters);
                return IndexedProperty.GetType().InvokeMember("Item", invokeAttr, binder, IndexedProperty, args, modifiers, culture, namedParameters);
            }
            // Test if it is an indexed Property - Setter
            // args == 2 : args(0)=Position, args(1)=Vlaue
            if (name != "Item" && (invokeAttr & BindingFlags.PutDispProperty) == BindingFlags.PutDispProperty && (args.Length == 2) && this.GetType().GetProperty(name) != null)
            {
                // Get The indexer Property
                BindingFlags invokeAttr2 = BindingFlags.GetProperty;
                object IndexedProperty = this.GetType().InvokeMember(name, invokeAttr2, binder, target, null, modifiers, culture, namedParameters);

                // Invoke the Setter Property
                return IndexedProperty.GetType().InvokeMember("Item", invokeAttr, binder, IndexedProperty, args, modifiers, culture, namedParameters);
            }


            // default InvokeMember
            return this.GetType().InvokeMember(name, invokeAttr, binder, target, args, modifiers, culture, namedParameters);
        }
        catch (MissingMemberException ex)
        {
            // Well-known HRESULT returned by IDispatch.Invoke:
            const int DISP_E_MEMBERNOTFOUND = unchecked((int)0x80020003);
            throw new COMException(ex.Message, DISP_E_MEMBERNOTFOUND);
        }
    }

WAG here... Have you examined your assembly with oleview to make sure your public interface has an indexer visible to com consumers? Second WAG is to use the get_Item method directly, rather than trying to use the indexer property (CLS compliance issues)...

I found that testRequest.QueryString()("key") works, but what I want is testRequest.QueryString("key").

I found a very relevant article by Eric Lippert (who has some really great articles on VBScript, by the way). The article, VBScript Default Property Semantics, discusses the conditions for whether to invoke a default property or just a method call. My code is behaving like a method call, though it seems to meet the conditions for a default property.

Here are the rules from Eric's article:

The rule for implementers of IDispatch::Invoke is if all of the following are true:

  • the caller invokes a property
  • the caller passes an argument list
  • the property does not actually take an argument list
  • that property returns an object
  • that object has a default property
  • that default property takes an argument list

then invoke the default property with the argument list.

Can anyone tell if any of these conditions are not being met? Or could it be possible that the default .NET implementation of IDispatch.Invoke behaves differently? Any suggestions?

I have spent a couple of days with the exact same issue trying every possible variation using multiple tactics. This post solved my issue:

following used to generate error parentobj.childobj(0) previously had to do: parentobj.childobj.item(0)

by changing:

Default Public ReadOnly Property Item(ByVal key As Object) As string
    Get
        Return strSomeVal

    End Get
End Property

to:

Public Function Fields(Optional ByVal key As Object = Nothing) As Object

    If key Is Nothing Then
        Return New clsFieldProperties(_dtData.Columns.Count)
    Else
        Return strarray(key)
    End If
End Function

where:

Public Class clsFieldProperties Private _intCount As Integer

Sub New(ByVal intCount As Integer)
    _intCount = intCount

End Sub
Public ReadOnly Property Count() As Integer
    Get
        Return _intCount
    End Get
End Property

End Class

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top