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

Usar un componente .NET desde COM (3ª parte)
Añadir nuevos eventos sin perder la compatibilidad binaria


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

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

Nota del 19/Oct/2004:
Si bien todo lo dicho en este artículo es válido, con lo que aquí se explica no nos permite añadir nuevos eventos sin perder la compatibilidad binaria, al menos sin cambiar los números de versiones de los ensamblados, ya que al cambiar la versión y mantener copias de cada uno de ellos en el GAC (caché de ensamblados global) funcionaba bien, (al menos a mi me funcionaba cuando hice todas estas pruebas), pero si sólo queremos mantener una versión de la librería y no queremos romper la compatibilidad binaria con VB6 (el componente COM), tendremos que hacerlo de otra forma, la cual publicaré en breve y si eres asiduo de los grupos de noticias públicos de Microsoft ya tendrás la respuesta.

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

 

Introducción

Pues fíjate, no ha pasado ni un día para que haya una tercera parte de esta serie... y si no llega a ser porque convertí el código, (de la librería .NET para usar desde un cliente COM), a C#, lo mismo hubiese tardado algo más.

El porqué de esta tercera parte es para explicarte cómo añadir nuevos eventos a un componente (para usar desde COM) previamente distribuido.

En las pruebas que hice anteriormente al añadir un nuevo evento, los clientes antiguos dejaban de funcionar. La razón, según creo, es porque el componente COM utilizaba los nuevos eventos, pero si el cliente, (al no conocer previamente de su existencia), no los implementaba, daba error, entre otras cosas porque el componente COM lo "intentaba" lanzar, pero al no estar capturado (o definido un procedimiento de captura del evento), se producía una excepción. Dicha excepción, según he podido comprobar, se produce en la librería, no en la aplicación cliente.

Por suerte, (si es que se puede llamar suerte), al convertir el código de Visual Basic .NET a C#, pude comprobar que si un cliente no implementaba los eventos, se producía una excepción. La suerte o casualidad fue probarlo con el cliente creado en .NET (al que por descuido u olvido) no había añadido los eventos de la colección, como sabrás en VB la declaración de variables que interceptarán eventos es muy simple, ya que sólo hay que añadir la instrucción WithEvents en la declaración y asunto arreglado, pero en C# hay que "crear" (o indicar) explícitamente el evento.

Como te comentaba, el error se producía en el componente, y para que funcionara, (después de ver un ejemplo en la ayuda de VS), había que comprobar si era null y en caso de que no fuese, se "lanzaba" el evento. En Visual Basic esto no se puede hacer, (en breve veremos el código de C# y VB.NET), por tanto, añadí un Try... Catch y ¡Oh Dioses! funcionaba. Funcionaba incluso con los ejecutables que había compilado anteriormente, que al fin y al cabo era de lo que se trataba... sino, ¿para que dar gracias a los Dioses de la Programación?

Bueno, ya está bien de tanta historia y exclamaciones.
Veamos el código que he usado tanto en VB como en C#, aunque en este último lenguaje, no veremos el código completo, sólo veremos la clase PalabrasIO y las declaraciones de las interfaces, tanto la de los eventos como la propia interfaz implementada por la clase. El resto del código no cambia, salvo en lo concerniente a la forma de trabajar de C# y VB, pero ese código lo puedes conseguir al completo, así como el del cliente creado en VB6, en el fichero zip que está al final.

Nota:
Si bien se soluciona el problema de los eventos en los clientes antiguos y nuevos. Si la clase se crea usando CreateObject, no funcionará. Puede que este sea el tema de una cuarta parte... pero eso será cuando lo averigüe.

 

Te muestro a continuación el código para VB de la interfaz de los eventos y la parte de la clases PalabrasIO que utiliza el nuevo evento, el resto es igual que el mostrado en la segunda parte de esta serie de artículos.

<InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIDispatch)> _
Public Interface IPalabrasEvents
    Sub Leida(ByVal elNombre As String)
    Sub Guardada(ByVal elNombre As String)
    '
    Sub Nueva(ByVal elNombre As String)
