c# - ¿Exponiendo eventos.NET a COM?
vba events (1)
He estado tratando de exponer y disparar un evento a un cliente VBA. Hasta ahora, en el lado del cliente VBA, el evento está expuesto y veo el método de manejo de eventos agregado a mi clase de módulo, sin embargo, el método de manejo de eventos VBA no se activa. Por alguna razón, cuando la depuración del evento es nula. Modificar mi código con síncrono tampoco ayudó.
Para el registro, he comprobado otras preguntas de SO, pero no ayudaron.
Cualquier buena respuesta será apreciada.
[ComVisible(true)]
[Guid("56C41646-10CB-4188-979D-23F70E0FFDF5")]
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(IWebEvents))]
[ProgId("MyAssembly.MyClass")]
public class MyClass : ServicedComponent, IMyClass
{
public string _address { get; private set; }
public string _filename { get; private set; }
[DispId(4)]
public void DownloadFileAsync(string address, string filename)
{
_address = address;
_filename = filename;
System.Net.WebClient wc = new System.Net.WebClient();
Task.Factory.StartNew(() => wc.DownloadFile(_address, _filename))
.ContinueWith((t) =>
{
if (null != this.OnDownloadCompleted)
OnDownloadCompleted();
});
}
public event OnDownloadCompletedEventHandler OnDownloadCompleted;
}
[ComVisible(false)]
public delegate void OnDownloadCompletedEventHandler();
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IWebEvents
{
[DispId(1)]
void OnDownloadCompleted();
}
Este es un buen concierto para todos los cazarrecompensas, 200 puntos de repetición.
El concepto clave en el código .net es definir el (los) evento (s) como método (s) en una interfaz separada y conectarlo a la clase a través de [ComSourceInterfacesAttribute]
. En el ejemplo, esto se hace con este código [ComSourceInterfaces(typeof(IEvents))]
donde la interfaz de IEvents
define los eventos que deben manejarse en el cliente COM.
Nota para la denominación de eventos: los nombres de eventos definidos en la clase c # y los nombres de los métodos de interfaz definidos en la interfaz deben ser los mismos. En este ejemplo, IEvents::OnDownloadCompleted
corresponde con DemoEvents::OnDownloadCompleted
.
Luego se define una segunda interfaz que representa la API pública de la clase en sí, aquí se llama IDemoEvents
. En esta interfaz se definen los métodos que se llaman en el cliente COM.
Código C # (compilaciones a COMVisibleEvents.dll)
using System;
using System.EnterpriseServices;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace COMVisibleEvents
{
[ComVisible(true)]
[Guid("8403C952-E751-4DE1-BD91-F35DEE19206E")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IEvents
{
[DispId(1)]
void OnDownloadCompleted();
}
[ComVisible(true)]
[Guid("2BF7DA6B-DDB3-42A5-BD65-92EE93ABB473")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IDemoEvents
{
[DispId(1)]
Task DownloadFileAsync(string address, string filename);
}
[ComVisible(true)]
[Guid("56C41646-10CB-4188-979D-23F70E0FFDF5")]
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(IEvents))]
[ProgId("COMVisibleEvents.DemoEvents")]
public class DemoEvents
: ServicedComponent, IDemoEvents
{
public delegate void OnDownloadCompletedDelegate();
private event OnDownloadCompletedDelegate OnDownloadCompleted;
public string _address { get; private set; }
public string _filename { get; private set; }
private readonly string _downloadToDirectory =
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
public async Task DownloadFileAsync(string address, string filename)
{
try
{
using (WebClient webClient = new WebClient())
{
webClient.Credentials = new NetworkCredential(
"user", "psw", "domain");
string file = Path.Combine(_downloadToDirectory, filename);
await webClient.DownloadFileTaskAsync(new Uri(address), file)
.ContinueWith(t =>
{
if (OnDownloadCompleted != null)
{
OnDownloadCompleted();
}
}, TaskScheduler.FromCurrentSynchronizationContext());
}
}
catch (Exception ex)
{
// Log exception here ...
}
}
}
}
Nota para la descarga de archivos: Para descargar el archivo se usa el método WebClient.DownloadFileTaskAsync
. Descarga el recurso especificado a un archivo local como una operación asincrónica utilizando un objeto de tarea. El objeto de tarea normalmente se ejecuta de forma asíncrona en un subproceso de grupo de subprocesos en lugar de sincronizarse en el subproceso de la aplicación principal. Por lo tanto, es necesario llamar a ContinueWith
en el hilo principal, ya que de lo contrario no será posible ejecutar el evento OnDownloadCompleted
. Es por eso que se utiliza ContinueWith(continuationAction, TaskScheduler.FromCurrentSynchronizationContext)
.
regasmo
C:/Windows/Microsoft.NET/Framework/v4.0.30319>regasm C:/Temp/COMVisibleEvents/bin/Debug/COMVisibleEvents.dll /tlb: C:/Temp/COMVisibleEvents/bin/Debug/COMVisibleEvents.tlb
Referencia de cliente VBA al archivo
*.tlb
Agregar referencia a *tlb
que fue generada por regasm
. Aquí el nombre de este archivo COMVisibleEvents
es COMVisibleEvents
.
Aquí se utilizó el formulario de usuario de Excel como cliente VBA. Después de hacer clic en el botón, se ejecutó el método DownloadFileAsync
y cuando este método se completa, el evento se m_eventSource_OnDownloadCompleted
en el controlador m_eventSource_OnDownloadCompleted
. En este ejemplo, puede descargar los códigos fuente de c # project COMVisibleEvents.dll de mi dropbox.
Código de cliente VBA (MS Excel 2007)
Option Explicit
Private WithEvents m_eventSource As DemoEvents
Private Sub DownloadFileAsyncButton_Click()
m_eventSource.DownloadFileAsync "https://www.dropbox.com/s/0q3dskxopelymac/COMVisibleEvents.zip?dl=0", "COMVisibleEvents.zip"
End Sub
Private Sub m_eventSource_OnDownloadCompleted()
MsgBox "Download completed..."
End Sub
Private Sub UserForm_Initialize()
Set m_eventSource = New COMVisibleEvents.DemoEvents
End Sub
Resultado