Índice de la sección dedicada a .NET (en el Guille) Cómo... en .NET

Usar un componente .NET desde COM
o cómo crear un servidor .NET para usar desde COM


Publicado el 04/Ene/2003
Actualizado el 29/Nov/2006

Links a los otros artículos de esta serie: 1, 2 y 3

Usar un formulario de .NET desde VB6 (29/Nov/06)
(con resumen con los pasos a seguir)

 

La batallita del abuelo:

Estaba comprobando cómo utilizar un componente creado con .NET Framework en una aplicación de Visual Basic 6.0 mediante automatización COM y me puse a leer la documentación de Visual Studio .NET, encontré varios ejemplos y los puse en práctica.

Pero no conseguía lo que yo quería: crear un componente que fuese "compatible" (u operativo) con COM, que no me obligara a regenerar la aplicación cliente (creada con VB6) cada vez que modificara el componente creado con .NET (el servidor).

Seguí buscando en la documentación, (seguramente en la red habrá algún ejemplo que hiciera lo que yo quería hacer, pero como, en mi casa que es donde estaba programando, no tengo conexión a internet).
La cuestión es que no encontraba nada que me "solucionara el problemilla".
Lo único que encontré eran recomendaciones de cómo no debía hacerlo, pero los ejemplos, (seguramente por ser simples), siempre mostraban el código no recomendable.

Hice algunas pruebas y, más o menos, conseguí lo que quería, pero me faltaba poder usar los eventos que el componente .NET implementaba. La única forma de que esos eventos fuesen visibles en VB6, (que me permitiera crear un objeto usando WithEvents), era usando nuevamente lo que no recomendaban, además de que perdía la "compatibilidad" con el ejecutable creado.

Seguí buscando y me encontré con un ejemplo que estaba escrito en C#, pero si has tenido algún contacto con C#, sabrás que las cosas "complicadas" se hacen de forma más fácil o, si lo prefieres, de forma más natural o intuitiva, pero de eso hablaremos en otra ocasión.

A esas alturas del día, (más bien de la noche), la situación era la siguiente:
Podía crear un componente .NET que funcionara en VB6 y que no me obligara a volver a generar el ejecutable cada vez que hiciera una modificación, por pequeña que fuese; incluso con el simple hecho de compilar la librería, tenía que volver a generar el ejecutable.

Pero no podía utilizar los eventos producidos por el componente .NET.
Seguí probando un ciento de cosas, para ver cómo podía crear el componente .NET y que me permitiera declarar un evento que fuese entendido por COM y mantuviese la "compatibilidad binaria" con el cliente (un ejecutable creado con VB6).
Y al final, después de probar hasta las cosas más rebuscadas, decidí tirar por "el camino de en medio" y hacer lo que siempre recomiendo a otros: buscar la solución más sencilla... y efectivamente, así fue.

Otro inconveniente con el que me encontré, fue que al estar utilizando clases que estaban derivadas de otras, (mediante herencia), el componente .NET no mostraba, en el cliente COM, los métodos y propiedades heredadas. A esto no encontré solución en la documentación, (si quería hacer las cosas bien), así que, tuve que hacer algo que no es lo más correcto, ya que, precisamente la herencia está para no tener que hacerlo, pero, es que si no lo hacía, no podía usar los miembros heredados.
Por tanto, tuve que escribir cada uno de los miembros heredados en cada una de las clases derivadas, aunque, eso sí, con el mínimo código posible, pero... como te comento, esto no es "lógico", así que seguiré buscando la solución, pero por ahora, salvo ese "pequeño" inconveniente, todo funciona bien, y lo más importante: ¡ya se cómo hacer las cosas bien!, a pesar de que no haya ejemplos "clarificadores" en la documentación de Visual Studio .NET, por suerte existen sitios en internet que te muestre cómo hacer las cosas que no te explican de forma "sencilla" en los libros y la documentación.

 

En este artículo veremos tanto cómo hacer las cosas de la forma adecuada y recomendada, pero también veremos cómo NO se deben hacer las cosas, para que no tengas excusas.

 

La teoría:

La verdad es que si me conoces, esto de las teorías y los métodos científicos no es lo mío, así que no te cansaré demasiado y te lo explicaré de forma que, aunque no técnica, espero que te quede claro.

Nota: Si quieres algo más técnico, te invito a que consultes la documentación correspondiente.

Los componentes COM (librerías ActiveX, etc.) utilizan siempre interfaces para poder acceder a los métodos y propiedades de una clase. Cuando creamos uno de estos componentes se crea una especie de "lista" con las interfaces que expone así como una lista con los miembros de cada una de esas interfaces.
Si creamos un cliente para ese componente, por ejemplo: un ejecutable que utilice la librería ActiveX, se creará otra lista en la que estarán todos los miembros de cada una de las interfaces de dicho componente.
Esta lista simplemente serán punteros o referencias a dichos miembros, sin saber con exactitud a que miembro hace referencia; me explico: si tenemos tres métodos en una interfaz expuesta por el componente, el ejecutable accederá a cada una de ellas de una forma parecida a como se acceden a los miembros de un array: interfaz(1), interfaz(2) e interfaz(3).

En la figura 1 podemos ver la relación entre el código del componente, la interfaz creada por COM y las referencias que hace el código del ejecutable (cliente) a dicha interfaz.
 


Figura 1, relación entre el código, la interfaz y el cliente que usa el componente


Pero si modificamos el código, (supongamos que el orden en el que aparece en las interfaces es por orden alfabético), al crear un nuevo método y éste se inserta entre el segundo y el tercero, el método que originalmente ocupaba la tercera posición en la "lista", ahora ocupará la cuarta posición, por tanto, tendremos una pequeña catástrofe, ya que el cliente no sabrá qué hacer con el nuevo método que le han puesto en la posición de interfaz(3), por la sencilla razón de que él conoce cómo acceder al que, ahora, está en interfaz(4).

En la figura 2, podemos ver cómo queda la relación después de haber modificado el componente. El cliente seguirá apuntando a la mismas posiciones de la interfaz, pero el tercer método de la interfaz, ahora es el método Mostrar y el método que antes estaba en la tercera posición, (Saludar), ha pasado a estar en la cuarta posición, sin embargo el cliente sigue apuntando al mismo sitio de la interfaz, con lo que estaría haciendo referencia al método erróneo.
 


Figura 2, la relación después de modificar el código y la interfaz creada.


Compatibilidad binaria

Este caso, si el componente COM se creara en Visual Basic 6, se solucionaría haciendo que el componente tenga compatibilidad binaria. Esto significa que si cambiamos "la interfaz", (los miembros expuestos por el componente), se creará una nueva y se mantendrá también la anterior.

En el caso anterior, se mantendría una interfaz para los tres métodos originales, de forma que los clientes "antiguos" puedan seguir funcionando, ya que encontrarán cada método en el sitio en el que lo buscarán. Pero también se crearía una nueva interfaz con los nuevos miembros, además de todos los miembros anteriores.
Los nuevos ejecutables (clientes) creados para este componente, utilizarán la nueva interfaz y crearán una lista que les permita saber en qué posición están cada uno de esos miembros.

En la figura 3, podemos ver cómo quedaría esa relación entre el Cliente 1 (el antiguo, líneas negras) y el Cliente 2 (el nuevo, líneas rojas) y su relación con las dos interfaces que ahora contiene el componente COM.
 


Figura 3, la relación entre el código, las interfaces del componente y los clientes.


La propuesta de Visual Studio .NET para crear componentes compatibles con COM


