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

Usar un componente .NET desde COM (2ª parte)
Crear un servidor .NET con nombre seguro para usar desde COM


Publicado el 13/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)

 

Introducción

Este artículo se puede considerar una continuación de Usar un componente .NET desde COM, entre otras cosas por se titula igual, además de que se indica que es la 2ª parte, por tanto, si la lógica no nos falla, (que en esto de lógica deberíamos saber bastante nosotros los programadores), podemos considerar este como la continuación del anterior, pero, si no lees el anterior y sólo lees este, te será igualmente útil, aunque es posible que algunos conceptos te parezca que no son tratados muy en profundidad, por tanto, puedes leer el anterior y así asegurarte de que no te pierdes nada.

De todas formas, aunque sólo sea por rellenar esta introducción, recapitulemos un poco, así no te sentirás tan perdido.

 

¿Por qué crear un componente COM con .NET Framework?

Antes de nada, voy a excusar (o a defender) el porqué de esta, aparentemente, complicación en crear un componente .NET para usar desde aplicaciones COM (como puede ser el VB6), ya que es posible que pienses que sería más fácil crear el componente con, por ejemplo, Visual Basic 6.0 si nuestra intención es esa: que el componente se utilice en ese lenguaje de automatización.
También puedes pensar que el componente creado con .NET y usado desde COM tendrá una sobrecarga, "aparentemente" innecesaria, ya que, para su funcionamiento tendremos en memoria el runtime del .NET Framework, además de la librería que hace de enlace entre .NET y COM, sin olvidarnos del propio componente, aunque ese no cuenta, ya que, esté creado con uno u otro sistema, tendríamos que tenerlo en la memoria.

Una excusa que se me ocurre, además de decirte que la razón es "porque sí", es la de poder actualizar o mejorar una aplicación existente, creada con Visual Basic 6.0 (por ejemplo).
Supongamos que tenemos una aplicación creada con VB6, la cual, porque tiene bastante código no nos decidimos a convertir a .NET, pero nos interesa aprovechar algunas de las características de .NET, ya sea el uso de ADO .NET, la creación de Threads o simplemente una serie de clases que utilicen la herencia para facilitarnos las cosas. Si has leído el artículo anterior, pensarás que es posible que tenga sus ventajas, pero también hay que escribir código extra para poder llegar a esa compatibilidad con un componente COM, y estarás en lo cierto, pero...nada es gratis, creo que peor sería tener que emplear meses en convertir la aplicación completa a .NET, (no pienses que el asistente de conversión te aliviará el trabajo en un proyecto de gran envergadura), además de que, según que casos, podríamos sacar más rendimiento a un nuevo código si dicho código lo creamos usando .NET Framework.
En fin... si lo dicho no termina de convencerte... puedes dejarlo ahora y emplear tu tiempo en cualquier otro artículo.

 

¿Sigues por aquí? Gracias... al menos se que lo que voy a escribir a continuación no seré yo el único que lo lea. Y para que te animes, si es que simplemente has mirado un poco más abajo, te diré qué es lo que te encontrarás en el resto de este texto:

Crear un componente .NET con compatibilidad binaria con COM

Ese será el contenido, además de que el componente .NET estará instalado en la caché global, de forma que un único ensamblado esté accesible en todo el equipo en el que lo instalemos y, no sólo accesible por ejecutables o librerías COM, sino también por cualquier otro ensamblado creado con cualquier lenguaje basado en .NET Framework.

Nota:
En Visual Basic 6.0 podemos crear componentes COM que pueden ser del tipo DLL también llamados In-Process server o del tipo EXE, Out-Process server. El primero será una librería que se usará desde "dentro" del ejecutable que lo utilice, es decir, aunque la librería sea externa al ejecutable, se cargará en la memoria junto al ejecutable, sin embargo un componente del tipo EXE actuará externamente, es decir, será un ejecutable independiente que funcionará en su propio espacio de memoria. En .NET sólo podemos crear componentes compatibles con COM que sean del tipo DLL.

 

¿Qué importancia tiene la compatibilidad binaria en los componentes COM?

No me voy a extender, ya que no sería este el sitio adecuado, pero al menos te diré, (para refrescarte la memoria), que cuando creamos una aplicación que utiliza un componente ActiveX (o COM), ya sea una librería DLL o un control OCX e incluso un ejecutable EXE; dicha aplicación accede a las interfaces que el componente expone. Si actualizamos el componente ActiveX, para asegurarnos que las aplicaciones que lo utilicen sigan funcionando igual que antes de la actualización, esas interfaces no deben cambiar y en caso de que así sea, se deben preservar las antiguas y añadir nuevas interfaces.
Si en VB6 marcamos un componente con compatibilidad binaria, cada vez que modifiquemos las interfaces expuestas, se creará una nueva con dichos cambios o al menos nos avisará de que lo hagamos, si es que no puede hacerlo de forma automática.
Esto es algo que debemos tener en cuenta al crear un componente en .NET, ya que el compilador no nos avisará de que estamos "rompiendo" la compatibilidad binaria, aunque si lo hacemos, seguramente nos enteraremos bien porque nosotros mismos lo detectemos o bien porque algún cliente nos llame y nos lo diga, normalmente no de una forma "agradable".

Si tenemos aplicaciones que utilicen un componente y hacemos cambios a ese componente, debemos tener la precaución de que no se rompa la compatibilidad con las aplicaciones existentes.

Aquí veremos cómo prevenir ese inconveniente, entre otras cosas, porque el tipo de componente que vamos a crear estará compartido y accesible por cualquier aplicación instalada en el equipo.

Ahora si te recomendaría que te leyeras la primera parte de este artículo, sobre todo lo indicado en la sección titulada "3- Haciendo las cosas bien o de la forma recomendada".

 

Crear un componente .NET para usarlo globalmente.

Independientemente de que el componente creado con .NET se vaya a usar desde un cliente COM o no, podemos registrarlo en el caché de ensamblados global (GAC) de esta forma un único componente (o ensamblado) estará accesible por cualquier aplicación que tengamos en el equipo.
Para que esto sea posible, dicho componente debe estar "firmado" con un nombre seguro (strong name) y registrado en el GAC.

Nota:
Si quieres detalles de cómo firmar un ensamblado con nombre seguro y realizar ese registro en el caché de ensamblados global, te recomiendo que leas el artículo Cómo crear y registrar un ensamblado con nombre seguro.

Cuando registramos el ensamblado (o componente .NET) en el caché de ensamblados global y dicho componente también está registrado para su uso desde clientes COM, tenemos la misma funcionalidad que teníamos con los componentes COM, ya que un componente COM siempre debe estar registrado para poder usarlo, por tanto esta sería la forma más correcta de hacer las cosas para usar componentes .NET desde aplicaciones clientes COM.

 

Y ya sin más preámbulos veamos las características que cualquier componente .NET debería cumplir para que todo esto sea posible y no rompa la compatibilidad binaria con los clientes COM.

Crear un componente .NET con compatibilidad binaria para usar desde COM

El componente que vamos a crear para este artículo será una colección personalizada, ya que así tendremos la ocasión de ver algunas características que nos podían alentar a realizar el componente en .NET.

Este componente tendrá dos clases "normales" y dos clases-colecciones.
Una de las clases tendrá implementada la interfaz IComparable de forma que la podamos usar para que esté contenida en una colección "clasificable".
La otra clase se derivará de esa y añadirá cierta funcionalidad extra.
Una de las colecciones será una colección estándar y la otra colección se derivará de ella, además de añadir funcionalidad para entrada/salida, es decir, permitirá guardar/leer el contenido de la colección en un fichero de texto, además de que producirá eventos.
También tendremos una enumeración y métodos para devolver los "nombres" de los miembros de la enumeración... y algunas cosillas más.

Creando el proyecto de prueba

