Usar un
componente .NET desde COM (2ª parte) Crear un servidor .NET con nombre seguro para usar desde COM |
Publicado el 13/Ene/2003 Links a los otros artículos de esta serie: 1, 2 y 3 Usar
un formulario de .NET desde VB6 (29/Nov/06) |
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 proyectoLo 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 EnumSigamos 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 InterfaceAhora 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 ClassPara 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 InterfaceEn 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 ClassComo 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 InterfaceVeamos 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 ClassLas 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 InterfaceEsta 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 InterfaceVeamos 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 ClassComo 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.dllUna 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 SubCreo 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 InterfaceLa 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 ClassComo puedes comprobar, hemos implementado las dos interfaces:
Implements IPalabra2, IPalabraDespué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 SubEn 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 SubLo 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.
GuillermoNerja, 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)