Según nos dice la documentación de Visual Studio .NET, para evitar la falta de "compatibilidad" entre distintas versiones, deberíamos crear explícitamente las interfaces y las clases que queramos exponer a COM deberían implementar esas interfaces. Por supuesto, una vez creada una interfaz, ésta no debería cambiar, es decir si tiene una serie de procedimientos y propiedades declaradas, esas siempre deberían permanecer en la interfaz. Por tanto, si necesitamos quitar métodos o añadir alguno nuevo, deberíamos crear una nueva interfaz para así no "romper" la compatibilidad con los clientes que estén usando el componente original.

Nota:
Te recuerdo que todo esto que estamos viendo, es para poder crear componentes que se puedan usar con las versiones anteriores de Visual Basic, ya que el CLR (Common Language Runtime) controla de forma automática el cambio de versiones.

Si te fijas en la figura 3, lo comprendas mejor, tenemos la Interfaz 1 que expone 3 métodos, cuando se crea el componente COM, se define una interfaz que es expuesta mediante la librería de tipos (el fichero .tlb), esa librería de tipos sería el equivalente al manifiesto que usa .NET Framework para indicar los miembros de la clase.
Cuando creamos un cliente (aplicación de VB6), el cliente sabe que tiene una interfaz con la que poder acceder a los métodos y propiedades de la clase incluida en el componente COM. Si posteriormente necesitamos cambiar el componente, deberíamos definir una nueva interfaz con los miembros que la clase debe exponer, pero de forma que la interfaz creada anteriormente no cambie, por tanto deberíamos seguir manteniendo la Interfaz 1 y crear una nueva, en la cual podemos añadir y/o quitar los miembros que ya no sean necesarios. Por supuesto, si la nueva interfaz elimina algunos de los métodos, esos métodos deberían seguir existiendo, para que los clientes que usen la Interfaz 1 sigan funcionando sin problemas, ya que si no fuese así, se rompería el compromiso con la interfaz implementada originalmente.
Si tu intención no es mantener un compromiso o compatibilidad hacia atrás, entonces, no tienes que hacer nada de todo esto que te estoy explicando... ya que todo esto es para que puedas hacer las modificaciones oportunas a un componente de forma que si tienes aplicaciones distribuidas, no necesites recompilarlos porque lo has cambiado o modificado. Esto te permitirá tener en una misma máquina un mismo componente que sea válido tanto para las aplicaciones anteriores como para las nuevas. Estas últimas se aprovecharán de los nuevos métodos que hayas añadido, pero las anteriores seguirán funcionando como si nada hubiese ocurrido; incluso puedes mejorar el código interno de los procedimientos, sin que ello suponga una ruptura de la "firma" de la interfaz.

Todo esto que te estoy explicando es válido sólo para métodos y propiedades, no para los eventos. Al menos que yo sepa, y después de probar una o dos veces... (realmente unas doscientas veces), si creas un cliente que acepte eventos y después decides añadir nuevos eventos, esa aplicación cliente dejará de funcionar, ya que los procedimientos de eventos son tratados de una forma muy particular y si hay dos eventos definidos en la clase, COM buscará esos dos eventos y si no los encuentra... ¡zas! se acabó lo que se daba...

Pero tampoco es tan drástico, ya que tiene una solución que es la que .NET Framework usa a menudo, y es la de copiar el componente en el mismo directorio del ejecutable, de forma que aunque en la librería de tipos haya defino más de un evento, como en el componente que tenemos en el mismo directorio del ejecutable sólo hay uno, no habrá problemas ni confusiones, por eso es muy importante mantener las interfaces, para que la librería de tipos siempre encuentre en el componente lo que busca.

 

¿Te estoy liando verdad?
No te preocupes, recuerda que no soy nada teórico, por tanto, confío que con los casos prácticos lo entiendas mejor.

 

La práctica:

Como te comentaba, vamos a ver cómo hacer las cosas de forma rápida y, aunque operativa, no recomendada por la documentación de Visual Studio .NET, (realmente por los que la han escrito).
Si te preguntas porqué te lo muestro, es por la sencilla razón de que posiblemente no necesites tanta "compatibilidad de versiones" y no tengas ningún problema en volver a generar el ejecutable cada vez que modifiques el componente creado en .NET.

Aunque si sigues estas sencillas instrucciones, te evitarás "posibles" quebraderos de cabeza:

Si tu intención es hacer las cosas de forma "fácil" y casi automáticas, el componente creado con .NET, (sólo la librería DLL), deberías copiarlo directamente en el mismo directorio del ejecutable (cliente), de esta forma cuando el ejecutable necesite dicho componente, usará el que esté en el mismo directorio del ejecutable. Pero esto sólo funcionará si la interfaz original sigue teniendo los mismos métodos que tenía originalmente, independientemente de que tenga otros nuevos.
Aunque no deberías dar por sentado que siempre funcionará, también es cierto que en las pruebas que he realizado siempre me ha funcionado bien. Pero recuerda que, si Microsoft dice que puede fallar... ¡fallará! Y además, tenemos la famosa Ley de Murphy, la cual dice que si algo no debe fallar, fallará, sobre todo si te es de vital importancia que no falle, así que... casi seguro que fallará, por tanto, si lo previenes, mejor que mejor.

 

En los ejemplos que podría mostrarte, hay uno que siquiera mostraré, aunque te lo voy a comentar, para que sepas porqué.

0- Haciendo las cosas mal.

Podemos crear un componente para usar desde clientes COM (por ejemplo, VB6), sin tener que hacer prácticamente nada de nada, salvo indicarle al Visual Studio .NET que tenemos intención de crear una librería de clases (o componente) que sea compatible con COM o ActiveX, (ahora queda mas "fino" decir COM, eso de ActiveX parece que ya ha quedado algo anticuado). Lo que ocurre es que el cliente, (el ejecutable VB6), usará lo que se llama late-binding o enlace tardío (en tiempo de ejecución), es decir, si definimos una clase basada en el componente y accedemos a una propiedad o método, hasta que no se esté ejecutando, no se comprobará si esa propiedad o método forma parte de la clase. Además de que el Intellisense no nos mostrará los métodos que una clase contiene, ni siquiera el examinador de objetos. Por tanto, yo, me niego a explicarte algo que, yo, no usaría... (espero que con las comas y la negrita se note el énfasis que quiero darle).

 

1- Haciendo las cosas "medio" a lo loco.

Realmente no tanto... aunque en este primer caso vamos a hacer las cosas sin usar interfaces.

Primer ejemplo.

El primer ejemplo que vamos a ver, consistirá en crear una librería de clases en la cual tendremos una clase con los tres métodos indicados en la figura 1. También crearemos un ejecutable realizado con Visual Basic 6.0 que utilice esos métodos. Después modificaremos la clase, para añadir un nuevo método, el cual ya existe en la clase base Object, me refiero al método ToString. Esto no romperá la compatibilidad, ya que ToString es un método que ya estará expuesto por la clase, aunque nosotros no lo definamos. Comprobaremos que el ejecutable seguirá funcionando, además de utilizar la nueva versión de dicho método.

Antes de mostrarte el código, veamos cómo crear una librería de clases que genere un componente compatible y, sobre todo, utilizable desde COM.
He de advertirte que si todas estas pruebas las realizas en el mismo equipo que has utilizado para desarrollo, puede que te resulte "extraño" o te parezca fuera de lugar algunos de los consejos que te daré. Pero si experimentas con otros equipos diferentes al de desarrollo, sabrás que no todo es tan automático como nos lo hace ver el Visual Studio .NET, por tanto creo conveniente que sepas con lo que te vas a encontrar y que sepas cómo solucionarlo.

Primero de todo, vamos a crear un nuevo proyecto de Visual Basic .NET, el cual será una librería (o biblioteca) de clases, ya que este es el único tipo de proyectos que podemos usar en clientes COM. Dejaremos el nombre que nos propone: ClassLibrary1.
 


Figura 4. Un nuevo proyecto para crear un componente COM