El proyecto será una librería (o biblioteca) de clases, ya que este es el único tipo de proyecto de .NET que podemos usar desde un cliente COM.
La librería creada para este ejemplo se llamará ClassLibraryVB y el espacio de nombres predeterminado será pruebasGuille, el nombre del ensamblado será pruebasGuille.ClassLibraryVB. Estos valores los podemos asignar en las propiedades del proyecto, tal como se muestra en la figura 1.


Figura 1, propiedades del proyecto

Lo primero que haremos será forzar la creación de interfaces para las clases que esta librería expondrá, ya que esta es la mejor forma de asegurarnos la compatibilidad binaria.
Para no tener que indicarlo en cada una de las clases e interfaces, vamos a añadir el atributo en el fichero AssemblyInfo.vb, de esta forma el "alcance" de este atributo será a todo el ensamblado:
<Assembly: ClassInterface(ClassInterfaceType.None)>

Otro atributo que añadiremos a este fichero será el que indicará que nuestra intención es crearlo con nombre seguro, en este caso el fichero de claves se llamará pruebasGuille.snk, pero puedes usar el que hayas generado con la utilidad sn.exe:
<Assembly: AssemblyKeyFileAttribute("..\..\pruebasGuille.snk")>

Nota:
Según podemos comprobar en un fichero AssemblyInfo.cs usado para crear una librería de clases con C#, la "ubicación" del fichero de claves puede indicarse de forma relativa al directorio obj\<configuración>, (donde <configuración> es Debug o Release), aunque también puede indicarse usando el path completo.

Ya que estamos modificando AssemblyInfo.vb, podemos asignar el resto de propiedades, al menos el que se usará desde COM para identificar el componente, (el nombre que se mostrará en las referencias):
<Assembly: AssemblyDescription("pruebasGuille.ClassLibraryVB - Librería colección de Palabras")>

Debido a que esta librería de clases la estamos creando con Visual Basic .NET, no tenemos que indicar manualmente el identificador (GUID) que se usará para el componente COM, pero si lo creamos con C#, habría que indicar ese ID, el cual podemos generarlo con la utilidad uuidgen.exe o guidgen.exe, aplicando el atributo tal como se hace en AseenblyInfo.vb:
<Assembly: Guid("8C37C5B8-A503-41B2-9281-878C563957D1")>

Nota:
El valor del GUID aquí mostrado seguramente será diferente al que esté en el fichero AssemblyInfo.vb que se ha creado con tu proyecto.

El resto de atributos los dejo a tu gusto.

Definiendo las clases e interfaces del componente.

Como ya te he comentado antes, vamos a crear una interfaz para cada una de las clases que la librería expondrá, esas interfaces tendrán el atributo Dual o IDispatch, si nos decidimos por el primero, no es necesario indicarlo. El atributo que no deberíamos utilizar es el atributo IUnknown, por la sencilla razón de que la clase que tenga ese atributo no se podrá usar en un bucle del tipo For Each.
Además de que en la interfaz que utilizaremos para indicar los eventos debe tener el atributo IDispatch.

Para asegurar la compatibilidad binaria.

Para asegurarnos de que el componente tendrá compatibilidad binaria, debemos tener en cuenta que una vez distribuida la librería, no podemos añadir ni quitar ninguno de los miembros que hayamos definido en las interfaces. Si queremos hacer eso, quitar o añadir miembros a una clase, tendremos que definir otra interfaz y mantener la anterior. De esto veremos un ejemplo en el código mostrado.

Sin embargo con las interfaz que implementa los eventos, debemos ser muy cuidadosos, ya que una vez que hemos definido la interfaz para los eventos, no podemos cambiarla, si lo hiciéramos romperíamos la compatibilidad binaria, con lo cual los clientes anteriores no podrán funcionar, si es que la clase se ha declarado con WithEvents.

En caso de que la clase se haya declarado en la aplicación cliente con WithEvents y cambiemos la interfaz usada para los eventos, (en breve veremos cómo se indica que una interfaz es la que se usará para los eventos), se mostrará un mensaje como el mostrado en la figura 2 y la aplicación finalizará:


Figura 2, error del cliente al cambiar los eventos de la clase

 

Las dos clases que se usarán como elementos de la colección.

Empecemos viendo el código de la clase básica que implementará la interfaz IComparable para que se puedan clasificar los elementos de la colección. A la hora de clasificar se podrá indicar si se clasifican de forma ascendente o descendente, así como si se hará una comparación en la que se tendrá en cuenta la diferencia de mayúsculas y minúsculas.
Ahora veremos la clase y después veremos el "tuco" que vamos a utilizar para que este orden de clasificación, así como si se tendrá en cuenta las letras que compongan el campo a clasificar. El truco realmente es más una comodidad que un truco, ya veremos porqué.

Empecemos por la enumeración para indicar si se clasificará de forma ascendente o descendente:

Public Enum SortOrderTypes As Integer
    Ascendente
    Descendente
End Enum

Sigamos con la interfaz que la clase IDClass implementará:

Public Interface IIDClass
    Property ID() As String
    Function Clone() As IDClass
    Property Descripción() As String
    '
    Function ToString() As String
End Interface

Ahora veamos la clase IDClass, la cual además de implementar esta interfaz, que será la que se exponga en el componente COM, también implementará la interfaz IComparable, el orden en la que se implementarán estas interfaces es importante, ya que COM utilizará como interfaz por defecto la primera que se implemente y nos interesa que sea la que expone los miembros que la clase tendrá.

Public Class IDClass
    Implements IIDClass, IComparable
    '
    Private mID As String
    Private mDescripción As String
    '
    ' para las propiedades compartidas
    Private Shared mIgnoreCase As Boolean = True
    Private Shared mSortOrder As SortOrderTypes = SortOrderTypes.Ascendente
    '
    ' No es necesario ocultarlo a COM,
    ' ya que estas propiedades no están definidas en las interfaces,
    ' pero así, quién lea esto sabrá que no serán visibles.
    ' Estas propiedades públicas se pueden declarar Friend o Public
    ' al cliente COM no afectará esa diferencia.
    <ComVisible(False)> _
    Public Shared Property IgnoreCase() As Boolean
        Get
            Return mIgnoreCase
        End Get
        Set(ByVal value As Boolean)
            mIgnoreCase = value
        End Set
    End Property
    '
    <ComVisible(False)> _
    Public Shared Property SortOrder() As SortOrderTypes
        Get
            Return mSortOrder
        End Get
        Set(ByVal value As SortOrderTypes)
            ' si el valor asignado no está definido en la enumeración,
            ' usar el valor ascendente
            If System.Enum.IsDefined(value.GetType, value) = False Then
                mSortOrder = SortOrderTypes.Ascendente
            Else
                mSortOrder = value
            End If
        End Set
    End Property
    '
    '
    ' El constructor sin parámetros para COM
    Sub New()
        MyBase.New()
    End Sub
    ' desde .NET se podrán usar estos constructores
    Sub New(ByVal elID As String)
        MyBase.New()
        mID = elID
    End Sub
    Sub New(ByVal elID As String, ByVal laDescripción As String)
        Me.New(elID)
        mDescripción = laDescripción
    End Sub
    '
    Public Overridable Function CopareTo(ByVal obj As Object) As Integer _
            Implements IComparable.CompareTo
        Dim objID As String = DirectCast(obj, IDClass).ID
        '
        Dim queOrden As Integer
        If mSortOrder = SortOrder.Ascendente Then
            queOrden = 1
        Else
            queOrden = -1
        End If
        Return String.Compare(mID, objID, mIgnoreCase) * queOrden
    End Function
    '
    Public Overridable Property ID() As String _
            Implements IIDClass.ID
        Get
            Return mID
        End Get
        Set(ByVal value As String)
            mID = value
        End Set
    End Property
    '
    Public Overridable Property Descripción() As String _
            Implements IIDClass.Descripción
        Get
            Return mDescripción
        End Get
        Set(ByVal value As String)
            mDescripción = value
        End Set
    End Property
    '
    Public Overridable Function Clone() As IDClass _
            Implements IIDClass.Clone
        Return DirectCast(Me.MemberwiseClone, IDClass)
    End Function
    '
    Public Overrides Function ToString() As String _
            Implements IIDClass.ToString
        Return mID
    End Function
