Usar un
componente .NET desde COM (3ª parte) Añadir nuevos eventos sin perder la compatibilidad binaria |
Publicado el 14/Ene/2003 Links a los otros artículos de esta serie: 1, 2 y 3
|
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.
GuillermoNerja, 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)