IMPORTANTE: lo explicado en este artículo es para .NET framework (.NET clásico).
Si quieres usar esto con .NET Core o .NET, debes saber que hasta .NET 5 no se podía hacer. En .NET 5 o posterior hay soporte para hacerlo de la misma manera (en teoría idéntico, pero para ser franco ni lo he probado ni le veo mucho interés). Y con las mejoras en la palabra clave dynamic
en .NET 5 también se pueden consumir objetos COM (en versiones anteriores de .NET existía pero no estaba completamente implementado e iba fatal con COM).
Aunque la plataforma .NET lleva ya más de 16 años entre nosotros, la realidad es que hay muchas ocasiones en las que debemos hacer uso de otro tipo de tecnologías más antiguas y menos potentes, basadas en el vetusto estándar COM (también conocido como ActiveX). Por ejemplo, si queremos programar Office debemos utilizar VBA, para automatizar Windows es muy sencillo utilizar scripts escritos con Windows Scripting Host (que por debajo utiliza VBScript o JScript), y por supuesto, muchos tenemos que mantener todavía aplicaciones escritas con Visual Basic 6 o con ASP Clásico (ASP 3.0).
Creo que a la mayoría de los que llevamos muchos (pero que muchos) años en este mundillo, estas tecnologías antiguas nos siguen gustando mucho a pesar de sus obvias limitaciones. Sí, sin duda eran más limitadas, pero eran mucho más sencillas de aprender y utilizar y con poco trabajo podías hacer muchas cosas. Y si no me crees pregúntale a alguien que haya trabajado con Visual FoxPro: aún hoy en día los hay a miles que beben los vientos por esas herramientas.
El caso es que una gran parte de las limitaciones de estas herramientas tienen que ver con que se quedaron congeladas en el tiempo, sin evolucionar para dar soporte a muchos estándares que aparecieron más adelante, como SOAP (ya en decadencia), JSON, servicios REST y cosas por el estilo. Tampoco permiten hacer ciertas "virguerías" que son muy sencillas en plataformas modernas como .NET: multi-subproceso, async/await
, manejo de datos en memoria... Y por supuesto la gran mayoría de proyectos Open Source modernos quedan fuera de su alcance.
Para estos casos en los que una aplicación antigua debe soportar características modernas, una estupenda opción consiste en escribir la funcionalidad con una herramienta actual y exponerla hacia estas tecnologías antiguas a través de COM/ActiveX.
Sí, COM era lo peor, y si no sabes lo que es el "DLL Hell" suerte para ti que no lo viviste. Pero sigue siendo una solución universal para componentes bajo Windows, y todas las tecnologías antiguas lo soportan, ¡incluso las de hace más de 20 o 25 años!
La web de Microsoft tiene una detallada documentación sobre cómo hacerlo, pero buena suerte para conseguirlo usando esos contenidos. Son liosos, tediosos y sin ningún ejemplo real. Realmente es complicado sacar algo en limpio de ellos. En Google hay mucha información, pero la inmensa mayoría está anticuada o es incompleta, por lo que buena suerte con eso también.
En realidad, conseguirlo es bastante sencillo si se sabe cómo, e implica unos pocos conceptos y pasos a seguir. En este artículo voy a explicar cómo crear un componente COM/ActiveX sencillo a partir de código .NET escrito con C#, y luego veremos cómo utilizarlo apropiadamente.
Vamos a ello...
Abre Visual Studio (yo usaré VS2017) y crea un nuevo proyecto C# de tipo "Biblioteca de clases":
.NET dispone de un espacio de nombres especializado en la compatibilidad con COM y ActiveX llamado InteropServices
.
Por lo tanto, lo primero que debes hacer es añadir en la parte de arriba de tu archivo de clases la línea:
using System.Runtime.InteropServices;
para que reconozca automáticamente todas las clases de este espacio de nombres y las puedas usar con facilidad, sin escribir su nombre completo.
Definir una interfaz
Debido al funcionamiento interno de COM, toda la funcionalidad que vayas a exponer hacia el exterior deberás hacerlo a través de interfaces de programación. Por ello, antes de nada debes pensar bien qué métodos quieres exponer hacia el exterior a través de COM y crear una interfaz que los defina. Por ejemplo, supongamos que quieres exponer un método llamado HacerAlgoComplejo
que no devuelve nada y que toma como parámetros una cadena y un número entero. Deberías definir una interfaz (con el nombre que quieras) de forma análoga a esta:
public interface IMiObjeto
{
void HacerAlgoComplejo(string a, int b);
}
Deberás definir tantas interfaces como objetos quieras exponer desde tu componente final.
Definir la clase correspondiente
Ahora que ya tienes la interfaz con todos los métodos que quieras exponer, debes crear una clase que la implemente. Para ello usarás la sintaxis convencional que consiste en indicar la interfaz a continuación del nombre de clase, separándola con dos puntos (esto es como se hace en C# siempre, no es nada particular de COM).
Es decir, tu clase quedaría similar a:
public class MiObjeto : IMiObjeto
{
public MiObjeto() {}
public void HacerAlgoComplejo(string a, int b)
{
//El código que sea va aquí dentro
}
}
Fíjate en dos cosas:
- He definido explícitamente un constructor sin parámetros, que en realidad no hace nada pues su código está vacío. Esto es necesario porque para instanciar un objeto COM necesitas ese constructor y aparentemente el que crea implícitamente el compilador no nos sirve. No cuesta nada: es una línea.
- Al contrario de lo que verás escrito por ahí en muchos sitios, no es necesario implementar la interfaz explícitamente. En este fragmento yo no lo he hecho. Puedes hacerlo poniendo
IMiObjeto.HacerAlgoComplejo
, pero entonces no le podrás poner el modificador de ámbito public
delante lo cual te dificulta poder usar la misma clase desde .NET también. Con esto la podrás usar en .NET y con COM simultáneamente.
Además deberás tener en cuenta algunas reglas mínimas que debe cumplir tu clase para que sea compatible:
- Deberá ser pública, al igual que todos los miembros que quieras exponer
- No podrá ser abstracta ni estática
- Todos los tipos que se devuelvan o se usen como parámetros deberán ser públicos
- Si la clase es derivada de otra que a su vez es también derivada de otra y así sucesivamente, debes saber que al exponerla a COM todo esto se elimina y queda una única herencia, "aplanada" de las demás.
- Mucho ojo con usar tipos muy especializados, como clases genéricas o cosas específicas de .NET, ya que no se soportarán.
- No podrás usar parámetros opcionales ni valores por defecto
Mientras lo que expongas sea algo "normal" (un mínimo común denominador) no tendrás problemas.
Adornar la interfaz y la clase con atributos
Vale, ahora bien lo importante y específico para COM/ActiveX. Debes indicar al compilador cómo exponer la clase hacia el exterior. Para conseguirlo debes utilizar ciertos atributos especiales que vienen en el espacio de nombres que colocamos en el primer paso.
Generar y asignar GUIDs
Empecemos por la identificación. Todas las clases e interfaces de COM deben identificarse de manera única para evitar conflictos. Para ello se utilian GUIDs, que son identificadores universales. Debes generar uno para cada interfaz y clase y asignárselos.
Para generar un GUID Visual Studio nos proporciona una utilidad llamada newguid.exe
que puedes encontrar en la ruta C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\Tools\
o equivalente según la versión de Visual Studio que utilices.
Nota: en anteriores versiones de Visual Studio disponíamos de acceso directo a esta herramienta desde el menú "Herramientas" del entorno. Ahora ya no. De todos modos es muy fácil de solucionar usando el menú Tools>External Tools...
y agregando ese comando:
A partir de ese momento dispondrás de un acceso directo en el menú de herramientas (este es el mío):
Una vez que lances la herramienta deberás generar al menos dos nuevos GUID diferentes, uno para la interfaz y otro para la clase, usando el tipo 5: "[Gid("xxxx
"
Con la tecla Copy
ya los puedes copiar a Visual Studio listos para usar. Debes asignar uno a la Interfaz y otro a la clase usando el atributo Guid
, así:
[Guid("A8E38597-0E0B-45F3-A264-6E0D9CC49598")]
public interface IMiObjeto
....
[Guid("0E234FAE-F5D2-4DA5-A539-628864F28471}"]
public class MiObjeto : IMiObjeto
....
Vale, ahora que ya tenemos los GUID debemos asignarle el resto de atributos mínimos que necesitamos. Aunque algunos tienen un valor por defecto yo prefiero asignarlos explícitamente para no dejar lugar a dudas sobre lo que estoy haciendo. Serían en concreto estos:
[Guid("A8E38597-0E0B-45F3-A264-6E0D9CC49598")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
[ComVisible(true)]
public interface IMiObjeto
....
[Guid("0E234FAE-F5D2-4DA5-A539-628864F28471")]
[ClassInterface(ClassInterfaceType.None)]
[ComVisible(true)]
[ProgId("MiLib.MiObjeto")]
public class MiObjeto : IMiObjeto
....
En el caso de la interfaz le estamos diciendo que implemente una interfaz dual de modo que puede ser utilizada tanto a través de early binding (mediante IUnknown
añadiendo una referencia antes de usarla, como lo hace VB6 o VBA de Office) como late binding
(que es lo que usan VBScript o ASP clásico, por ejemplo). De todos modos aquí tienes todos los valores posibles, por si te resultaran útiles.
Además le estamos diciendo que esa interfaz debe ser visible para COM. Si no, le ponemos explícitamente este atributo ConVisible
no se exportará y tendremos un problema porque no se verá desde COM.
En el caso de la clase además se usan dos atributos adicionales:
ClassInterface
: que indica el tipo de interfaz de clase que se debe generar para COM. En este caso indicamos el valor None
porque la interfaz necesaria la hemos generado nosotros ya. Los valores posibles y sus explicaciones los tienes en Microsoft Learn.
ProgID
: este es un atributo clave ya que determina la manera en la que vamos a instanciar esta clase desde COM cuando hacemos early binding, es decir, desde un entorno de scripting como VBS o ASP 3.0. Como ves se le pasa una cadena con al menos dos partes separadas por puntos: el nombre de la biblioteca y el nombre de la clase, en nuestro ejemplo: MiLib.MiObjeto
.
Una última cosa que debemos hacer es asignarle identificadores en la interfaz a los diferentes métodos que tengamos (no así a las propiedades) para que se identifiquen de manera única y podamos utilizarlas, para lo cual se usa el atributo DispId
(de "Dispatch Id"). Este atributo toma un número único que puede ser cualquiera. Yo suelo numerarlas desde el 1 en adelante. Con esto nuestro código de ejemplo quedaría:
[Guid("A8E38597-0E0B-45F3-A264-6E0D9CC49598")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
[ComVisible(true)]
public interface IMiObjeto
{
[DispId(1)]
void HacerAlgoComplejo(string a, int b);
}
[Guid("0E234FAE-F5D2-4DA5-A539-628864F28471")]
[ClassInterface(ClassInterfaceType.None)]
[ComVisible(true)]
[ProgId("MiLib.MiObjeto")]
public class MiObjeto : IMiObjeto
{
public MiObjeto() {}
public void HacerAlgoComplejo(string a, int b)
{
//El código que sea va aquí dentro
}
}
dentro del espacio de nombres que le corresponda.
¡Listo! Con esto tenemos ya lo que necesitamos para generar el objeto y podemos compilar nuestra biblioteca. Hazlo y continuamos en un minuto viendo cómo registrarla y usarla. pero antes...
En la propiedades de un proyecto de biblioteca de clases de Visual Studio existe una opción específica llamada de este modo:
Lo que hace esta opción es que se registra automáticamente el componente en el registro de Windows en cada compilación (des-registrándolo antes si había una versión previa. IMPORTANTE: si utilzias esta opción tienes que ejecutar Visual Studio como administrador). Puede ser útil si quieres probarlo desde un script externo (un .vbs
por ejemplo) mientras desarrollas, pero teniendo en cuenta que los tests los harás seguramente en .NET y que el funcionamiento es exactamente el mismo, personalmente creo que no merece la pena en absoluto. A la hora de registrarlo en cualquier otra máquina que no sea la tuya tienes que hacerlo a mano, por lo que mejor que aprendas a hacerlo así de todos modos. Y es lo que vamos a ver ahora mismo.
Registrar el componente para poder usarlo
Para poder utilizar un componente COM/ActiveX es necesario registrarlo en el sistema. No llega con copiarlo y listo, como pasa con .NET. Hoy en día no nos damos cuenta de la gran ventaja que supone, pero en su momento fue algo revolucionario ;-)
Para registrar el componente debemos utilizar una herramienta de línea de comandos que viene con la plataforma .NET, así que la tendrás ya disponible en cualquier máquina donde lo vayas a instalar. No es necesario descargarse nada, ya que .NET viene con el sistema operativo desde hace más de una década (eso sí, actualízalo a la última versión si es la que has utilizado para compilar). La herramienta se llama RegAsm.exe
y la encontrarás en la carpeta C:\Windows\Microsoft.NET\Framework64\v4.0.30319
o similar.
Nota: si tu sistema es de 32bits deberás usar la carpeta C:\Windows\Microsoft.NET\Framework\v4.0.30319
.
Abre una línea de comandos como administrador y escribe:
RegAsm.exe "C:\Utilidades\MiLib.dll" /tlb:MiLib.tlb /codebase
poniendo primero la ruta a donde hayas copiado la DLL que vas a exponer e indicando en el parámetro /tlb
un nombre cualquiera para el archivo .tlb
de definición de interfaces COM que se genera y que es el que se utiliza para registrar la biblioteca.
El parámetro /codebase
se utiliza para indicarle al programa que debe registrar la DLL incluso aunque no esté firmada digitalmente. Se quejará pero lo hará igualmente. El hecho de que te diga ue es mejor firmarla se debe a que si por cualquier motivo debes instalar varias versiones de la misma DLL, tenerla firmada te permite introducirla en la GAC del sistema (la caché global de ensamblados .NET) y facilita la gstión de versiones, algo que ayuda mucho con el "DLL Hell" del que hablábamos al principio. Si tu biblioteca está bajo tu control y no la vas a distribuir como parte de un producto no es necesario. Simplemente desinstálala antes de copiar e instalar una nueva versión y listo. Aquí te dejo instrucciones para firmarla digitalmente y para meterla en la GAC.
Bien, lo anterior habrá registrado correctamente tu ensamblado y podrás utilizarlo desde cualquier entorno compatible con COM/ActiveX, como veremos a continuación.
Para desinstalarlo en el futuro solo debes hacer:
RegAsm.exe /unregister "C:\Utilidades\MiLib.dll"
Usarlo desde VBA o VB6
Para poder usarlo desde estos entornos lo más habitual es agregar una referencia explícita en las propiedades del proyecto (early binding), que es más eficiente. Para ello debemos usar el diálogo de agregar referencia que tienen estos entornos. Vamos a verlo en Office (concretamente yo he usado Word, pero valdría cualquier otra aplicación de la Suite).
Abre el editor de Visual Basic for Applications pulsando ALT+F11
o yendo a la pestaña de "Developer". Ahora vete al menú Tools
y elige la primera opción para gestionar las referencias del proyecto:
En VB6 es idéntico. En el diálogo localiza tu ensamblado en la lista y marca el check que tiene a la izquierda. Ya está listo para usar como cualquier otro objeto. Puedes comprobarlo sacando el examinador de objetos (tecla F2
) y buscando tu clase en la lista. Podrás ver sus métodos y propiedades.
Usarlo desde WSH o ASP Clásico
Desde entornos de scripting es incluso más fácil (aunque menos eficiente) ya que lo único que tienes que hacer es instanciar un nuevo objeto usando CreateObject
. Por ejemplo en WSH crea un nuevo archivo de texto con extensión .vbs y escribe:
Dim miObj
Set miObj = WScript.CreateObject("MiLib.MiObjeto")
miObj.HacerAlgoComplejo "Primer parámetro", 1
WScript.Echo "¡¡La llamada ha funcionado!!"
En ASP clásico sería lo mismo solo que usarías Server.CreateObject
.
Inciso: si quieres aprender a sacarle todo el jugo a .NET nada mejor que empezar con el curso de "Desarrollo con la plataforma .NET y C#" de campusMVP.
En resumen
Mantener aplicaciones antiguas que tienen limitaciones a la hora de utilizar tecnologías modernas es algo que todos, tarde o temprano, nos vemos obligados a hacer. Por suerte cuando Microsoft lanzó .NET en 2001 pensó (por la cuenta que le traía) en cómo facilitar la interoperabilidad entre lo viejo y lo nuevo.
En este largo artículo hemos repasado cómo podemos crear bibliotecas de clases en C# que además de poder ser reutilizadas en .NET podremos exponer también como objetos de tipo COM/ActiveX susceptibles de ser utilizados en lenguajes y entornos antiguos. De este modo podremos traer funcionalidades complejas y/o modernas a nuestras aplicaciones "Legacy", por lo que hemos abierto un mundo de nuevas posibilidades.
Te dejo aquí un ejemplo completo pero sencillo (12Kb, incluye la DLL ya compilada) de cómo utilizar Interop entre .NET y COM.
La mayor parte de la información que existe sobre estas técnicas es antigua y poco actualizada por lo que resulta complicado ponerlo en marcha. Pero en el fondo es muy sencillo si se sabe exactamente cómo proceder, que es lo que hemos visto en este documento.
¡Espero que te resulte útil!