End Class

Para no entrar en demasiados detalles, veamos las cosas a destacar del código.
En primer lugar vemos la implementación de las interfaces que se usarán en esta clase. Como te comenté anteriormente, primero indicamos la interfaz que se expondrá al cliente COM, es decir la que se usará por defecto.

Nota:
Si una clase implementa más de una interfaz, podremos usar objetos de cualquiera de esas interfaces para acceder al objeto (o a la parte del objeto que la interfaz implementa).

Las propiedades IgnoreCase y SortOrder se han ocultado a COM mediante el atributo <ComVisible(False)>, además de que no se han declarado en la interfaz, por tanto tampoco serían visibles. Pero, no se han declarado en la interfaz por la sencilla razón de que no se pueden declarar miembros compartidos en una interfaz.
La razón de que se hayan declarado compartidas estas propiedades, es porque se usarán desde las clases-colección para indicar esas dos propiedades de forma genérica, para todas las instancias de los objetos de este tipo, de forma que no nos viésemos obligados a asignar estos valores de forma individual a la hora de clasificar los miembros de la colección.
Este era el "truco" al que me refería, dentro de poco veremos cómo aplicarlo.

Debido a que esas dos propiedades están declaradas como compartidas (mediante el atributo Shared), las variables privadas que se usan para mantener el valor, también están declarados como Shared.

Cualquier componente que se vaya a usar desde un cliente COM debe implementar un constructor sin parámetros, si no definimos un constructor para una clase, el compilador de VB.NET creará uno por nosotros, precisamente sin parámetros. Pero si declaramos alguno que reciba parámetros, deberíamos declarar uno sin parámetros de forma explícita, si no lo hiciéramos, no podríamos usar ese componente desde COM. Por tanto, este "pequeño" detalle hay que tenerlo muy presente.

El procedimiento que se utiliza para la interfaz IComparable, deberá devolver un valor adecuado según el elemento indicado en el parámetro sea mayor o menor que el que esa clase implementa, en este caso lo que se comprueba es la propiedad ID, no el objeto completo.
Dependiendo de que se clasifique de forma ascendente o descendente, utilizamos la variable queOrden para que el valor devuelto esté en concordancia con el orden indicado. Esto es así porque el valor devuelto por CompareTo será menor que cero si el valor de ID de la instancia de la clase es menor que la del objeto pasado como parámetro, al menos si se clasifica de forma ascendente, por eso cuando se clasifica de forma descendente el valor de la variable queOrden es -1.
Por otro lado, se utiliza el método Compare del objeto String, para que se pueda indicar si se tendrá o no en cuenta la diferencia entre mayúsculas y minúsculas.

Como habrás notado, los miembros "propios" de la clase, los que hemos definido nosotros, están declarados con Overridable, por si queremos crear una nueva versión en la clase derivada.
Sin embargo, el método ToString lo declaramos Overrides ya que sobrescribe uno con el mismo nombre heredado "implícitamente" de la clase Object.

Para el método Clone, que devuelve una copia de la clase, hemos usado el método MemberwiseClone, ya que esta clase no tiene miembros que hagan referencia a ningún objeto, por tanto la copia devuelta será independiente de la instancia actual, es decir, se devolverá un nuevo objeto, no una referencia al objeto o instancia actual.

 

Veamos ahora la interfaz IPalabra y la clase Palabra, esta clase se derivará de IDClass, pero como comprobaremos, cuando derivamos una clase de otra, los miembros de la clase base no se exponen en COM, al menos directamente, por tanto debemos incluirlos en la definición de la interfaz.

Public Enum TiposPalabra As Integer
    Normal
    Especial
    Super
    ExtraSuper
End Enum
'
'<InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)> _
' si una clase va a formar parte de una colección, debe estar declarada
' como IDispatch o Dual, sino, IEnumerator no funcionará porque no devuelve
' el tipo correcto.
'<InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIDispatch)> 
Public Interface IPalabra
    Property Nombre() As String
    Property Descripción() As String
    Property Tipo() As TiposPalabra
    ReadOnly Property Tipos() As String()
    ReadOnly Property TiposIndex(ByVal index As Integer) As String
    Property Veces() As Integer
    Function Clone() As Palabra
    Function ToString() As String
End Interface

En la interfaz hemos declarado dos propiedades que se podían haber llamado igual, entre otras cosas porque el nombre realmente es el mismo: Tipos y TiposIndex, como veremos en la definición de la clase, se han implementado como propiedades sobrecargadas, pero como COM no permite que haya dos miembros con el mismo nombre y para evitar que se creen esos nombres automáticamente, he optado por darle nombres distintos en la interfaz, ya que COM siempre usará los nombres indicados en la interfaz.
Sin embargo, desde una aplicación .NET se usarán los nombres declarados en la clase.

Public Class Palabra
    Inherits IDClass
    Implements IPalabra
    '
    ' variables privadas para mantener los valores de las propiedades
    Private mTipo As TiposPalabra
    Private mVeces As Integer
    '
    ' COM necesita un constructor sin parámetros
    Sub New()
        mVeces = 0
    End Sub
    ' estos constructores sólo podrán usarse en .NET
    Sub New(ByVal elNombre As String)
        Me.New()
        MyBase.ID = elNombre
    End Sub
    Sub New(ByVal elNombre As String, ByVal laDescripción As String)
        Me.New(elNombre)
        MyBase.Descripción = laDescripción
    End Sub
    '
    ' las propiedades
    Public Overrides Property Descripción() As String _
            Implements IPalabra.Descripción
        Get
            Return MyBase.Descripción
        End Get
        Set(ByVal value As String)
            MyBase.Descripción = value
        End Set
    End Property
    '
    Public Overridable Property Nombre() As String _
            Implements IPalabra.Nombre
        Get
            Return MyBase.ID
        End Get
        Set(ByVal value As String)
            MyBase.ID = value
        End Set
    End Property
    '
    Public Overridable Property Tipo() As TiposPalabra _
            Implements IPalabra.Tipo
        Get
            Return mTipo
        End Get
        Set(ByVal value As TiposPalabra)
            ' si el valor asignado está en la enumeración
            If System.Enum.IsDefined(value.GetType, value) Then
                ' asignar ese valor
                mTipo = value
            Else
                ' en otro caso, usar el valor normal
                mTipo = TiposPalabra.Normal
            End If
        End Set
    End Property
    '
    Public ReadOnly Property Tipos() As String() _
            Implements IPalabra.Tipos
        Get
            Return System.Enum.GetNames(mTipo.GetType)
        End Get
    End Property
    '
    ' En .NET se accederá como Tipos, en COM se accederá como TiposIndex
    Public ReadOnly Property Tipos(ByVal index As Integer) As String _
            Implements IPalabra.TiposIndex
        Get
            Return System.Enum.GetNames(mTipo.GetType)(index)
        End Get
    End Property
    '
    Public Overridable Property Veces() As Integer _
            Implements IPalabra.Veces
        Get
            Return mVeces
        End Get
        Set(ByVal value As Integer)
            mVeces = value
        End Set
    End Property
    '
    ' debe ser Shadows porque no puede remplazar al Clone de la base
    ' ya que no hay parámetros que los diferencie.
    Public Shadows Function Clone() As Palabra _
            Implements IPalabra.Clone
        Return DirectCast(Me.MemberwiseClone, Palabra)
    End Function
    '
    ' los métodos sobrescritos
    ' El método ToString será el método predeterminado en COM
    Public Overrides Function ToString() As String _
            Implements IPalabra.ToString
        Return MyBase.ID
    End Function
    '
    ' los métodos
    ' Protected Friend para que sean Overridable
    ' no serán visibles desde el cliente COM
    Protected Friend Overridable Sub Guardar(ByVal sw As StreamWriter)
        sw.WriteLine(Me.Nombre)
        sw.WriteLine(Me.Descripción)
        sw.WriteLine(Me.Tipo)
        sw.WriteLine(Me.Veces)
    End Sub
    Protected Friend Overridable Sub Leer(ByVal sr As StreamReader)
        Me.Nombre = sr.ReadLine
        Me.Descripción = sr.ReadLine
        Try
            ' usamos CType que es menos restrictivo que DirectCast
            Me.Tipo = CType(Integer.Parse(sr.ReadLine), TiposPalabra)
        Catch
            Me.Tipo = TiposPalabra.Normal
        End Try
        Try
            Me.Veces = Integer.Parse(sr.ReadLine)
        Catch
            Me.Veces = 0
        End Try
    End Sub