Una vez que pulsemos en Aceptar, se añadirá una clase llamada Class1, dejaremos ese mismo nombre.
Seguramente estarás pensando que ahora vamos a añadir una nueva clase del tipo Clase COM, pero no, entre otras cosas, porque no es una buena idea y además sería algo de lo que no me sentiría a gusto explicándote, ya que el tratamiento que se le da a esa clase no es precisamente el correcto.
 


Figura 5. En VB.NET tenemos una clase especial para exponer a COM


Cuando se usa el código indicado en la clase del tipo Clase COM, sólo estarán disponibles los miembros creados en la propia clase, no los que se hayan heredado, por tanto, no veo recomendable su uso.
Aunque, ciertamente, esta sería una forma fácil de crear una clase que exponga eventos, pero aún así, quiero que te compliques un poquitín la vida.
De todas formas, te diré que al añadir una clase de este tipo al proyecto, el Visual Studio hace una serie de asignaciones al proyecto, con lo que nos facilitaría las cosas.
Pero, sinceramente, prefiero que aprendas a hacer las cosas "casi" manualmente, para que lo comprendas mejor y, sobre todo, para que no te lleves pequeñas decepciones o pases malos tragos cuando vayas a llevarle a algún colega lo que has hecho y después resulte que en ese equipo no te funciona.

 

Bueno, para que no te quedes con la duda, vamos a usar tanto la clase COM como el método que antes no quise explicar. Así sabrás y comprobarás porqué no es recomendable.

Añade una clase COM al proyecto, (déjale el nombre que Visual Studio propone), además del código que ha insertado en la clase, el VS.NET hace otra modificación, en este caso en las propiedades del proyecto y es marcarlo para que se registre como un componente COM.
 


Figura 6. Registrar para interoperabilidad COM se marca automáticamente al añadir una Clase COM


Ahora vamos a añadir un método a cada una de las clases, este método al que llamaremos Abrir, simplemente devolverá una cadena, la declaración sería esta:

Public Function Abrir() As String
    Return "método Abrir en Class1"
End Function

Copia este mismo código y pégalo en cada una de las clases, sólo tendrás que cambiar el mensaje, de forma que informe del nombre de la clase en la que está declarado. El código mostrado sería para la clase Class1.

Nota:
Tal como puedes comprobar, al crear la clase ComClass1, se han añadido unas declaraciones de constantes con unos GUIDs que serán los usados para registrar la clase para COM.
También se ha añadido un constructor sin parámetros, esto es "obligatorio" para cualquier clase que se quiera usar desde COM,  aunque no es obligatorio declararlo, ya que el compilador de Visual Basic .NET añadirá uno por nosotros, sólo y exclusivamente si no hay ningún otro constructor declarado, por ejemplo un constructor con parámetros, el cual no se podrá usar desde un cliente COM.

El siguiente paso es generar el proyecto.

Un vez que está generado (y libre de errores), vamos a crear un cliente en Visual Basic 6.0, para ello deberás abrir el VB6 y crear un nuevo proyecto de tipo estándar.

Añade al formulario los controles que se muestran en la figura 7, los nombres de los mismos será el mostrado, (txtAbrir, txtAbrir2, lblInfo), salvo el de las dos etiquetas que están junto a las cajas de textos, cuyos nombres serán los que VB asigne de forma predeterminada, el botón Ejecutar tendrá como nombre cmdEjecutar.
 


Figura 7. El formulario de VB6 en modo diseño.


A continuación tendremos que añadir una referencia al componente ClassLibrary1, para ello, en el menú Proyecto, selecciona la opción Referencias y marca el componente ClassLibrary1. En la figura 8 podemos ver el cuadro de diálogo de añadir referencias.
 


Figura 8. Referencias del proyecto de VB6.


Desde este momento tendremos acceso a las clases expuestas por este componente. Para comprobarlo podemos usar el examinador de objetos (pulsando F2) y seleccionando ClassLibrary1 del primer desplegable.
Tal como se muestra en la figura 9, que nos muestra los miembros de la clase Class1, no se muestra ningún método, pero sabemos que está el que hemos definido: Abrir y también estarán todos los heredados de la clase Object, que es la clase base de Class1.
 


Figura 9. Los miembros de Class1.


Por otro lado, la figura 10 nos muestra los miembros de ComClass1, como podemos comprobar sólo se muestra el método Abrir, pero en este caso ese es el único método al que podemos acceder, aunque la clase ComClass1 esté derivada de Object.
 


Figura 10. Los miembros de ComClass1.


Ahora vamos a añadir código al proyecto de Visual Basic 6.0.
Copia y pega el siguiente código.

'------------------------------------------------------------------------------
' Cliente 1 para el componente ClassLibrary1                         (03/Ene/03)
'
' ©Guillermo 'guille' Som, 2003
'------------------------------------------------------------------------------
Option Explicit

Private clase1 As ClassLibrary1.Class1
Private clase2 As ClassLibrary1.ComClass1

Private Sub cmdEjecutar_Click()
    txtAbrir = clase1.Abrir
    txtAbrir2 = clase2.Abrir
End Sub

Private Sub Form_Load()
    Set clase1 = New ClassLibrary1.Class1
    Set clase2 = New ClassLibrary1.ComClass1
    '
    lblInfo = clase1.ToString
End Sub

Primero de todo, declaramos dos clases para cada uno de las que el componente expone.
En el evento Load creamos (o instanciamos esos dos objetos) y asignamos a lblInfo el valor devuelto por la propiedad ToString del objeto basado en Class1, el cual, como te comenté antes, tiene todos los miembros de la clase Object, además del método Abrir, aunque, el Intellisense de VB6 no nos muestre los miembros de la clase. También quiero que sepas que los miembros de ese objeto se "resuelven" en tiempo de ejecución, es decir, hasta que no se llegue a esa línea de código no se sabrá si el método ToString pertenece a la clase Class1.
En el evento Click del botón Ejecutar se asigna a las cajas de textos el valor devuelto por el método Abrir de cada una de las clases.
En el caso del objeto clase2, al escribir el punto, nos daremos cuenta de que sólo tiene un método: Abrir y si intentamos usar el método ToString, recibiremos un error (en tiempo de ejecución) de que dicho miembro no pertenece a esa clase.

¿Comprendes ahora porqué no es recomendable esta forma de hacer las cosas?

Sólo decirte, (por si no quieres seguir las recomendaciones), que a la clase ComClass1, puedes añadirle los métodos y propiedades que quieras, incluso puedes implementar la propiedad ToString o un evento, los cuales se mostrarán en el examinador de objetos y, por tanto, estarán disponibles para usarlos.
El código a añadir a la clase ComClass1 de Visual Basic .NET, sería el siguiente:

Public Event elEvento(ByVal mensaje As String)

Public Overrides Function ToString() As String
    RaiseEvent elEvento("este es el método ToString de ComClass1")
    Return "El ToString de ComClass1"
End Function

Y para que esto funcione, realiza los cambios en el programa de Visual Basic 6.0, para que ahora acepte eventos y nos muestre lo que la nueva propiedad devuelve:

Private WithEvents clase2 As ClassLibrary1.ComClass1

Private Sub clase2_elEvento(ByVal mensaje As String)
    MsgBox mensaje
End Sub

Private Sub cmdEjecutar_Click()
    txtAbrir = clase1.Abrir
    txtAbrir2 = clase2.Abrir
    '
    lblInfo = clase2.ToString
End Sub

 

Nota:
Para poder crear el nuevo componente, tendrás que cerrar el proyecto de Visual Basic 6.0, si no lo haces, el Visual Studio .NET no podrá compilar el componente, por la sencilla razón de que el VB6 lo está usando.

 