End Interface
<ComVisible(False)> _
Public Delegate Sub PalabraNueva(ByVal elNombre As String)
'
Public Event Nueva As PalabraNueva
'
' para poder añadir nuevos eventos y no perder la compatibilidad binaria
' el uso de los nuevos eventos deben usarse con captura de errores
' por si el cliente no los utiliza...
Protected Sub OnNueva(ByVal elNombre As String)
    Try
        RaiseEvent Nueva(elNombre)
    Catch
    End Try
End Sub
'
' ...
'
Public Overrides Function Add(ByVal unaPalabra As Palabra) As Integer _
        Implements IPalabrasIO.Add
    RaiseEvent Nueva(unaPalabra.Nombre)
    Return MyBase.Add(unaPalabra)
End Function

 

 

El código de C# es el de las dos interfaces y la clase PalabrasIO, como te comenté antes podrás conseguir el código completo en el fichero zip.
Sólo advertir que en el proyecto de C# he creado dos espacios de nombres anidados, por tanto para poder acceder a estas clases hay que usar esos dos espacios de nombres delante del nombre de la clase o bien importar el espacio de nombres, tal como es habitual en los lenguajes .NET.

//-----------------------------------------------------------------------------
// Colección de palabras con métodos de entrada/salida              (13/Ene/03)
//
// ©Guillermo 'guille' Som, 2003
//-----------------------------------------------------------------------------

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Collections; 

namespace pruebasGuille
{
    namespace ClassLibraryCS
    {
        //
        // 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
        {
            void Leida(string elNombre);
            void Guardada(string elNombre);
            // en C# se pueden añadir nuevos eventos sin romper la compatibilidad binaria
            void Nueva(string elNombre);
        }
        //
        public interface IPalabrasIO
        {
            //
            // los miembros heredados
            int Add(Palabra  unaPalabra);
            void Clear();
            Palabras Clone();
            bool Contains(Palabra unaPalabra);
            int Count{get;}
            void CopyTo(Array unArray, int index);
            int IndexOf(Palabra unaPalabra);
            Palabra this[int  index]{get; set;}
            void Remove(Palabra  unaPalabra);
            void RemoveAt(int  index);
            void Reverse();
            void Sort();
            string[] Tipos();
            //
            IEnumerator  GetEnumerator();
            //
            bool IgnoreCase{get; set;}
            SortOrderTypes SortOrder{get; set;}
            //
            // los nuevo métodos
            void Guardar(string fichero);
            void Leer(string fichero);
            string Versión{get;}
        }
        //
        // ComSourceInterfaces se utiliza para las clases que produzcan eventos
        // el nombre de la interfaz debe incluir el namespace en el que se incluye
        [ComSourceInterfaces("pruebasGuille.ClassLibraryCS.IPalabrasEvents")]
        public class PalabrasIO : Palabras, IPalabrasIO
        {
            //
            const string cVersion = "PalabrasIO v";
            //
            // los delegados y eventos
            // Visual Basic permite hacerlo más simple,
            // pero esta es la forma recomendada
            [ComVisible(false)]
                public delegate void PalabraLeida(string elNombre);

            [ComVisible(false)]
                public delegate void PalabraGuardada(string elNombre);