End Class

Como puedes comprobar, no se ha implementado la propiedad ID, (en un cliente COM esa propiedad no será visible aunque se haya heredado la clase IDClass), en su lugar se utiliza Nombre, además de que se utiliza MyBase.ID tanto para asignar el nuevo valor como para devolver el valor de esa propiedad, por tanto, si usamos esta clase desde una aplicación .NET, (la cual si verá la propiedad ID heredada de la clase IDClass), el valor devuelto por ID y Nombre será el mismo valor.

La declaración del método Clone se ha declarado con Shadows por la sencilla razón de que no puede declararse con Overrides porque el número de parámetros es el mismo en la clase base y en la clase derivada y sólo cambia el tipo de datos devuelto.

Por último, los métodos Guardar y Leer se han declarado Protected Friend, pero sólo serán visibles dentro del ensamblado y por las clases que se deriven de Palabra. Estos dos métodos no serán accesibles desde el cliente COM.

 

Las clases-colección.

En el componente tenemos dos clases-colección, la primera estará derivada de CollectionBase y la segunda estará derivada de la primera y añadirá dos métodos para leer y guardar el contenido de las palabras contenidas en la colección. Esta segunda colección será la que se usará en los programas de prueba.

Public Interface IPalabras
    Function Add(ByVal unaPalabra As Palabra) As Integer
    Sub Clear()
    Function Clone() As Palabras
    Function Contains(ByVal unaPalabra As Palabra) As Boolean
    ReadOnly Property Count() As Integer
    Sub CopyTo(ByVal unArray As Array, ByVal index As Integer)
    Function IndexOf(ByVal unaPalabra As Palabra) As Integer
    Default Property Item(ByVal index As Integer) As Palabra
    Sub Remove(ByVal unaPalabra As Palabra)
    Sub RemoveAt(ByVal index As Integer)
    Sub Reverse()
    Sub Sort()
    Function Tipos() As String()
    '
    Function GetEnumerator() As IEnumerator
    '
    Property IgnoreCase() As Boolean
    Property SortOrder() As SortOrderTypes
End Interface

Veamos el código de la clase Palabras.

Public Class Palabras
    ' esto parace que no va con las interoperabilidad COM
    Inherits CollectionBase
    Implements IPalabras
    '
    ' COM necesita un constructor sin parámetros,
    ' aunque VB crea automáticamente uno, es preferible definirlo,
    ' así si se añade otro con parámetros, tenemos el que COM necesita.
    Sub New()
        MyBase.New()
    End Sub
    '
    Public Overridable Function Add(ByVal unaPalabra As Palabra) As Integer _
            Implements IPalabras.Add
        Return MyBase.List.Add(unaPalabra)
    End Function
    '
    Public Overridable Shadows Sub Clear() _
            Implements IPalabras.Clear
        MyBase.Clear()
    End Sub
    '
    Public Overridable Function Clone() As Palabras _
            Implements IPalabras.Clone
        Dim tPalabra As Palabra
        Dim tPalabras As New Palabras()
        '
        For Each tPalabra In Me
            tPalabras.Add(tPalabra.Clone)
        Next
        '
        Return tPalabras
    End Function
    '
    Public Overridable Function Contains(ByVal unaPalabra As Palabra) As Boolean _
            Implements IPalabras.Contains
        Return MyBase.List.Contains(unaPalabra)
    End Function
    '
    Public Overridable Shadows ReadOnly Property Count() As Integer _
            Implements IPalabras.Count
        Get
            Return MyBase.Count
        End Get
    End Property
    '
    Public Overridable Sub CopyTo(ByVal unArray As Array, ByVal index As Integer) _
            Implements IPalabras.CopyTo
        MyBase.List.CopyTo(unArray, index)
    End Sub
    '
    Public Overridable Property IgnoreCase() As Boolean _
            Implements IPalabras.IgnoreCase
        Get
            Return IDClass.IgnoreCase
        End Get
        Set(ByVal value As Boolean)
            IDClass.IgnoreCase = value
        End Set
    End Property
    '
    Public Overridable Function IndexOf(ByVal unaPalabra As Palabra) As Integer _
            Implements IPalabras.IndexOf
        Return MyBase.List.IndexOf(unaPalabra)
    End Function
    '
    Default Public Overridable Property Item(ByVal index As Integer) As Palabra _
            Implements IPalabras.Item
        Get
            Return DirectCast(MyBase.List(index), Palabra)
        End Get
        Set(ByVal value As Palabra)
            MyBase.List(index) = value
        End Set
    End Property
    '
    Public Overridable Sub Remove(ByVal unaPalabra As Palabra) _
            Implements IPalabras.Remove
        MyBase.List.Remove(unaPalabra)
    End Sub
    '
    Public Overridable Shadows Sub RemoveAt(ByVal index As Integer) _
            Implements IPalabras.RemoveAt
        MyBase.RemoveAt(index)
    End Sub
    '
    Public Overridable Sub Reverse() _
            Implements IPalabras.Reverse
        MyBase.InnerList.Reverse()
    End Sub
    '
    Public Overridable Sub Sort() _
            Implements IPalabras.Sort
        MyBase.InnerList.Sort()
    End Sub
    '
    Public Overridable Property SortOrder() As SortOrderTypes _
            Implements IPalabras.SortOrder
        Get
            Return IDClass.SortOrder
        End Get
        Set(ByVal value As SortOrderTypes)
            IDClass.SortOrder = value
        End Set
    End Property
    '
    Public Overridable Function Tipos() As String() _
            Implements IPalabras.Tipos
        Return System.Enum.GetNames(GetType(TiposPalabra))
    End Function
    '
    Public Overridable Shadows Function GetEnumerator() As IEnumerator _
            Implements IPalabras.GetEnumerator
        Return MyBase.GetEnumerator
    End Function
End Class

Las propiedades o métodos que se han declarado con Shadows, son miembros que están implementados en la clase base.

El método Clone de esta clase se ha definido de forma diferente a como se ha hecho en las dos anteriores, en lugar de usar MemberwiseClone, he optado por copiar uno a uno los objetos incluidos en la colección, si no se hiciera así, se devolvería una referencia a esos objetos y no una copia independiente. MemberwiseClone funciona bien con tipos por valor, no por referencia.

Para terminar con las clases del componente veremos la clase derivada de Palabras, en la cual, además de los nuevos métodos para guardar y leer, vamos a implementar dos eventos, los cuales se "dispararán" cuando se lea o se guarde cada uno de los objetos de la colección.

Public Interface IPalabrasIO
    '
    ' los miembros heredados
    Function Add(ByVal unaPalabra As Palabra) As Integer
    Sub Clear()
    Function Clone() As Palabras
    Function Contains(ByVal unaPalabra As Palabra) As Boolean
    ReadOnly Property Count() As Integer
    Sub CopyTo(ByVal unArray As Array, ByVal index As Integer)
    Function IndexOf(ByVal unaPalabra As Palabra) As Integer
    Default Property Item(ByVal index As Integer) As Palabra
    Sub Remove(ByVal unaPalabra As Palabra)
    Sub RemoveAt(ByVal index As Integer)
    Sub Reverse()
    Sub Sort()
    Function Tipos() As String()
    '
    Function GetEnumerator() As IEnumerator
    '
    Property IgnoreCase() As Boolean
    Property SortOrder() As SortOrderTypes
    '
    ' los nuevo métodos
    Sub Guardar(ByVal fichero As String)
    Sub Leer(ByVal fichero As String)
    ReadOnly Property Versión() As String
    '