Advertencia:
Si todo esto lo estás haciendo en el equipo de desarrollo, todo funcionará sin problemas.
El propio Visual Studio .NET se encarga de los "pormenores" de registrar la librería para que COM sepa que existe y hará que Visual Basic 6.0 pueda "localizar" el componente creado, (la librería o el ensamblado ClassLibrary1.dll).

Pero si quieres probar el componente creado con VB.NET en otro equipo diferente al de desarrollo, nos encontraremos con estos inconvenientes:
COM no sabrá que existe esa librería de tipos (porque no está registrada en el equipo), además de que Visual Basic 6.0 no tendrá acceso al ensamblado.
Para solucionarlos, tendremos que copiar el ensamblado en el directorio en el que se encuentra el ejecutable de VB6 o en una carpeta incluida en el PATH, por ejemplo el directorio System de Windows; y registrar la librería de clases por medio de la utilidad RegAsm.
Para esto último, abrimos una ventana de consola, nos posicionamos en la carpeta en la que se encuentra el ensamblado y ejecutamos lo siguiente:
regasm ClassLibrary1.dll /tlb
Con esto también creamos la librería de tipos y la registramos, con lo cual estará disponible para usar desde cualquier aplicación que utilice COM.
Ni que decir tiene, que regasm debe estar en una carpeta incluida en la variable de entorno PATH.

 

Ahora vamos a ver cómo habría que hacer las cosas para que las clases creadas muestren todos los miembros que contienen, incluidos los heredados, aunque esta segunda aproximación tampoco es la más recomendada, al menos es menos mala que lo que acabamos de ver.

 

2- Haciendo las cosas un "poco" mejor.

Lo que te voy a explicar ahora tampoco está recomendado en la documentación de Visual Studio .NET, pero, como comprobaremos es mejor que la anterior, además de que nos permite acceder a todos los miembros de la clase, incluso los heredados.

Para que una clase exponga a COM todos los miembros, además de los heredados, podemos usar el atributo:
<ClassInterface(ClassInterfaceType.AutoDual)>
Este atributo lo podemos usar en la declaración de cada clase o bien aplicarlo a todo el ensamblado.
Si queremos que ese atributo se aplique a una clase, lo usaremos de la siguiente forma:

Imports System.Runtime.InteropServices

<ClassInterface(ClassInterfaceType.AutoDual)> _
Public Class Class1

End Class

Para aplicarlo a todo el ensamblado, es decir a todas las clases que tenga el proyecto, definiremos ese atributo en el archivo AssemblyInfo.vb:

<Assembly: ClassInterface(ClassInterfaceType.AutoDual)>

Una vez aclarados estos puntos, vamos a crear un nuevo proyecto del tipo Librería de clases llamado ClassLibrary2, no vamos a añadir ninguna clase del tipo COMClass, por tanto no se marcará la opción de Registrar para interoperabilidad COM, ya que haremos las cosas manualmente.

La declaración de la clase quedaría de la siguiente forma:

Imports System.Runtime.InteropServices

<ClassInterface(ClassInterfaceType.AutoDual)> _
Public Class Class1
    Public Function Abrir() As String
        Return "El método Abrir"
    End Function
    '
    Public Function Cerrar() As String
        Return "El método Cerrar"
    End Function
    '
    Public Function Saludar(ByVal aQuien As String) As String
        Return "Saludo a " & aQuien
    End Function
End Class

Aquí tenemos tres métodos, uno de ellos recibe un parámetro de tipo String.

Para completar el ciclo de prueba, seguiremos estos pasos:
Aunque todos estos pasos los vimos anteriormente, los repito para que no te pierdas.

-Compilamos la librería.
-Abrimos una ventana de consola (DOS), (preferiblemente usando la que se incluye en Inicio>Programas>Microsoft Visual Studio .NET>Visual Studio .NET Tools>Símbolo del sistema de Visual Studio .NET, para que tengamos acceso a las utilidades de VS.NET), y nos posicionamos en el directorio en el que está el ensamblado ClassLibrary2.dll y escribimos: regasm ClassLibrary2.dll /tlb para crear la librería de tipos y registrarla.
-Copiamos el ensamblado en el directorio de Visual Basic 6.0 (por defecto será: C:\Archivos de programa\Microsoft Visual Studio\VB98), esto es para que dicha librería esté accesible al IDE de VB6.
-Creamos un proyecto en Visual Basic 6.0
-Añadimos la referencia a ClassLibrary2
-Diseñamos el formulario (el aspecto será el mostrado en la figura 12)
-Añadimos el código de prueba.

Para comprobar que todo está bien, si pulsamos la tecla F2 para mostrar el examinador de objetos, al seleccionar la clase Class1 de la librería ClassLibrary2, se mostrarán todos los miembros de la clase, incluidos los heredados de Object, tal como podemos ver en la figura 11:
 


Figura 11. El examinador de objetos nos muestra todos los miembros de Class1.

 


Figura 12. El formulario para usar ClassLibrary2 en tiempo de diseño.


Veamos el código del cliente creado con Visual Basic 6.0

'------------------------------------------------------------------------------
' Cliente 1 para el componente ClassLibrary2                         (03/Ene/03)
'
' ©Guillermo 'guille' Som, 2003
'------------------------------------------------------------------------------
Option Explicit

Private clase1 As ClassLibrary2.Class1

Private Sub cmdEjecutar_Click()
    txtAbrir = clase1.Abrir
    txtCerrar = clase1.Cerrar
    txtSaludar = clase1.Saludar(txtQueSaludo)
End Sub

Private Sub Form_Load()
    Set clase1 = New ClassLibrary2.Class1
    '
    txtQueSaludo = "el Guille"
    lblInfo = clase1.ToString
End Sub

Cuando pulsemos en el botón ejecutar, se pasará al método Saludar el contenido de la caja de textos txtQueSaludo, el cual será el que se use para emitir el saludo que se mostrará en txtSaludar.

Como podemos comprobar en la figura 13, el IDE de VB6 nos muestra todos los miembros de la clase Class1:


Figura 13. Intellisense conoce los miembros de la clase Class1.


Ahora nos queda comprobar que funciona bien.
Pulsa F5 y verás que se muestra el nombre de la librería en la etiqueta lblInfo, este es el valor predeterminado de la propiedad ToString.
Escribe algo en la caja de textos txtQueSaludo y pulsa en el botón Ejecutar, se mostrará lo que esperamos que se muestre, por tanto, todo funciona bien.

Compila el ejecutable y vamos a probarlo fuera del entorno de desarrollo.
Seguramente te encontrarás con el error mostrado en la figura 14:


Figura 14. No se encuentra el componente ClassLibrary2.


Esto ocurre porque el ensamblado no está en un sitio accesible por el runtime del .NET Framework.
Para solucionarlo, podemos hacer dos cosas:
1- Copiar el ensamblado en el propio directorio del ejecutable (recomendado)
2- Copiar el ensamblado a un directorio incluido en PATH.
Optaremos por la primera propuesta: copiar el ensamblado en el mismo directorio del ejecutable.

Si vuelves a ejecutarlo, verás que ahora si que funciona.

Ahora vamos a modificar el código de la librería, de forma que ToString muestre lo que nosotros queramos.
Para ello, añade este código a la clase Class1 y vuelve a compilarlo.

Public Overrides Function ToString() As String
    Return "El ToString de Class1 en ClassLibrary2"
End Function

Ejecuta de nuevo el exe creado anteriormente con VB6 y...
¿Qué ha pasado?
Muestra lo mismo.
Esto es así, porque, aunque hayamos compilado nuevamente el ensamblado, se usará el que esté en el mismo directorio del ejecutable, por tanto, copia el nuevo ensamblado en ese directorio y cuando ejecutes el cliente1.exe, se mostrará el nuevo mensaje de ToString.

