views:

1833

answers:

3

I have a legacy COM-component with an interface declaring a property like this (IDL notation):

interface IPasswordCallback : IUnknown {
    [propget] HRESULT Password( [in] VARIANT_BOOL ownerNeeded, [in, out]
        VARIANT_BOOL* isResultValid, [out, retval] BSTR* password );
}

This interface is implemented in a calling application written in VB6 like this:

Public Property Get IPasswordCallback_Password(ByVal ownerNeeded As Boolean,
    ByRef isResultValid As Boolean) As String
    'implementation cut
End Property

Everyting works fine until I try to do the same in VB.Net. VB.Net refuses to compile code where a parameter of a property is passed ByRef. MSDN says VB.Net doesn't allow such parameters.

Is there any way I could implement such a property in VB.Net?

+2  A: 

Create a new interface around the old interface that has the property properly implemented. You don't need the original source you can just reference it from VB6 and make a new ActiveX DLL/OCX to do the job. A bit of pain to do I realize.

I ran into this myself on my conversion project and had to go back to the old VB6 code and make sure all the property parameters were declared ByVal. The problem was that we had to wait for a major version change in the VB6 code (when we break binary compatibility). Doing makes the new DLL not compatible with the old DLL.

RS Conley
Did I get it right that you suggest creating a wrapper that implements the old interface and tunnels calls to/from my application? If so I'll have to distribute VB6 runtime in addition to .Net runtime with every copy of my application.
sharptooth
You tagged your question as vb6 so I assumed that the COM library was made with VB6. In which case the runtime is present if your are using the library. But even if it wasn't using VB6 to create is far easier then mucking around with C++.
RS Conley
+1  A: 

If you can't change the source code for the legacy COM component, you'll have to work around it. One possibility is to create a new VB6 component that implements IPasswordCallback and which does the actual work of getting the password by calling back into your .NET code. This way, the VB6 code can deal with the ByRef parameter, and the .NET code won't see it.

Most of the code to make it work will be VB6 code, which you can put into a new ActiveX DLL project and reference from your .NET project.

Here is an outline of the different classes and interfaces needed:

  • Class ByValPasswordCallbackWrapper (VB6): This class is a wrapper class that hides the ByRef parameter from the .NET code. When this class's Password property is called by the legacy COM component, this class will call a helper class (written in .NET) that will do the actual callback work. The VB6 class will then take the results from the helper class and return them to the legacy COM component.

  • Class PasswordCallbackArgs (VB6): This class is used to pass the parameters from a call to IPasswordCall_Password to the .NET helper class that will doing the real work.

  • Interface IPasswordCallbackProvider (VB6): This is the interface that your .NET code will implement instead of implementing IPasswordCallback directly.

The Code

Below is the code listing for each of the VB6 components mentioned above. You can add this code to a new ActiveX DLL project and compile it for use from your .NET code. Each file is listed separately. As an aside, make sure the Instancing property for each of these classes is set to "5-MultiUse".


File: PasswordCallbackArgs.cls

'Used to pass arguments around'

Public OwnerNeeded As Boolean
Public IsValidResult As Boolean


File: IPasswordCallbackProvider.cls

' This is the interface that your .NET                  '
' class should implement (instead of IPasswordCallback) '

Public Function GetPassword(ByVal args As PasswordCallbackArgs)

End Function


File: ByValPasswordCallbackWrapper.cls

Implements IPasswordCallback

Private m_callbackProvider As IPasswordCallbackProvider

Pubic Property Set CallbackProvider(ByVal callbackProvider As IPasswordCallbackProvider)

   Set m_callbackProvider = callbackProvider

End Property

Private Property Get IPasswordCallback_Password( _
   ByVal ownerNeeded As Boolean, _
   ByRef isResultValid As Boolean) As String

   IPasswordCallback_Password = DoGetPassword(ownerNeeded, isResultValid)

End Property

Private Function DoGetPassword(
   ByVal ownerNeeded As Boolean, _
   ByRef isResultValid As Boolean) As String

   If m_callbackProvider Is Nothing Then
      Err.Raise 5,,"No callback provider. DoGetPassword failed."
   End If

   'Wrap the arguments in a PasswordCallbackArgs object.'
   'Do not need to fill args.IsResultValid here - the callback provider will do that'

   Dim args As New PasswordCallbackArgs
   args.OwnerNeeded = ownerNeeded

   'Get the password and a value to put back into our ByRef isResultValid'
   DoGetPassword = m_callbackProvider.GetPassword(args)
   isResultValid = args.IsResultValid