End Interface

Esta es la declaración de la interfaz que se usará para los eventos.

' Este atributo registrará la interfaz con los eventos,
' si no se aplica, el evento se mostrará en el examinador de objetos,
' pero la clase no se podrá declarar con WithEvents.
<InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIDispatch)> _
Public Interface IPalabrasEvents
    Sub Leida(ByVal elNombre As String)
    Sub Guardada(ByVal elNombre As String)
End Interface

Veamos por último la declaración de la clase PalabrasIO que, como he comentado antes, se deriva de Palabras.

' ComSourceInterfaces se utiliza para las clases que produzcan eventos
<ComSourceInterfaces("pruebasGuille.IPalabrasEvents")> _
Public Class PalabrasIO
    Inherits Palabras
    Implements IPalabrasIO
    '
    Private Const cVersion As String = "PalabrasIO v"
    '
    ' los delegados y eventos
    ' Visual Basic permite hacerlo más simple, 
    ' pero esta es la forma recomendada
    <ComVisible(False)> _
    Public Delegate Sub PalabraLeida(ByVal elNombre As String)
    <ComVisible(False)> _
    Public Delegate Sub PalabraGuardada(ByVal elNombre As String)
    '
    Public Event Leida As PalabraLeida
    Public Event Guardada As PalabraGuardada
    '
    '
    Protected Sub OnLeida(ByVal elNombre As String)
        'RaiseEvent Iniciado(elNombre)
        RaiseEvent Leida(elNombre)
    End Sub
    Protected Sub OnGuardada(ByVal elNombre As String)
        RaiseEvent Guardada(elNombre)
    End Sub
    '
    ' El constructor
    Sub New()
        MyBase.New()
    End Sub
    '
    Public Overridable Sub Guardar(ByVal fichero As String) _
            Implements IPalabrasIO.Guardar
        Dim unaPalabra As Palabra
        Dim sw As New StreamWriter(fichero, False, System.Text.Encoding.Default)
        '
        sw.WriteLine(Me.Versión)
        For Each unaPalabra In Me
            unaPalabra.Guardar(sw)
            ' producir un evento con el dato guardado
            OnGuardada(unaPalabra.Nombre)
        Next
        sw.Close()
    End Sub
    '
    Public Overridable Sub Leer(ByVal fichero As String) _
            Implements IPalabrasIO.Leer
        Dim unaPalabra As Palabra
        Dim sr As New StreamReader(fichero, System.Text.Encoding.Default)
        Dim s As String = sr.ReadLine
        ' sólo leer las palabras si es de esta versión
        ' cuando haya nuevas versiones, añadir la comparación correspondiente
        If s.StartsWith(cVersion) Then
            Me.Clear()
            While sr.Peek <> -1
                unaPalabra = New Palabra()
                unaPalabra.Leer(sr)
                Me.Add(unaPalabra)
                ' producir un evento con el dato leído
                OnLeida(unaPalabra.Nombre)
            End While
        End If
        sr.Close()
    End Sub
    '
    Public Overridable ReadOnly Property Versión() As String _
            Implements IPalabrasIO.Versión
        Get
            Return cVersion & "1"
        End Get
    End Property
    '
    ' los miembros de la clase base
    Public Overrides Function Add(ByVal unaPalabra As Palabra) As Integer _
            Implements IPalabrasIO.Add
        Return MyBase.Add(unaPalabra)
    End Function
    '
    Public Overrides Sub Clear() _
            Implements IPalabrasIO.Clear
        MyBase.Clear()
    End Sub
    '
    Public Overrides Function Clone() As Palabras _
            Implements IPalabrasIO.Clone
        Return MyBase.Clone
    End Function
    '
    Public Overrides Function Contains(ByVal unaPalabra As Palabra) As Boolean _
            Implements IPalabrasIO.Contains
        Return MyBase.Contains(unaPalabra)
    End Function
    '
    Public Overrides ReadOnly Property Count() As Integer _
            Implements IPalabrasIO.Count
        Get
            Return MyBase.Count
        End Get
    End Property
    '
    Public Overrides Sub CopyTo(ByVal unArray As Array, ByVal index As Integer) _
            Implements IPalabrasIO.CopyTo
        MyBase.CopyTo(unArray, index)
    End Sub
    '
    Public Overrides Property IgnoreCase() As Boolean _
            Implements IPalabrasIO.IgnoreCase
        Get
            Return IDClass.IgnoreCase
        End Get
        Set(ByVal value As Boolean)
            IDClass.IgnoreCase = value
        End Set
    End Property
    '
    Public Overrides Function IndexOf(ByVal unaPalabra As Palabra) As Integer _
            Implements IPalabrasIO.IndexOf
        Return MyBase.IndexOf(unaPalabra)
    End Function
    '
    Default Public Overrides Property Item(ByVal index As Integer) As Palabra _
            Implements IPalabrasIO.Item
        Get
            Return DirectCast(MyBase.Item(index), Palabra)
        End Get
        Set(ByVal value As Palabra)
            MyBase.Item(index) = value
        End Set
    End Property
    '
    Public Overrides Sub Remove(ByVal unaPalabra As Palabra) _
            Implements IPalabrasIO.Remove
        MyBase.Remove(unaPalabra)
    End Sub
    '
    Public Overrides Sub RemoveAt(ByVal index As Integer) _
            Implements IPalabrasIO.RemoveAt
        MyBase.RemoveAt(index)
    End Sub
    '
    Public Overrides Sub Reverse() _
            Implements IPalabrasIO.Reverse
        MyBase.Reverse()
    End Sub
    '
    Public Overrides Sub Sort() _
            Implements IPalabrasIO.Sort
        MyBase.Sort()
    End Sub
    '
    Public Overrides Property SortOrder() As SortOrderTypes _
            Implements IPalabrasIO.SortOrder
        Get
            Return IDClass.SortOrder
        End Get
        Set(ByVal value As SortOrderTypes)
            IDClass.SortOrder = value
        End Set
    End Property
    '
    Public Overrides Function Tipos() As String() _
            Implements IPalabrasIO.Tipos
        Return MyBase.Tipos
    End Function
    '
    Public Overrides Function GetEnumerator() As IEnumerator _
            Implements IPalabrasIO.GetEnumerator
        Return MyBase.GetEnumerator()
    End Function
End Class

 Como esta clase expone eventos, tenemos que indicarlo con el atributo <ComSourceInterfaces, dentro de las comillas dobles se indicará el nombre de la interfaz usada, en este ejemplo, se supone que el espacio de nombres de la librería es pruebasGuille.