Fíjate que no hemos necesitado volver a crear la librería de tipos, ya que esta no ha cambiado. Y aunque la hubiésemos "regenerado", seguiría funcionando igual. Esto es así porque las interfaces no han cambiado.

Antes de seguir haciendo pruebas, vamos a hacer una copia del ejecutable y de la librería, para ello, crea una carpeta nueva y copia en ella el EXE creado con VB6 y la DLL creada con VB.NET.

Añade este nuevo método a la clase Class1:

Public Function Mostrar() As String
    Return "El método Mostrar de Class1 en ClassLibrary2"
End Function

Compila nuevamente la librería y copiala en el directorio del ejecutable (no en la carpeta con la copia).
Ejecútalo y verás que te muestra este error:


Figura 15. Error de Automatización.


Esto es así, porque la interfaz ha cambiado.
Y no tiene nada que ver con no haber "regenerado" la librería de tipos, ya que si volvemos a crearla con regasm no lo solucionaríamos.
Este error es por la sencilla razón de que la interfaz ha cambiado, ahora es otra diferente, ya que tiene un nuevo miembro.

Si usamos el ejecutable que copiamos antes, veremos que todo sigue funcionando, ya que al estar también copiada la librería (o ensamblado), COM encuentra la interfaz que espera encontrar.

Para que un nuevo ejecutable pueda usar el nuevo método, y por tanto la nueva interfaz, debemos volver a generarlo.

Nota:
Recuerda que debemos remplazar la librería que anteriormente copiamos en el directorio de VB6.exe.


La única forma de que esa interfaz no se perdiera, sería creando una nueva clase que heredara la primera, con lo cual tendríamos dos interfaces, una para los clientes antiguos y otra para los nuevos. Realmente no sólo tendríamos dos interfaces, sino dos clases diferentes y los nuevos clientes de nuestro componente deberían usar la nueva clase.

Probemos esto que acabo de decir, aunque no podremos volver a recuperar la interfaz original.

Elimina el método Mostrar y vuelve a compilar la librería.
Crea de nuevo la librería de tipos (con regasm) y copia el ensamblado (el DLL) tanto en el directorio de VB6.exe como en el del cliente creado con VB6. Vuelve a generar el ejecutable y pruébalo, ahora funcionará normalmente.

Haz una copia del ejecutable que acabas de generar y cámbiale el nombre para que ahora se llame copia2.exe.

Añade la siguiente clase a la librería de clases:

<ClassInterface(ClassInterfaceType.AutoDual)> _
Public Class Class2
    Inherits Class1
    '
    Public Function Mostrar() As String
        Return "El método Mostrar de Class2 en ClassLibrary2"
    End Function
End Class

Compila el ensamblado.
Regístralo con regasm.
Copia la DLL en el directorio de VB6.exe y en el del ejecutable.
Abre el proyecto y modifícalo para que quede así:

Option Explicit

Private clase1 As ClassLibrary2.Class2

Private Sub cmdEjecutar_Click()
    txtAbrir = clase1.Abrir
    txtCerrar = clase1.Cerrar
    txtSaludar = clase1.Saludar(txtQueSaludo)
End Sub

Private Sub Form_Load()
    Set clase1 = New ClassLibrary2.Class2
    '
    'txtQueSaludo = "el Guille"
    txtQueSaludo = clase1.Mostrar
    lblInfo = clase1.ToString
End Sub

En negrita tienes los cambios realizados al código anterior.

Crea el ejecutable.
Cierra el IDE de VB6 y ejecuta el cliente1.exe

Ahora funcionará bien.

Además, si ejecutas la copia que hicimos antes de generar la librería, (copia2.exe), comprobarás que también funciona, es decir, tanto un ejecutable como otro funcionarán con la nueva librería.

Nota:
El primer ejecutable, (del que hicimos una copia en otra carpeta), no funcionará con esta nueva librería debido a que "rompimos" la firma de la interfaz al añadir y después quitar el método Mostrar de la clase Class1, pero si lo hará con la librería que copiamos en la carpeta.

Si desde el entorno de desarrollo del VB6 mostramos el examinador de objetos, comprobaremos que tenemos dos clases y en la segunda clase (Class2), tenemos además de Mostrar, todos los que Class1 tiene.
Comprueba también que lo que muestra ToString es la versión reemplazada de Class1.
Si quieres que Class2, tenga su propia versión de ToString, añade este método a la clase Class2:

Public Overrides Function ToString() As String
    Return "El ToString de Class2 en ClassLibrary2"
End Function

Vuelve a compilar la librería y copiala en el directorio del ejecutable.
No es necesario que recompiles el ejecutable, ya que funcionará igual que antes, pero el mensaje a mostrar en lblInfo será la nueva cadena que la función ToString devuelve. Lo mismo ocurre con la copia2.exe, funcionará bien, pero mostrará el mensaje que está definido en la clase Class1.
 

Sólo comentarte que usando este método para crear componentes "compatibles" con COM, no podemos añadir eventos a las clases, al menos no para poder usarlos desde VB6.

Comprobemos que esto es así.
Añade esta nueva clase a la librería de clases:

<ClassInterface(ClassInterfaceType.AutoDual)> _
Public Class Class3
    Inherits Class2
    '
    Public Event elEvento(ByVal mensaje As String)
    '
    Public Overrides Function Mostrar() As String
        RaiseEvent elEvento("Hola desde Mostrar3 de la clase Class3")
        Return "El método Mostrar de Class3 en ClassLibrary2"
    End Function
    '
    Public Overrides Function ToString() As String
        Return "El ToString de Class3 en ClassLibrary2"
    End Function
End Class

Como puedes comprobar, además de heredar Class2, (que tiene el método Mostrar), hemos hecho que ese método sobrescriba al de la clase base, para que esto sea posible, la clase Class2 debe implementar ese método como Overridable, por tanto deberás modificar el método Mostrar de Class2 para que quede así:

Public Overridable Function Mostrar() As String
    Return "El método Mostrar de Class2 en ClassLibrary2"
End Function

Compila la librería y, ya sabes, regenera la librería de tipos (con regasm), copia la DLL en los directorios de VB6 y del ejecutable. Renombra el ejecutable actual de cliente1.exe a copia3.exe, abre el IDE de Visual Basic 6.0 y modifica el código para que ahora utilice la clase Class3 en lugar de Class2.
Si el objeto usado lo declaras con WithEvents, al ejecutarlo (e incluso al intentar compilarlo), te dará un error como el mostrado en la figura 16:


Figura 16. La clase Class3 no tiene eventos.


Si abres el examinador de objetos, comprobarás que no hay ningún evento (tienen una especie de rayo de color amarillo), pero si vemos dos nuevos procedimientos: add_elEvento y remove_elEvento, tal como podemos ver en la siguiente figura:


Figura 17. Los miembros de Class3 creados al definir un evento.


La "firma" que tienen esos dos métodos es la típica de los delegados de .NET, pero no se pueden usar desde VB6.
Al menos en las pruebas que he hecho, no he sido capaz de usarlos, ni con AddressOf ni de ninguna de las otras "200" formas que te comenté hace unos párrafos.

Veamos cómo solucionar este inconveniente, esta misma solución nos será de utilidad en la última parte de este artículo.

Para que nuestro componente COM pueda mostrar los eventos de una clase, debemos definir una interfaz a la que debemos aplicar el siguiente atributo:
<InterfaceTypeAttribute(ComInterfaceType.???)>
El valor indicado por las tres interrogaciones dependerá de si quieres que se comprueben los tipos en tiempo de compilación o de ejecución.

Definimos el evento como un procedimiento de tipo Sub, tal como se muestra a continuación:

<InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)> _
Public Interface IClass3Events
    Sub elEvento(ByVal mensaje As String)
