可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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:
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
}
回答2:
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; }
}
}
回答3:
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);
}
}
回答4:
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)...
回答5:
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?
回答6:
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