Los eventos producidos por la clase se han declarado usando la forma "recomendada", es decir creando el delegado correspondiente y definiendo los eventos del tipo del delegado correspondiente.
Estos delegados los hemos ocultado al cliente COM, (usando el atributo <ComVisible(False)>), ya que no nos interesa que aparezcan en el examinador de objetos.
Para lanzar los eventos, he seguido las "normas" o recomendaciones de la documentación de Visual Studio .NET, por tanto he definido dos procedimientos protegidos que serán los que se llamen cuando queramos "lanzar" el evento.
Seguramente pensarás que todo sería más fácil haciéndolo al estilo de Visual Basic, es decir declarar sólo los eventos y ya está. Esa es una opción fácil y se puede hacer, pero, aunque no declaremos los delegados, el compilador lo hará por nosotros. La ventaja de hacerlo de esta forma es, entre otras cosas, para que si quieres convertir el proyecto a otro lenguaje de .NET Framework, (por ejemplo C#), te sea más fácil, además de que si el que lee este artículo prefiere trabajar con C# en lugar de VB.NET, le resultará más cómodo.

Fíjate que en esta clase no hemos declarado ningún método o propiedad como Shadows, por la sencilla razón de que estamos derivándola de Palabras y simplemente las sobrescribimos, en el caso de la otra clase (Palabras), teníamos que usar Shadows porque era la única forma que .NET nos permitía para poder sobrescribir los procedimientos de la clase base: CollectionBase.

 

Compilar y registrar la librería.

Una vez que tenemos todo el código de la librería escrito, podemos compilarlo (o generar la DLL). También necesitamos crear la librería de tipos que usará el cliente COM, además de registrarla en el sistema.
Esta librería (o componente) la vamos a registrar en la caché global (GAC), para que tanto las aplicaciones .NET como los clientes COM puedan encontrarla, sin necesidad de hacer una copia local. Si has leído el artículo anterior, esa era la forma de hacer las cosas: copiar la DLL en la misma carpeta (o directorio) del ejecutable que lo usaba, además de que también teníamos que copiarla en el directorio del VB6.exe. Registrándola en el GAC, no tendremos que copiar la librería con el componente ni la librería de tipos, ya que se utilizará la que utilicemos para registrarla en el sistema.

Para registrar el componente y crear la librería de tipos, usaremos la utilidad regasm.exe. Suponiendo que el nombre de la librería es pruebasGuille.ClassLibraryVB.dll, nos posicionaremos en el directorio en el que esté esa librería y escribiremos lo siguiente:
regasm pruebasGuille.ClassLibraryVB.dll /tlb
Con esto creamos la librería de clases y registramos la librería para usar desde COM.

Ahora necesitamos registrarla en el GAC (caché de ensamblados global), de forma que cualquier aplicación pueda hacer referencia a ella. Como te comenté antes, para que un componente se pueda instalar en la caché global, debe estar firmada con nombre seguro. Para registrar la librería en el GAC, usaremos gacutil.exe:
gacutil /i pruebasGuille.ClassLibraryVB.dll

Una vez hecho esto, podremos hacer referencia a la librería desde un proyecto de VB6 o cualquier otro lenguaje que "sepa" cómo manejar las referencias COM.

Nota:
Para tener acceso tanto a la utilidad regasm como a gacutil, deberíamos usar el acceso directo "Símbolo del sistema de Visual Studio .NET", éste acceso directo estará en el menú de Inicio/Programas/... en la carpeta que el VS.NET haya creado en dicho menú de inicio.

 

La aplicación de prueba (el cliente de VB6).

Ahora vamos a ver el cliente de Visual Basic 6.0 que usará esta librería. En el fichero zip con el código, se incluye también un proyecto de Visual Basic .NET que utiliza esta librería.

Empecemos por el diseño del formulario, en la siguiente figura puedes ver el aspecto del mismo en tiempo de diseño:
 


Figura 3, el formulario de VB6 en tiempo de diseño.
 

Los controles usados son:
Un ListView (palabrasLvw) para mostrar los elementos de la colección.
Un CheckBox (ignoreCaseChk) para indicar si distinguiremos las mayúsculas de las minúsculas.
Un ComboBox (sortOrderCbo) para indicar cómo se clasificará el contenido.
Para los datos individuales de cada Palabra, se usarán tres cajas de textos (nombreTxt, descripciónTxt y vecesTxt), un ComboBox (tiposCbo) para saber el "tipo" de palabra y un botón para añadir la palabra a la lista (addBtn).
Un botón para leer el fichero de palabras (leerBtn) y otro para guardarlas (guardarBtn).
Un botón para clasificar las palabras (clasificarBtn), otro para invertir el orden de las palabras (invertirBtn).
El botón con el caption "con CreateObject" nos permitirá crear el objeto usando CreateObject.
Y por último una etiqueta para mostrar información (lblInfo), el botón para cerrar la aplicación (cerrarBtn) y un control de diálogos comunes para seleccionar el fichero de palabras (CommonDialog1).

Nota:
El código del formulario se incluye también en el fichero zip con el código.

Veamos el código usado por el formulario y después iremos siguiendo los pasos indicados, con las modificaciones oportunas, para modificar el código de la clase Palabra y comprobar que no se rompe la compatibilidad con las aplicaciones clientes creadas antes de modificar las interfaces expuestas.

Nota:
Para poder usar el componente creado con .NET Framework, tendremos que añadir una referencia al proyecto de Visual Basic 6.0. El nombre del componente (o la referencia) que tienes que buscar será el indicado en el atributo: AssemblyDescription, si estás usando lo recomendado en el artículo, el nombre será: "pruebasGuille.ClassLibraryVB - Librería colección de Palabras".

 

'------------------------------------------------------------------------------
' Prueba de cliente VB6 para ClassLibraryVB                         (05/Ene/03)
'
' ©Guillermo 'guille' Som, 2003
'------------------------------------------------------------------------------
Option Explicit

' el objeto del componente .NET
Private WithEvents mPalabras As PalabrasIO  'pruebasGuille_ClassLibraryVB.PalabrasIO

Private elGuille As String


Private Sub Form_Load()
    '
    Set mPalabras = New PalabrasIO
    '
    ' El listview se usará para mostrar la información de las palabras
    With palabrasLvw
        .LabelEdit = lvwManual
        .View = lvwReport
        .MultiSelect = True
        .HideSelection = False
        '
        .ColumnHeaders.Add , , "Nombre", 1400
        .ColumnHeaders.Add , , "Descripción", 1400
        .ColumnHeaders.Add , , "Tipo", 600
        .ColumnHeaders.Add , , "Veces", 600, lvwColumnRight
    End With
    '
    elGuille = "©Guillermo 'guille' Som, 2003"
    If Year(Now) > 2003 Then
        elGuille = elGuille & "-" & CStr(Year(Now))
    End If
    lblInfo.Caption = elGuille
    lblInfo.Refresh
    '
    ' Asignar los tipos posibles de cada palabra
    Dim i As Long
    Dim aTipos() As String
    '
    aTipos = mPalabras.Tipos
    For i = 0 To UBound(aTipos)
        tiposCbo.AddItem aTipos(i)
    Next
    tiposCbo.ListIndex = 0
    '
    ' Asignar la forma de clasificación
    sortOrderCbo.AddItem "Ascendente"
    sortOrderCbo.AddItem "Descendente"
    sortOrderCbo.ListIndex = 0
    '
    ' Por defecto se tendrá en cuenta la diferencia de mayúsculas/minçúsculas
    ignoreCaseChk.Value = vbUnchecked
    '
End Sub

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
    palabrasLvw.ListItems.Clear
    mPalabras.Clear
End Sub

Private Sub Form_Unload(Cancel As Integer)
    Set mPalabras = Nothing
End Sub


Private Sub lblInfo_DblClick()
    '
    ' Para probar un método que después eliminaremos:
    ' Sólo deberíamos pulsar en la etiqueta,
    ' cuando haya aunque sea una palabra en la colección
    lblInfo = mPalabras(0).TiposIndex(1)
    '
End Sub


Private Sub addBtn_Click()
    ' añadir la palabra a la lista
    Dim tPalabra As Palabra
    Dim i As Long
    '
    Set tPalabra = New Palabra
    '
    tPalabra.Nombre = nombreTxt.Text
    tPalabra.Descripción = descripciónTxt.Text
    tPalabra.Tipo = tiposCbo.ListIndex
    tPalabra.Veces = CInt(vecesTxt.Text)
    i = mPalabras.Add(tPalabra)
    With palabrasLvw.ListItems.Add(, , tPalabra.Nombre)
        .SubItems(1) = tPalabra.Descripción
        .SubItems(2) = tPalabra.Tipo
        .SubItems(3) = tPalabra.Veces
        .Tag = i
    End With
End Sub

Private Sub cerrarBtn_Click()
    Unload Me
End Sub

Private Sub clasificarBtn_Click()
    ' clasificar usando el objeto COM
    mPalabras.SortOrder = sortOrderCbo.ListIndex
    mPalabras.IgnoreCase = (ignoreCaseChk.Value = vbUnchecked)
    mPalabras.Sort
    palabras2Lista
End Sub

Private Sub cmdConCreateObject_Click()
    ' para crear una instancia de la clase usando CreateObject
    ' esto sólo es recomendable en lenguajes Script.
    Set mPalabras = CreateObject("pruebasGuille.PalabrasIO")
End Sub

Private Sub guardarBtn_Click()
    ' guardar en un fichero
    On Local Error Resume Next
    '
    With CommonDialog1
        .DialogTitle = "Guardar las palabras"
        .Filter = "Ficheros de texto (*.txt)|*.txt|Todos los ficheros (*.*)|*.*"
        .CancelError = True
        .ShowSave
        If Err = 0 Then
            '
            mPalabras.Guardar .FileName
            '
            lblInfo.Caption = elGuille
        End If
    End With
End Sub

Private Sub invertirBtn_Click()
    mPalabras.Reverse
    palabras2Lista
End Sub

Private Sub leerBtn_Click()
    ' leer un fichero de palabras
    On Local Error Resume Next
    '
    With CommonDialog1
        .DialogTitle = "Leer las palabras"
        .Filter = "Ficheros de texto (*.txt)|*.txt|Todos los ficheros (*.*)|*.*"
        .CancelError = True
        .ShowOpen
        If Err = 0 Then
            mPalabras.Clear
            mPalabras.Leer .FileName
            '
            palabras2Lista
            '
            'lblInfo.Caption = elGuille
        End If
    End With
End Sub


' Los eventos producidos por el componente
Private Sub mPalabras_Guardada(ByVal elNombre As String)
    lblInfo.Caption = "Guardando " & elNombre
    lblInfo.Refresh
End Sub

Private Sub mPalabras_Leida(ByVal elNombre As String)
    lblInfo.Caption = "Leyendo " & elNombre
    lblInfo.Refresh
End Sub


Private Sub palabrasLvw_ItemClick(ByVal Item As ComctlLib.ListItem)
    With Item
        nombreTxt.Text = .Text
        descripciónTxt.Text = .SubItems(1)
        tiposCbo.ListIndex = CInt(.SubItems(2))
        vecesTxt.Text = .SubItems(3)
    End With
End Sub

Private Sub palabrasLvw_KeyUp(KeyCode As Integer, Shift As Integer)
    If KeyCode = vbKeyDelete Then
        ' eliminar los seleccionados
        Dim i As Long
        '
        For i = palabrasLvw.ListItems.Count To 1 Step -1
            If palabrasLvw.ListItems(i).Selected Then
                ' se usará el valor del Tag para referenciar al elemento de la clase
                mPalabras.RemoveAt palabrasLvw.ListItems(i).Tag
                palabrasLvw.ListItems.Remove i
            End If
        Next
    End If
End Sub


Private Sub palabras2Lista()
    ' pasar las palabras al listview
    Dim tPalabra As Palabra
    Dim i As Long
    '
    i = -1
    palabrasLvw.ListItems.Clear
    For Each tPalabra In mPalabras
        With palabrasLvw.ListItems.Add(, , tPalabra.Nombre)
            .SubItems(1) = tPalabra.Descripción
            .SubItems(2) = tPalabra.Tipo
            .SubItems(3) = tPalabra.Veces
            i = i + 1
            .Tag = i
        End With
    Next
    '
End Sub

Creo que lo único a destacar o aclarar es la asignación a la propiedad Tag del elemento añadido al ListView, esa asignación se hace en el procedimiento palabras2Lista y también en el evento Click del botón añadir.
Cuando añadimos un nuevo elemento a la colección, el método Add devuelve el índice o posición en la que se ha guardado ese elemento dentro de la colección, utilizamos ese valor para "saber" dónde está, de esta forma, cuando eliminamos elementos de la colección (al eliminarlos del ListView), sabremos en qué posición exacta está.
En el procedimiento palabras2Lista he usado un bucle For Each, ya que de esta forma comprobaba mejor si la función GetEnumerator funcionaba al hacer cambios en las interfaces de las clases expuestas por la librería, pero para este ejemplo, en el que se utiliza la posición del elemento dentro de la colección para guardarlo en la propiedad Tag del elemento del ListView, hubiese sido más recomendable utilizar un bucle For normal y corriente, al estilo de:
For i = 0 To mPalabras.Count - 1
y quitar el incremento de la variable i dentro del bucle (i = i + 1)

Una vez que tengamos todo el código escrito, podemos comprobar que todo funciona bien. Añade algunas palabras, clasifícalas, invierte el orden, guárdalas, etc.
Una vez que has comprobado que funciona bien. Compila el proyecto para crear el ejecutable. Prueba el ejecutable desde fuera del IDE de VB6 y haz una copia del mismo, al que llamaremos copia1.exe, de esta forma, después de modificar el código del componente, podremos comprobar que sigue funcionando igual.

Nota:
Cada vez que modifiques el componente .NET, tendrás que crear la librería de tipos usando la utilidad regasm. Esto no puedes hacerlo si tienes el IDE de Visual Basic 6.0 abierto, ya que el VB la estará usando, por tanto, antes de volver a registrar la librería, tendrás que cerrar el IDE de Visual Basic 6.0

 

Modificar el componente creado con .NET Framework.

Si después de crear el componente y haberlo distribuido junto con alguna aplicación de VB6, decides añadir alguna nueva propiedad o método e incluso si decides quitar o cambiar el nombre de las existentes, habrá que seguir unas "pequeñas" reglas para no romper la compatibilidad con las aplicaciones que ya estén distribuidas. Con las instrucciones que indicaré, podrás tener el nuevo componente funcionando tanto con aplicaciones antiguas como nuevas, es decir todas las aplicaciones cliente funcionarán sin problemas, al menos ¡eso espero!

Todas las indicaciones que voy a dar a continuación son para clientes COM, ya que en los clientes .NET no tendremos prácticamente ninguna complicación.

Lo primero que debemos saber es que, una vez definidos los eventos que producirá el componente, estos no pueden modificarse, ni añadir nuevos ni quitar uno existente, ya que esto rompería la compatibilidad hacia atrás, al menos con las aplicaciones que declaren la clase con WithEvents para recibir eventos. Si la aplicación cliente no utiliza los eventos, no habrá problemas.

Si decidimos añadir alguna nueva propiedad (o método) a alguna de las clases, tendremos que crear una nueva interfaz y mantener la anterior. Esa nueva interfaz podrá contener nuevos miembros así como eliminar o cambiar de nombre algunos de los miembros anteriores.

Nota:
Si cambiamos el comportamiento interno de cualquiera de los procedimientos, esto no cambiará las interfaces expuestas por el componente, por tanto, podemos modificar libremente el código de cualquier propiedad o método sin temor a que se pierda la compatibilidad con las aplicaciones distribuidas anteriormente.

Para ver en la práctica este último caso, (que será el único que se nos debería dar, ya que no podemos, al menos yo no sé cómo solucionarlo, cambiar los eventos producidos por las clases), vamos a añadir un nuevo método a la clase Palabra y vamos a quitar uno de los existentes.

El método que vamos a añadir se llamará Mostrar y será una función que devuelva una cadena. El método que quitaremos de la interfaz será TiposIndex.
Como te he comentado, hay que crear una nueva interfaz y mantener la anterior, por tanto vamos a definir esa interfaz, a la que llamaremos IPalabra2. Esa interfaz habrá que implementarla junto con la otra en la clase Palabra. El consejo, tal como veremos, es implementar primero la nueva interfaz, con idea de que sea la que COM utilice como predeterminada. Cuando los clientes anteriores utilicen la librería seguirán usando la otra interfaz, ya que no sabrán nada de la antigua.
Incluso con los nuevos clientes podremos seguir usando la interfaz original, si es que necesitamos acceder a algún miembro que la nueva no implemente. Un ejemplo de esto último lo tienes en la primera parte de esta "serie" de artículos.

Public Interface IPalabra2
    Property Nombre() As String
    Property Descripción() As String
    Property Tipo() As TiposPalabra
    ReadOnly Property Tipos() As String()
    Property Veces() As Integer
    '
    Function Clone() As Palabra
    '
    Function ToString() As String
    '
    Function Mostrar() As String
End Interface

La nueva definición de la clase Palabra quedará como sigue:

Public Class Palabra
    Inherits IDClass
    Implements IPalabra2, IPalabra
    '
    ' variables privadas para mantener los valores de las propiedades
    Private mTipo As TiposPalabra
    Private mVeces As Integer
    '
    ' COM necesita un constructor sin parámetros
    Sub New()
        mVeces = 0
    End Sub
    ' estos constructores sólo podrán usarse en .NET
    Sub New(ByVal elNombre As String)
        Me.New()
        MyBase.ID = elNombre
    End Sub
    Sub New(ByVal elNombre As String, ByVal laDescripción As String)
        Me.New(elNombre)
        MyBase.Descripción = laDescripción
    End Sub
    '
    ' las propiedades
    Public Overrides Property Descripción() As String _
            Implements IPalabra.Descripción, IPalabra2.Descripción
        Get
            Return MyBase.Descripción
        End Get
        Set(ByVal value As String)
            MyBase.Descripción = value
        End Set
    End Property
    '
    Public Overridable Property Nombre() As String _
            Implements IPalabra.Nombre, IPalabra2.Nombre
        Get
            Return MyBase.ID
        End Get
        Set(ByVal value As String)
            MyBase.ID = value
        End Set
    End Property
    '
    Public Overridable Property Tipo() As TiposPalabra _
            Implements IPalabra.Tipo, IPalabra2.Tipo
        Get
            Return mTipo
        End Get
        Set(ByVal value As TiposPalabra)
            ' si el valor asignado está en la enumeración
            If System.Enum.IsDefined(value.GetType, value) Then
                ' asignar ese valor
                mTipo = value
            Else
                ' en otro caso, usar el valor normal
                mTipo = TiposPalabra.Normal
            End If
        End Set
    End Property
    '
    Public ReadOnly Property Tipos() As String() _
            Implements IPalabra.Tipos, IPalabra2.Tipos
        Get
            Return System.Enum.GetNames(mTipo.GetType)
        End Get
    End Property
    '
    ' En .NET se accederá como Tipos, en COM se accederá como TiposIndex
    Public ReadOnly Property Tipos(ByVal index As Integer) As String _
            Implements IPalabra.TiposIndex
        Get
            Return System.Enum.GetNames(mTipo.GetType)(index)
        End Get
    End Property
    '
    Public Overridable Property Veces() As Integer _
            Implements IPalabra.Veces, IPalabra2.Veces
        Get
            Return mVeces
        End Get
        Set(ByVal value As Integer)
            mVeces = value
        End Set
    End Property
    '
    ' debe ser Shadows porque no puede remplazar al Clone de la base
    ' ya que no hay parámetros que los diferencie.
    Public Shadows Function Clone() As Palabra _
            Implements IPalabra.Clone, IPalabra2.Clone
        Return DirectCast(Me.MemberwiseClone, Palabra)
    End Function
    '
    ' los métodos sobrescritos
    ' El método ToString será el método predeterminado en COM
    Public Overrides Function ToString() As String _
            Implements IPalabra.ToString, IPalabra2.ToString
        Return MyBase.ID
    End Function
    '
    Public Overridable Function Mostrar() As String _
            Implements IPalabra2.Mostrar
        Return MyBase.ID & " " & MyBase.Descripción '& " " & MyBase.IgnoreCase.ToString
    End Function
    '
    ' los métodos
    ' Protected Friend para que sean Overridable
    ' no serán visibles desde el cliente COM
    Protected Friend Overridable Sub Guardar(ByVal sw As StreamWriter)
        sw.WriteLine(Me.Nombre)
        sw.WriteLine(Me.Descripción)
        sw.WriteLine(Me.Tipo)
        sw.WriteLine(Me.Veces)
    End Sub
    Protected Friend Overridable Sub Leer(ByVal sr As StreamReader)
        Me.Nombre = sr.ReadLine
        Me.Descripción = sr.ReadLine
        Try
            ' usamos CType que es menos restrictivo que DirectCast
            Me.Tipo = CType(Integer.Parse(sr.ReadLine), TiposPalabra)
        Catch
            Me.Tipo = TiposPalabra.Normal
        End Try
        Try
            Me.Veces = Integer.Parse(sr.ReadLine)
        Catch
            Me.Veces = 0
        End Try
    End Sub
End Class

Como puedes comprobar, hemos implementado las dos interfaces:
Implements IPalabra2, IPalabra

Después aplicamos Implements IPalabra2 y el método o la propiedad que corresponda a los mismos procedimientos que ya teníamos, ya que eso es precisamente lo que queremos hacer, entre otras cosas para no tener que volver a escribir el código.
En el caso de la propiedad Tipos que recibe un parámetro de tipo Integer, el cual está implementado como el miembro TiposIndex de IPalabra, no implementamos nada de la nueva interfaz, por tanto no estará accesible a los nuevos clientes, al menos si acceden a la clase Palabra. Sin embargo los clientes anteriores que lo utilicen seguirán teniendo acceso a esa propiedad.

En el código del proyecto de VB6 accedemos al método TiposIndex en el evento DblClick de lblInfo, cuando creemos el componente con los cambios indicados no tendremos acceso a esa propiedad, ya que la clase Palabra no lo implementa, pero no está todo perdido, podemos utilizar un objeto del tipo IPalabra (la interfaz anterior) para poder seguir accediendo a esa propiedad.

Veamos el código anterior y el nuevo:

Private Sub lblInfo_DblClick()
    lblInfo = mPalabras(0).TiposIndex(1)
End Sub

En el código anterior, podemos acceder a TiposIndex(1) porque forma parte de la clase Palabra (antes de la modificación), pero no después de la modificación indicada.
Veamos el código que tendríamos que usar en los nuevos clientes de nuestro componente modificado:

Private Sub lblInfo_DblClick()
    ' en la nueva versión del componente esa propiedad no existe
    ' pero podemos acceder mediante la interfaz antigua
    Dim oAnt As IPalabra
    Set oAnt = mPalabras(0)
    lblInfo = oAnt.TiposIndex(1)
End Sub

Lo que aquí hacemos es "tomar" la parte del objeto representado por mPalabras(0) que implementa la interfaz IPalabra y desde el objeto del tipo de esa interfaz llamamos a la propiedad.

Para probar el nuevo método Mostrar, puedes hacerlo, por ejemplo, en el procedimiento palabras2Lista, dentro del bucle o en cualquier otro sitio, simplemente para que compruebes que es accesible.

Compila nuevamente el proyecto con estos cambios y si ejecutas tanto este nuevo ejecutable como el anterior (copia1.exe), verás que ambos siguen funcionando.

 

Sólo me queda decirte que cada vez que hagas cambios en el código de la librería y la vuelvas a generar, independientemente de que cambies o no las interfaces, es conveniente que vuelvas a registrar la librería e instalarla en el GAC, usando para estas tareas los comandos mostrados hace unos cuantos párrafos.

 

Espero que haya quedado bien claro todo esto de crear un componente .NET para usar desde un cliente COM. Por supuesto, todas estas "complicaciones" no son necesarias si tu intención es usar el componente sólo y exclusivamente desde aplicaciones creadas con lenguajes .NET.

 

Nos vemos.
Guillermo

Nerja, a 13 de enero de 2003

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


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