End Interface

Lo que ahora nos queda es indicarle a la clase que implementa eventos, que debe usar esa interfaz, esto no se hace mediante Implements, sino aplicando otro atributo a la clase, en este caso lo haremos a la última clase que hemos definido: Class3, la cual quedaría de la siguiente forma:

<ClassInterface(ClassInterfaceType.AutoDual), _
 ComSourceInterfaces("ClassLibrary2.IClass3Events")> _
Public Class Class3
    Inherits Class2
    '
    Public Event elEvento(ByVal mensaje As String)
    '
    Public Overrides Function Mostrar() As String
        RaiseEvent elEvento("Hola desde Mostrar3 de la clase Class3")
        Return "El método Mostrar de Class3 en ClassLibrary2"
    End Function
    '
    Public Overrides Function ToString() As String
        Return "El ToString de Class3 en ClassLibrary2"
    End Function
End Class

Para poder probarlo, tenemos que generar la librería, registrarla, copiarla en el directorio de VB6.exe y también en el del proyecto de VB6.
Abrimos el IDE de VB6, y escribimos el siguiente código, (realmente es el mismo que teníamos antes, pero añadiendo WithEvents a la declaración de la variable y, como es natural, el procedimiento de evento:

Private WithEvents clase1 As ClassLibrary2.Class3

Private Sub clase1_elEvento(ByVal mensaje As String)
    MsgBox mensaje
End Sub

Private Sub cmdEjecutar_Click()
    txtAbrir = clase1.Abrir
    txtCerrar = clase1.Cerrar
    txtSaludar = clase1.Saludar(txtQueSaludo)
End Sub

Private Sub Form_Load()
    Set clase1 = New ClassLibrary2.Class3
    '
    'txtQueSaludo = "el Guille"
    txtQueSaludo = clase1.Mostrar
    lblInfo = clase1.ToString
    '
End Sub

Si compilamos el proyecto, (renombra antes el ejecutable cliente1.exe a copia4.exe), comprobaremos que todo funciona bien, además de que el evento también se ejecuta, en este caso al cargarse el formulario, ya que lo hemos puesto dentro del método Mostrar y este sólo se utiliza en el evento Form_Load.

Si ahora ejecutamos las copias que antes hicimos, veremos que siguen funcionando bien, incluso la copia4, que usaba la clase Class3.
También podemos comprobar que el examinador de objetos nos sigue mostrando los dos procedimientos add_elEvento y remove_elEvento, además del evento, con su rayito amarillo.

 

Como comprobarás, esta aproximación no es tan mala, ya que no rompe la compatibilidad con los clientes anteriores, pero la pega que tiene, es que tenemos que definir una nueva clase cada vez que queramos añadir un nuevo miembro a la librería de clases.

 

3- Haciendo las cosas bien o de la forma recomendada.

Ahora vamos a ver cómo habría que crear la librería de clases de la forma recomendada por Visual Studio .NET.

No te asustes, si compruebas que es un poco, por no decir mucho, más trabajosa que la anterior y el efecto final es prácticamente el mismo, salvo que de esta forma sólo exponemos en COM una sola clase, de forma que el usuario que quiera usar el componente no tenga que "preguntarse" qué clase usar, al ver que hay varias clases disponibles.

Para esta última prueba, vamos a crear un nuevo proyecto del tipo Librería de clases, al que llamaremos ClassLibrary3 y aplicaremos el atributo:
<ClassInterface(ClassInterfaceType.None)>
para evitar que se creen de forma automática las interfaces.
Esto nos obligará a crear explícitamente las interfaces que queramos implementar.

Por tanto, definiremos una interfaz llamada IClass1 con tres métodos de nuestra propia cosecha, además del método ToString, heredado de Object.
Esto es así, porque no se crean las interfaces automáticamente y al tener que definir nosotros mismos las interfaces debemos especificar todos los miembros que queremos que se expongan.

Veamos el código de la interfaz y cómo habría que implementarlo en la clase.

Imports System.Runtime.InteropServices

<Assembly: ClassInterface(ClassInterfaceType.None)>

<InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)> _
Public Interface IClass1
    Function Abrir() As String
    Function Cerrar() As String
    Function Saludar(ByVal aQuien As String) As String
    Function ToString() As String
End Interface

Public Class Class1
    Implements IClass1
    '
    Public Function Abrir() As String _
            Implements IClass1.Abrir
        Return "El método Abrir"
    End Function
    '
    Public Function Cerrar() As String _
            Implements IClass1.Cerrar
        Return "El método Cerrar"
    End Function
    '
    Public Function Saludar(ByVal aQuien As String) As String _
            Implements IClass1.Saludar
        Return "Saludo a " & aQuien
    End Function
    '
    Public Overrides Function ToString() As String _
            Implements IClass1.ToString
        Return "El ToString de Class1 en ClassLibrary3"
    End Function
End Class

Como podemos comprobar, el atributo se ha incluido en el propio archivo donde están definidas las clases, pero con el atributo Assembly, para que tenga una cobertura a todo el ensamblado, esto mismo lo podíamos haber hecho en el archivo AssemblyInfo.vb.

Ahora vamos a compilar la librería de clases y a crear el cliente en Visual Basic 6.0.
Los pasos a dar son los ya conocidos:
Compilar la librería.
Registrarla y crear la librería de tipos.
Copiar el ensamblado a los directorios de VB6.exe y al del proyecto de prueba.

El código del cliente de VB6 es prácticamente el mismo que el usado anteriormente, con la única diferencia de que el objeto lo crearemos a partir de ClassLibrary3.Class1.

Veamos ese código, aunque en las siguientes pruebas puede que sólo se muestre el código que ha cambiado.

'------------------------------------------------------------------------------
' Cliente 1 para el componente ClassLibrary3                        (03/Ene/03)
'
' ©Guillermo 'guille' Som, 2003
'------------------------------------------------------------------------------
Option Explicit

Private clase1 As ClassLibrary3.Class1

Private Sub cmdEjecutar_Click()
    txtAbrir = clase1.Abrir
    txtCerrar = clase1.Cerrar
    txtSaludar = clase1.Saludar(txtQueSaludo)
End Sub

Private Sub Form_Load()
    Set clase1 = New ClassLibrary3.Class1
    '
    txtQueSaludo = "el Guille"
    lblInfo = clase1.ToString
    '
End Sub

Generamos el ejecutable y haremos una copia del mismo, para poder comprobar que sigue funcionando cuando hagamos modificaciones.

Si después de crear el componente nos vemos en la necesidad de añadir nuevos métodos, tendremos que crear una nueva interfaz con los nuevos métodos, además de los anteriores, para que podamos seguir usándolos.

Para ver cómo lo haríamos, vamos a añadir un nuevo método: Mostrar.

La interfaz quedaría de la siguiente forma:

<InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)> _
Public Interface IClass2
    Function Abrir() As String
    Function Cerrar() As String
    Function Saludar(ByVal aQuien As String) As String
    Function ToString() As String
    '
    Function Mostrar() As String
End Interface

Lo que debemos hacer a continuación es implementar esta nueva interfaz en la clase Class1, no necesitamos crear una nueva clase, aunque también podríamos hacerlo de esa forma, pero tendríamos nuevamente varias clases, y eso no sería lo deseado.

Debido a que COM sólo usará una de las interfaces del componente, deberemos implementar la nueva interfaz en primer lugar. Veamos el código de la clase:

Public Class Class1
    Implements IClass2, IClass1
    '
    Public Function Abrir() As String _
            Implements IClass1.Abrir, IClass2.Abrir
        Return "El método Abrir"
    End Function
    '
    Public Function Cerrar() As String _
            Implements IClass1.Cerrar, IClass2.Cerrar
        Return "El método Cerrar"
    End Function
    '
    Public Function Saludar(ByVal aQuien As String) As String _
            Implements IClass1.Saludar, IClass2.Saludar
        Return "Saludo a " & aQuien
    End Function
    '
    Public Overrides Function ToString() As String _
            Implements IClass1.ToString, IClass2.ToString
        Return "El ToString de Class1 en ClassLibrary3"
    End Function
    '
    Public Function Mostrar() As String _
            Implements IClass2.Mostrar
        Return "El método Mostrar"
    End Function
End Class

Como podemos comprobar, los métodos que teníamos antes definidos, implementan las dos interfaces.
Si nuestra intención es la de que algunos de esos métodos no se expongan con la nueva interfaz, simplemente no los implementaríamos, ni los definiríamos en la nueva interfaz.

Si ahora generamos la librería y creamos un cliente para usarla, comprobaremos que el código del cliente no cambia, ya que se sigue usando la misma clase del componente.
Lo único que tendríamos que añadir serían los nuevos métodos que esta nueva versión de la clase contiene, tal como podemos comprobar en el código del evento Form_Load:

Private Sub Form_Load()
    Set clase1 = New ClassLibrary3.Class1
    '
    'txtQueSaludo = "el Guille"
    txtQueSaludo = clase1.Mostrar
    lblInfo = clase1.ToString
    '
End Sub

Si piensas que esto es algo rebuscado... puede que tengas razón, pero si te lo piensas un poco, no es tan descabellado. Además de que tenemos el valor añadido de que si creamos el ensamblado después de haber modificado una de las interfaces, podemos volver a dejar el código como estaba originalmente sin perder la compatibilidad con los clientes que ya tengamos distribuidos.

Para añadir eventos a la clase, podemos hacerlo como vimos anteriormente.
En el código que voy a mostrar, se utiliza la definición de delegados, además de un procedimiento protegido que será el que se encargue de producir el evento.
Esta es la forma recomendada de definir eventos, aunque en Visual Basic .NET podemos crearlos como lo hicimos anteriormente, al viejo estilo de VB6.

Nota:
Esta es una de las características de Visual Basic .NET, aunque pueda parecer una ventaja,  que más desorientan a los recién llegados a este lenguaje, ya que te permite hacer las cosas de dos formas diferentes: al estilo de como se hacía con las versiones anteriores y la otra al estilo de .NET Framework.
Normalmente esto no es un problema, salvo para los que no "sepan" de Visual Basic, a los que les costará un poco más comprender el código que están viendo. Además de que no "obliga" a usar las nuevas formas propuestas por .NET Framework.

Veamos el código de la interfaz para los eventos y el código de la clase que lo implementa, el resto que no se muestra es el mismo que antes.

<InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)> _
Public Interface IClass1Events
    Sub elEvento(ByVal mensaje As String)
End Interface

<ComSourceInterfaces("ClassLibrary3.IClass1Events")> _
Public Class Class1
    Implements IClass2, IClass1
    '
    Public Delegate Sub elEventoHandler(ByVal mensaje As String)
    '
    Public Event elEvento As elEventoHandler
    '
    Protected Sub OnElEvento(ByVal mensaje As String)
        RaiseEvent elEvento(mensaje)
    End Sub
    '
    '... el resto del código ...
    '
    Public Function Mostrar() As String _
            Implements IClass2.Mostrar
        OnElEvento("Evento producido en el método Mostrar")
        Return "El método Mostrar"
    End Function
End Class

Nota:
El nombre del evento expuesto por COM será el que se haya declarado en la interfaz, aunque éste sea diferente del definido en la clase.

El código del cliente de VB6 quedaría así:

Option Explicit

Private WithEvents clase1 As ClassLibrary3.Class1

Private Sub clase1_elEvento(ByVal mensaje As String)
    MsgBox mensaje
End Sub

Private Sub cmdEjecutar_Click()
    txtAbrir = clase1.Abrir
    txtCerrar = clase1.Cerrar
    txtSaludar = clase1.Saludar(txtQueSaludo)
End Sub

Private Sub Form_Load()
    Set clase1 = New ClassLibrary3.Class1
    '
    txtQueSaludo = clase1.Mostrar
    lblInfo = clase1.ToString
    '
End Sub

Como en ocasiones anteriores, te recomiendo que antes de crear el ejecutable hagas una copia, para que puedas comprobar que los ejecutables anteriores siguen funcionando.
También te recuerdo que cada vez que hagas una nueva versión de la clase, sigas los pasos que hemos estado dando anteriormente: registrar el ensamblado, copiarla en los directorios, etc.

Si abres el examinador de objetos, comprobarás que en el componente ClassLibrary3 se muestra el delegado del evento, tal como podemos ver en la figura 18.


Figura 18. El delegado también se muestra en el examinador de objetos.


Si no queremos que esa definición del delegado se muestre en el examinador de objetos, podemos aplicar el atributo <ComVisible(False)>, para que no sea visible en COM. Este atributo lo podemos aplicar a las clases y a los métodos y propiedades.

Para que ese delegado no se muestre, usa este código:

<ComVisible(False)> _
Public Delegate Sub elEventoHandler(ByVal mensaje As String)

Este cambio no romperá la compatibilidad, ya que el delegado realmente no se usa.

Después de aplicar este cambio el examinador de objetos no lo muestra, tal como podemos comprobar en la siguiente figura:


Figura 19. Los miembros de la librería después de ocultar el delegado.


Para finalizar, veamos un último ejemplo en el cual usaremos una de las otras interfaces expuestas por el componente COM.

Como has podido ver, al definir la segunda interfaz e implementarla en primer lugar, la primera interfaz se muestra en el examinador de objetos. Eso nos da la posibilidad de poder aplicarla al objeto creado a partir de Class1.

Como en el código que tenemos actualmente no tendría mucho sentido, vamos a crear una tercera interfaz de la cual se eliminará el método Mostrar y se añadirán nuevos métodos y propiedades, de forma que podamos ver otros aspectos interesantes e incluso curiosos.

La nueva interfaz y la clase quedará de esta forma:

<InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)> _
Public Interface IClass3
    Function Abrir() As String
    Function Cerrar() As String
    Function Saludar(ByVal aQuien As String) As String
    Function ToString() As String
    'Function Mostrar() As String
    Sub Abrir(ByVal archivo As String)
    Sub Abrir(ByVal archivo As String, ByRef aDatos() As String)
    Sub Guardar(ByVal archivo As String)
    Property Datos() As String()
    Property Datos(ByVal index As Integer) As String
End Interface

<ComSourceInterfaces("ClassLibrary3.IClass1Events")> _
Public Class Class1
    Implements IClass3, IClass2, IClass1
    '
    Private mDatos() As String
    '
    <ComVisible(False)> _
    Public Delegate Sub elEventoHandler(ByVal mensaje As String)
    '
    Public Event elEvento As elEventoHandler
    '
    Protected Sub OnElEvento(ByVal mensaje As String)
        RaiseEvent elEvento(mensaje)
    End Sub
    '
    Public Function Abrir() As String _
            Implements IClass1.Abrir, IClass2.Abrir, IClass3.Abrir
        Return "El método Abrir"
    End Function
    '
    Public Function Cerrar() As String _
            Implements IClass1.Cerrar, IClass2.Cerrar, IClass3.Cerrar
        Return "El método Cerrar"
    End Function
    '
    Public Function Saludar(ByVal aQuien As String) As String _
            Implements IClass1.Saludar, IClass2.Saludar, IClass3.Saludar
        Return "Saludo a " & aQuien
    End Function
    '
    Public Overrides Function ToString() As String _
            Implements IClass1.ToString, IClass2.ToString, IClass3.ToString
        Return "El ToString de Class1 en ClassLibrary3"
    End Function
    '
    Public Function Mostrar() As String _
            Implements IClass2.Mostrar
        OnElEvento("Evento producido en el método Mostrar")
        Return "El método Mostrar"
    End Function
    '
    Public Sub Abrir(ByVal archivo As String) Implements IClass3.Abrir
        OnElEvento("El método Abrir con un parámetro")
    End Sub
    '
    Public Sub Guardar(ByVal archivo As String) Implements IClass3.Guardar
        OnElEvento("El método Guardar con parámetros")
    End Sub
    '
    Public Sub Abrir(ByVal archivo As String, ByRef aDatos() As String) _
            Implements IClass3.Abrir
        OnElEvento("El método Abrir con dos parámetros")
        mDatos = aDatos
    End Sub
    '
    Public Property Datos() As String() Implements IClass3.Datos
        Get
            Return mDatos
        End Get
        Set(ByVal value As String())
            mDatos = value
        End Set
    End Property
    Public Property Datos(ByVal index As Integer) As String Implements IClass3.Datos
        Get
            Return mDatos(index)
        End Get
        Set(ByVal value As String)
            mDatos(index) = value
        End Set
    End Property
