为什么 COM 事件处理程序总是空的?
Why are COM event handlers always null?
使用 this article,我设置了这个 COM 可见接口来定义我的事件:
[ComVisible(true)]
[Guid("3D8EAA28-8983-44D5-83AF-2EEC4C363079")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IParserStateEvents
{
void OnParsed();
void OnReady();
void OnError();
}
事件应由实现此接口的 class 触发:
[ComVisible(true)]
public interface IParserState
{
void Initialize(VBE vbe);
void Parse();
void BeginParse();
Declaration[] AllDeclarations { get; }
Declaration[] UserDeclarations { get; }
}
实现如下:
[ComVisible(true)]
[Guid(ClassId)]
[ProgId(ProgId)]
[ClassInterface(ClassInterfaceType.AutoDual)]
[ComDefaultInterface(typeof(IParserState))]
[ComSourceInterfaces(typeof(IParserStateEvents))]
[EditorBrowsable(EditorBrowsableState.Always)]
public class ParserState : IParserState
{
//...
public event Action OnParsed;
public event Action OnReady;
public event Action OnError;
private void _state_StateChanged(object sender, System.EventArgs e)
{
var errorHandler = OnError; // always null
if (_state.Status == Parsing.VBA.ParserState.Error && errorHandler != null)
{
errorHandler.Invoke();
}
var parsedHandler = OnParsed; // always null
if (_state.Status == Parsing.VBA.ParserState.Parsed && parsedHandler != null)
{
parsedHandler.Invoke();
}
var readyHandler = OnReady; // always null
if (_state.Status == Parsing.VBA.ParserState.Ready && readyHandler != null)
{
readyHandler.Invoke();
}
}
//...
_state_StateChanged
处理程序正在响应从后台工作线程引发的事件。
COM 客户端代码是 VBA class 如下所示:
Private WithEvents state As Rubberduck.ParserState
Public Sub Initialize()
Set state = New Rubberduck.ParserState
state.Initialize Application.vbe
state.BeginParse
End Sub
Private Sub state_OnError()
Debug.Print "error"
End Sub
Private Sub state_OnParsed()
Debug.Print "parsed"
End Sub
Private Sub state_OnReady()
Debug.Print "ready"
End Sub
虽然从 对象浏览器 中看起来一切正常:
...当 VBA 代码调用 BeginParse
时,断点在 C# 代码中命中,但所有处理程序都是 null
,因此 VBA 处理程序不要 运行:
我做错了什么?
您的 COM/VBA 集成是正确的,但是您需要牢记 COM 线程模型和在单线程单元中使用 COM class 的规则。
您在 STA 线程上创建了 Rubberduck.ParserState
实例。 VBA 立即看到 WithEvents
说明符并尽力将事件处理程序连接到 COM class 实现的连接点。具体来说,COM class 接收 COM 接口指针以在同一线程 上接受事件调用 并存储指针以便稍后在事件调用时使用它。
当您引发事件时,服务器 (C#) 和客户端 (VBA) 可能会也可能不会检查是否在正确的线程(而不是正确的单元)上执行。对于 C++ 开发,您可能有机会忽略线程不匹配(这不是一件好事,但我们假设您知道自己在做什么),并且像 VBA 和 .NET COM 互操作这样的环境更严格地尝试注意整体环境的完整性,如果线程错误,它们很可能会失败。也就是说,您必须在正确的线程上引发您的事件!如果你有一个后台工作线程,你不能直接从它引发事件,你需要先将它传递给实际需要调用的单元线程。
如果您的线程问题仅限于从工作线程调用,则问题宁愿是非空事件接收器调用,您会收到异常或其他调用未到达您的 VBA。然而,你有 null,所以线程很可能以另一种方式影响(从工作线程上的某些回调实例化等)无论哪种方式,一旦你违反了不在公寓之间传递接口指针的 COM 规则,指针就会变得不可用,导致呼叫失败或无法提供预期的演员等等)。修复它后,事件就会正常运行。
奖励代码:最小的 C# 项目和 XLS 文件证明事件以最简单的形式运行良好 (Subversion/Trac)。
事件是从 Initialize 调用中引发的:
public void Initialize()
{
if (OnReady != null)
OnReady();
}
Private Sub Worksheet_Activate()
If state Is Nothing Then Set state = New ComEvents01.ParserState
' Initialize below will have C# raise an event we'd receive state_OnReady
state.Initialize
End Sub
Private Sub state_OnReady()
' We do reach here from Initialize and Worksheet_Activate
End Sub
使用 this article,我设置了这个 COM 可见接口来定义我的事件:
[ComVisible(true)]
[Guid("3D8EAA28-8983-44D5-83AF-2EEC4C363079")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IParserStateEvents
{
void OnParsed();
void OnReady();
void OnError();
}
事件应由实现此接口的 class 触发:
[ComVisible(true)]
public interface IParserState
{
void Initialize(VBE vbe);
void Parse();
void BeginParse();
Declaration[] AllDeclarations { get; }
Declaration[] UserDeclarations { get; }
}
实现如下:
[ComVisible(true)]
[Guid(ClassId)]
[ProgId(ProgId)]
[ClassInterface(ClassInterfaceType.AutoDual)]
[ComDefaultInterface(typeof(IParserState))]
[ComSourceInterfaces(typeof(IParserStateEvents))]
[EditorBrowsable(EditorBrowsableState.Always)]
public class ParserState : IParserState
{
//...
public event Action OnParsed;
public event Action OnReady;
public event Action OnError;
private void _state_StateChanged(object sender, System.EventArgs e)
{
var errorHandler = OnError; // always null
if (_state.Status == Parsing.VBA.ParserState.Error && errorHandler != null)
{
errorHandler.Invoke();
}
var parsedHandler = OnParsed; // always null
if (_state.Status == Parsing.VBA.ParserState.Parsed && parsedHandler != null)
{
parsedHandler.Invoke();
}
var readyHandler = OnReady; // always null
if (_state.Status == Parsing.VBA.ParserState.Ready && readyHandler != null)
{
readyHandler.Invoke();
}
}
//...
_state_StateChanged
处理程序正在响应从后台工作线程引发的事件。
COM 客户端代码是 VBA class 如下所示:
Private WithEvents state As Rubberduck.ParserState
Public Sub Initialize()
Set state = New Rubberduck.ParserState
state.Initialize Application.vbe
state.BeginParse
End Sub
Private Sub state_OnError()
Debug.Print "error"
End Sub
Private Sub state_OnParsed()
Debug.Print "parsed"
End Sub
Private Sub state_OnReady()
Debug.Print "ready"
End Sub
虽然从 对象浏览器 中看起来一切正常:
...当 VBA 代码调用 BeginParse
时,断点在 C# 代码中命中,但所有处理程序都是 null
,因此 VBA 处理程序不要 运行:
我做错了什么?
您的 COM/VBA 集成是正确的,但是您需要牢记 COM 线程模型和在单线程单元中使用 COM class 的规则。
您在 STA 线程上创建了 Rubberduck.ParserState
实例。 VBA 立即看到 WithEvents
说明符并尽力将事件处理程序连接到 COM class 实现的连接点。具体来说,COM class 接收 COM 接口指针以在同一线程 上接受事件调用 并存储指针以便稍后在事件调用时使用它。
当您引发事件时,服务器 (C#) 和客户端 (VBA) 可能会也可能不会检查是否在正确的线程(而不是正确的单元)上执行。对于 C++ 开发,您可能有机会忽略线程不匹配(这不是一件好事,但我们假设您知道自己在做什么),并且像 VBA 和 .NET COM 互操作这样的环境更严格地尝试注意整体环境的完整性,如果线程错误,它们很可能会失败。也就是说,您必须在正确的线程上引发您的事件!如果你有一个后台工作线程,你不能直接从它引发事件,你需要先将它传递给实际需要调用的单元线程。
如果您的线程问题仅限于从工作线程调用,则问题宁愿是非空事件接收器调用,您会收到异常或其他调用未到达您的 VBA。然而,你有 null,所以线程很可能以另一种方式影响(从工作线程上的某些回调实例化等)无论哪种方式,一旦你违反了不在公寓之间传递接口指针的 COM 规则,指针就会变得不可用,导致呼叫失败或无法提供预期的演员等等)。修复它后,事件就会正常运行。
奖励代码:最小的 C# 项目和 XLS 文件证明事件以最简单的形式运行良好 (Subversion/Trac)。
事件是从 Initialize 调用中引发的:
public void Initialize()
{
if (OnReady != null)
OnReady();
}
Private Sub Worksheet_Activate()
If state Is Nothing Then Set state = New ComEvents01.ParserState
' Initialize below will have C# raise an event we'd receive state_OnReady
state.Initialize
End Sub
Private Sub state_OnReady()
' We do reach here from Initialize and Worksheet_Activate
End Sub