            [ComVisible(false)]
                public delegate void PalabraNueva(string elNombre);
            //
            public event PalabraLeida Leida;
            public event PalabraGuardada Guardada;
            public event PalabraNueva Nueva;
            //
            //
            protected virtual void OnLeida(string elNombre)
            {
                if (Leida != null)
                    Leida(elNombre);
            }
            protected virtual void OnGuardada(string elNombre)
            {
                if (Guardada != null)
                    Guardada(elNombre);
            }
            protected virtual void OnNueva(string elNombre)
            {
                if (Nueva != null)
                    Nueva(elNombre);
            }
            //
            // El constructor
            public PalabrasIO() : base()
            {
            }
            //
            public virtual void Guardar(string fichero)
            {
                StreamWriter sw = new  StreamWriter(fichero, false, System.Text.Encoding.Default);
                //
                sw.WriteLine(this.Versión);
                foreach(Palabra unaPalabra in this)
                {
                    unaPalabra.Guardar(sw);
                    // producir un evento con el dato guardado
                    OnGuardada(unaPalabra.Nombre);
                }
                sw.Close();
            }
            //
            public virtual void Leer(string fichero)
            {
                Palabra unaPalabra;
                StreamReader sr = new StreamReader(fichero, System.Text.Encoding.Default);
                string s  = 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) )
                {
                    this.Clear();
                    while( sr.Peek() != -1)
                    {
                        unaPalabra = new Palabra();
                        unaPalabra.Leer(sr);
                        this.Add(unaPalabra);
                        // producir un evento con el dato leído
                        OnLeida(unaPalabra.Nombre);
                    }
                }
                sr.Close();
            }
            //
            public virtual string Versión
            {
                get
                {
                    return cVersion + "1";
                }
            }
            //
            // los miembros de la clase base
            public override int Add(Palabra unaPalabra)
            {
                OnNueva(unaPalabra.Nombre);
                return base.Add(unaPalabra);
            }
            //
            public override void Clear()
            {
                base.Clear();
            }
            //
            public override Palabras Clone()
            {
                return base.Clone();
            }  
            //
            public override bool Contains(Palabra unaPalabra)
            {
                return base.Contains(unaPalabra);
            }
            //
            public override int Count
            {
                get
                {
                    return base.Count;
                }
            }
            //
            public override void CopyTo(Array unArray, int index)
            {
                base.CopyTo(unArray, index);
            }
            //
            public override bool IgnoreCase
            {
                get
                {
                    return IDClass.IgnoreCase;
                }
                set
                {
                    IDClass.IgnoreCase = value;
                }
            }
            //
            public override int IndexOf(Palabra unaPalabra)
            {
                return base.IndexOf(unaPalabra);
            }
            //
            public override Palabra this[int index]
            {
                get
                {
                    return (Palabra)base[index];
                }
                set
                {
                    base[index] = value;
                }
            }
            //
            public override void Remove(Palabra unaPalabra)
            {
                base.Remove(unaPalabra);
            }
            //
            public override void RemoveAt(int index)
            {
                base.RemoveAt(index);
            }
            //
            public override void Reverse()
            {
                base.Reverse();
            }
            //
            public override void Sort()
            {
                base.Sort();
            }  
            //
            public override SortOrderTypes SortOrder
            {
                get
                {
                    return IDClass.SortOrder;
                }
                set
                {
                    IDClass.SortOrder = value;
                }
            }
            //
            public override string[] Tipos()
            {
                return base.Tipos();
            }
            //
            public override IEnumerator GetEnumerator()
            {
                return base.GetEnumerator();
            }  
        }
    }
}

 

Nota:
Para crear la librería de tipos y registrarla, así como para registrar el componente en el GAC, utiliza estos comandos:
regasm pruebasGuille.ClassLibraryCS.dll /tlb
gacutil /i pruebasGuille.ClassLibraryCS.dll
 

 

Espero que con esta nueva aportación sea más fácil crear componentes que nos ayuden a empezar a migrar las aplicaciones de Visual Basic 6.0, aunque, como he comentado antes, aún seguimos teniendo el problema de la creación de los objetos mediante CreateObject.

 

Nos vemos.
Guillermo

Nerja, a 14 de enero de 2003

Aquí tienes el código para C#, el de VB.NET será como el de la segunda parte, además del código indicado en el artículo. Lo puedes conseguir en este link: servidorNETparaCOM03.zip (142 KB)


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