End Class

En este nuevo código tenemos que destacar un par de cosas:
Hemos declarado nuevos métodos y propiedades, entre los cuales hay varios que están sobrecargados, además de que uno de ellos utiliza un array (o matriz) de tipo String y una de las versiones de la propiedad Datos es del tipo String() (array de String).

Veremos cómo se exponen esos métodos sobrecargados en COM, recordemos que en COM no se pueden usar métodos sobrecargados, es decir procedimientos que tienen el mismo nombre pero distinto número o tipo de parámetros.

Vamos a ver el código de VB6 en el que usaremos estas propiedades, pero antes vamos a ver cómo resuelve ese pequeño inconveniente con el que nos encontramos al no aceptar COM el mismo nombre en distintos procedimientos.
En la siguiente figura vemos los miembros de la clase Class1:


Figura 20. Los procedimientos sobrecargados se cambian de nombre

Como podemos comprobar en la figura 20, lo que se hace es cambiar el nombre de los procedimientos sobrecargados, para que no haya conflictos.
Otro detalle que debemos tener en cuenta es que los arrays o matrices se deben pasar por referencia (ByRef) en lugar de ByVal.

Nota:
Hay que tener en cuenta que en Visual Basic 6.0 los parámetros por defecto, es decir, si no se indica lo contrario, son ByRef, mientras que en Visual Basic .NET éstos son ByVal.

De todas formas, si el array no se declara como ByRef, no se podrá usar en el cliente COM.

Veamos ahora el código de Visual Basic 6.0 que utiliza estos nuevos métodos, para ello tendremos que cambiar el "aspecto" del formulario, para que podamos ver en acción estas nuevas propiedades y métodos.


Figura 21. El nuevo aspecto del formulario.


Y este es el código completo:

Nota:
Es posible que los nombres de los métodos y propiedades que en el componente .NET están sobrecargados, no tengan siempre el mismo nombre, por tanto, si en este código se muestra Datos_2, (por ejemplo), es posible que en el código generado por ti, tenga el nombre Datos, ya que la exportación a COM de miembros sobrecargados no garantiza que siempre tengan el mismo nombre.
La forma de solucionar esta generación automática de nombres es indicando en la interfaz el nombre que queremos que se muestre en COM; doy por supuesto que esos nombres deben ser diferentes para cada una de las sobrecargas.
Esa asignación de nombres de forma manual no afecta a los clientes creados con .NET Framework, ya que siempre usarán los nombres de los métodos declarados en la clase.

 

'------------------------------------------------------------------------------
' Cliente 1 para el componente ClassLibrary3                        (03/Ene/03)
'
' ©Guillermo 'guille' Som, 2003
'------------------------------------------------------------------------------
Option Explicit

Private WithEvents clase1 As ClassLibrary3.Class1

Private Sub clase1_elEvento(ByVal mensaje As String)
    MsgBox mensaje
End Sub

Private Sub cmdEjecutar_Click()
    txtAbrir = clase1.Abrir
    txtCerrar = clase1.Cerrar
    txtSaludar = clase1.Saludar(txtQueSaludo)
End Sub

Private Sub cmdPrueba5_Click()
    Dim o2 As IClass2
    Set o2 = clase1
    lblInfo2 = o2.Mostrar
End Sub

Private Sub cmdPrueba6_Click()
    Dim aD(20) As String
    Dim i As Long
    '
    For i = 0 To 20
        aD(i) = "prueba " & CStr(i)
    Next
    clase1.Abrir_3 "prueba", aD
    '
    For i = 0 To UBound(clase1.Datos)
        List1.AddItem clase1.Datos_2(i)
    Next
    '
    Dim aD2() As String
    aD2 = clase1.Datos
    For i = 0 To UBound(aD2)
        List1.AddItem aD2(i)
    Next
End Sub

Private Sub Form_Load()
    Set clase1 = New ClassLibrary3.Class1
    '
    txtQueSaludo = "el Guille"
    'txtQueSaludo = clase1.Mostrar
    lblInfo = clase1.ToString
    '
End Sub

Como hemos comprobado, hemos eliminado de la interfaz el método Mostrar, por tanto la clase Class1 no muestra ese método, tal como podemos comprobar en la figura 20.
Pero el método mostrar sigue estando definido en la clase, lo único que ocurre es que la interfaz IClass3 no lo implementa. Si por alguna razón quisiéramos utilizar ese método, podemos hacerlo, pero por medio de la interfaz IClass2 que si lo implementa. La forma de usarlo, es el mostrado en el evento cmdPrueba5_Click, que es la forma "clásica" de usar las interfaces que una clase implementa.
En el procedimiento cmdPrueba6_Click podemos ver cómo acceder a las propiedades que manejan el array de tipo String, así como la implementación del método Abrir que recibe como parámetro un array, al cual tenemos que acceder mediante Abrir_3 que es el nombre que utiliza COM.
 

Nota:
En .NET Framework los arrays siempre deben tener el límite inferior de cada dimensión en cero.
Por tanto, (aunque en Visual Basic 6.0 podamos definir una matriz (o array) con un índice inferior distinto de cero), si pasamos a un componente creado en .NET un array que no tenga el índice inferior en cero, se producirá un error.

 

Nota:
Si a las interfaces aplicamos el atributo: <InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)>, la comprobación del tipo de los miembros que expone una interfaz, se hará en tiempo de compilación, por otro lado, aplicando el atributo: <InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIDispatch)>, esa comprobación se hará en tiempo de ejecución.
Por tanto, es recomendable siempre que sea posible, aplicar ese atributo con el valor InterfaceIsIUnknown, salvo cuando sea una colección que devuelva un enumerador, en esos casos habrá que usar InterfaceIsIDispatch tanto en la clase colección como en la clase que dicha colección contendrá.


Sólo me queda decir que si creamos un cliente en .NET Framework, usaremos las propiedades y métodos de la forma "habitual", es decir usando el mismo nombre con el que se ha definido en la librería de clases.

 

Y esto es todo. Se que ha sido algo extenso, pero creo que el tema se lo merecía.

Aunque si te ha parecido extenso, te advierto que aún tengo pendiente una continuación de este artículo en el que se tratará el tema de los componentes compartidos y los nombres seguros (strong name).

Lo que me gustaría es que todo esto realmente te haya sido de utilidad.

 

Nos vemos.
Guillermo

Nerja, del 31 diciembre 2002 al 4 de enero de 2003

Si quieres todo el código de los ejemplos aquí mostrados,
los puedes conseguir en este link: servidorNETparaCOM.zip (115 KB)


la Luna del Guille o... el Guille que está en la Luna... tanto monta...