将.NET事件暴露给COM?

33
我一直在尝试向VBA客户端公开和触发一个事件。到目前为止,在VBA客户端方面,事件已经被公开,并且我看到了事件处理方法添加到我的模块类中,但是VBA事件处理方法没有触发。出于某种原因,在调试时事件为空。修改我的代码并同步执行也没有帮助。
值得一提的是,我已经查看了其他SO问题,但它们没有帮助。
非常感谢任何好的答案。
[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();
}

尝试在Task之前声明MyClass parent = this。在你的任务中使用parent.OnDownloadCompleted。 - Jules
1个回答

35

.NET代码中的关键概念是将事件定义为单独接口上的方法,并通过[ComSourceInterfacesAttribute]将其连接到类上。在本例中,可以使用以下代码完成此操作:[ComSourceInterfaces(typeof(IEvents))],其中IEvents接口定义了应在COM客户端上处理的事件。

注意事件命名:
C#类中定义的事件名称和接口方法名称必须相同。在本例中,IEvents::OnDownloadCompleted对应于DemoEvents::OnDownloadCompleted

然后定义第二个界面,该界面代表类本身的公共API,这里称为IDemoEvents。在该界面上定义了要在COM客户端上调用的方法。

C#代码(构建为COMVisibleEvents.dll)

using System;
using System.Diagnostics;
using System.EnterpriseServices;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace COMVisibleEvents
{
    [ComVisible(true)]
    [Guid("8403C952-E751-4DE1-BD91-F35DEE19206E")]
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    public interface IEvents
    {
        [DispId(1)]
        void OnDownloadCompleted();

        [DispId(2)]
        void OnDownloadFailed(string message);
    }

    [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();
        public delegate void OnDownloadFailedDelegate(string message);

        public event OnDownloadCompletedDelegate OnDownloadCompleted;
        public event OnDownloadFailedDelegate OnDownloadFailed;

        private string FileNamePath(string filename) 
            => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), filename);

        public async Task DownloadFileAsync(string address, string filename)
        {
            try
            {
                using (var webClient = new WebClient())
                {
                    await webClient
                        .DownloadFileTaskAsync(new Uri(address), FileNamePath(filename))
                        .ContinueWith(t =>
                        {
                            if (t.Status == TaskStatus.Faulted)
                            {
                                var failed = OnDownloadFailed;
                                failed?.Invoke(GetExceptions(t));
                            }
                            else
                            {
                                var completed = OnDownloadCompleted;
                                completed?.Invoke();
                            }
                        }, TaskScheduler.FromCurrentSynchronizationContext());
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
            }

            #region Local

            string GetExceptions(Task task)
            {
                var innerExceptions = task.Exception?.Flatten().InnerExceptions;
                if (innerExceptions == null)
                    return string.Empty;
                var builder = new StringBuilder();
                foreach (var e in innerExceptions)
                    builder.AppendLine(e.Message);
                return builder.ToString();
            }

            #endregion Local
        }
    }
}

regasm
C:\Windows\Microsoft.NET\Framework\v4.0.30319>regasm C:\Temp\COMVisibleEvents\bin\Debug\COMVisibleEvents.dll /tlb: C:\Temp\COMVisibleEvents\bin\Debug\COMVisibleEvents.tlb

注册COM互操作 在本地计算机上的开发过程中使用此设置。 https://dev59.com/Q3A65IYBdhLWcg3wqQc8#3700057 VBA客户端参考*.tlb文件。
添加对由regasm生成的*tlb的引用。这个tlb文件的名称是COMVisibleEvents。

enter image description here

在这里,Excel用户表单被用作VBA客户端。点击按钮后,将执行方法DownloadFileAsync,当该方法完成时,事件将在处理程序m_eventSource_OnDownloadCompleted中捕获。在这个例子中,您可以从datahub.io下载机场代码。

VBA客户端代码(MS Excel 2016)

Option Explicit

Private WithEvents m_eventSource As DemoEvents

Private Sub DownloadFileAsyncButton_Click()
    m_eventSource.DownloadFileAsync "https://datahub.io/core/airport-codes/r/airport-codes.json", "airport-codes.json"
End Sub

Private Sub m_eventSource_OnDownloadCompleted()
    MsgBox "Download completed..."
End Sub

Private Sub m_eventSource_OnDownloadFailed(ByVal message As String)
    MsgBox "Download failed. " & message, vbCritical, "Error"
End Sub

Private Sub UserForm_Initialize()
    Set m_eventSource = New COMVisibleEvents.DemoEvents
End Sub

Result

enter image description here

源代码也在GitHub上:

https://github.com/ddmail/COMVisibleEvents.git


为什么 DemoEvents 中的 OnDownloadCompleted 事件是私有的? - Orace
m_eventSource_OnDownloadCompleted() 是事件处理程序,表单处理该事件,因此此行为属于表单类本身,并且通常是私有的。 - Daniel Dušek
啊,我明白了,那段代码只是一个演示,事件可以/应该是公共的。 - Daniel Dušek
我想知道如果这样声明,事件是否可以从VBA访问。COM事件的声明总是让我感到困扰。 - Orace
1
我想在VBA中这并不重要,因为你可以通过接口访问事件。 - Daniel Dušek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接