views:

400

answers:

5

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);
};
+1  A: 

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)...

Will
Thanks Will for your response. Using a temporary variable with an indexer does work (like testQueryString("key") ), but accessing it like testRequest.QueryString("key") does not work.
Mike Henry
I tried using get_Item and it had the same issue where I could use the indexer from a temporary variable but not like testRequest.QueryString("key").
Mike Henry
I got oleview working from the win 2003 resource kit and posted the generated IDL. PS Are your initials "WAG", or does that mean something else?
Mike Henry
A: 

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?

Mike Henry
+1  A: 

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
  }
mdraghi
Thanks for your thorough reply. I haven't had a chance to test this yet but will try to soon.
Mike Henry
+1  A: 

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

David Lee
A: 

Mike,

Did you ever resolve this issue - is there anyway to get this working other than the less than perfect work-around detailed above??

Matthew

matvdl
Sorry Matthew, it's been a while since I've looked at this issue and I haven't come up with a better solution. Have you looked at mdraghi's or David Lee's suggestions?
Mike Henry
Yes - I have reviewed these and they do provide a work-around - but I would term it more of a hack than a fix - and they wont work in my situation. I have opened a support case with Microsoft in the hope they might be able to come up with a better fix - if I get anywhere I will let you know.
matvdl