End Sub


How to Use This Code

Once the above code has been compiled into an ActiveX DLL, add a reference to it from your .NET project.

Next, put the .NET code that you would have put into your IPasswordCallback implementation into a new class that implements IPasswordCallbackProvider. Remember that the callback parameters (ownerNeeded and isResultValid) are passsed to your provider class in a PasswordCallbackArgs object, so you will have to use args.ownerNeeded and args.isResultValid in your .NET class to refer to them.

Here is a stub provider class to get you started:

File: MyPasswordCallbackProvider.vb (VB.NET)

' A stub implementation of an IPasswordCallbackProvider '

Public Class MyPasswordCallbackProvider Implements IPasswordCallbackProvider

   Public Function GetPassword(PasswordCallbackArgs args) As String _
      Implements IPasswordCallbackProvider.Password

      Dim password As String = ""
      Dim resultWasValid As Boolean

      If args.OwnerNeeded Then
         'do stuff'
      Else
         'do other stuff'
      End If

      'do even more stuff'

      'set whether the result was valid or not'
      args.ResultValid = resultWasValid

      Return password

   End Property

End Class

In order to pass a valid IPasswordCallback to your legacy COM object, you will need to create a ByValPasswordCallbackWrapepr, set its CallbackProvider property, and then pass the wrapper object to the legacy COM object.

Using the above example, and assuming you have a instance of your legacy COM component called LegacyComObject, you would do something like the following to set up the callback:

' Create the callback wrapper '
ByValPasswordCallbackWrapper wrapper = New ByValPasswordCallbackWrapper()

' Tell the wrapper to call our custom IPasswordCallbackProvider '
wrapper.CallbackProvider = New MyPasswordCallbackProvider()

''" Pass the call back wrapper to the legacy COM object ''"
''" (not sure how this is done in your scenario. ''"
''" I'm just pretending it's a property since I don't know... "''
LegacyComObject.PasswordCallback = wrapper

Why This Works

This works because ByValPasswordCallbackWrapper implements the IPasswordCallback interface that the legacy COM object is expecting to receive. The ByValPasswordCallbackWrapper in turn provides a way to hook into the callback process via the IPasswordCallbackProvider interface. The IPasswordCallbackProvider COM interface is compatible with .NET, so you can write an implementing class using VB.NET. The ByValPasswordCallbackWrapper calls your IPasswordCallbackProvider to do the work of getting the password, and makes sure to put a value back into the ByRef parameter.

Mike Spross
I'll have to reditribute the VB6 runtime in addition to .Net runtime with every copy of my application, won't I?
sharptooth
Good point. I'm so used to having to do that anyway (our main product is a VB6 app) that I forget that might not be easy/desirable.
Mike Spross
However, I can't think of another way to do it (not to say that there isn't one). You could use C++ to implement the parts where I used VB6 instead. The issue is that you have to get around the ByRef problem by going outside of .NET one way or the other.
Mike Spross
The VB6 runtime comes with XP and Vista.
RS Conley
@RS Conley: I feel dumb for not knowing that. Our VB6 program's installer still distributes msvbvm60.dll because the original app ran on Win9x machines. I didn't realize it came pre-installed on XP and Vista.
Mike Spross
+1  A: 

All the answers are very useful, but they all suggest that I create this callback redirector in VB6 which is not very convenient since I may have to redistribute the VB6 runtime with the client application.

Here's an extended version of the solution. The redirector can be written in C#, since C# doesn't moan about passing by ref. It can be implemented as a class library and distributed as a .dll assembly. This way anyone who has .Net Framework installed can use such a redirector. And those who use VB.Net surely have .Net Framework installed.

sharptooth
Make sure there are no side effects. But if it works then that is a excellent tip
RS Conley
I agree with RS Conley. If it works in C#, that would be definitely be a nice tip for others facing this problem. Let us know how it works out.
Mike Spross
Well, looks like it's working allright. No problems found so far.
sharptooth
The VB6 runtime is a preinstalled system component in any version of Windows likely to run your VB.Net. The only caveat might be to perform testing on older runtime versions to make sure there aren't any issues corrected through later Service Packs.
Bob Riemersma