c# - Excel VSTO 异步按钮 - 用户交互的奇怪行为?
问题描述
我有一个通过 VSTO 的 Excel 功能区。单击按钮时,会发生一些处理并在当前工作表上填充行。在此过程中,Excel 锁定 - 用户无法继续使用他们的程序。我的解决方法涉及如下实现异步解决方案:
// button1 click handler
private async void button1_Click(object sender, RibbonControlEventArgs e)
{
await Task.Run(new Action(func));
}
// simple func
void func()
{
var currSheet = (Worksheet) Globals.ThisAddIn.Application.ActiveSheet;
int rowSize = 50;
int colSize = 50;
for (int i = 1; i <= rowSize ; i++)
for (int j = 1; j <= colSize ; j++)
((Range) activeSheet.Cells[i, j]).Value2 = "sample";
}
这种方法的一个大问题是,当用户单击时,会弹出以下错误:
System.Runtime.InteropServices.COMException:“来自 HRESULT 的异常:0x800AC472”
但是,与键盘的交互不会触发此类事件。
我不确定如何调试这个错误,但它让我问了几个问题:
- 我是否在异步交互技术中遵循良好实践?
- VSTO 上下文中的异步交互是否存在一些限制?我知道过去有一些讨论,但是,2018 年的更新讨论是值得的。
解决方案
可能是 2018 年了,但是底层架构没变,还是不推荐多线程。
现在,尽管如此,还是有办法的。这是我所知道的关于正确操作的最佳资源......但它会预先警告你:
首先警告:这是一个高级场景,除非您确定自己知道自己在做什么,否则不应尝试使用此技术。发出此警告的原因是,虽然此处描述的技术非常简单,但也很容易出错,从而严重干扰主机应用程序。
其余的:
问题描述:您构建了一个定期调用回宿主对象模型的 Office 加载项。有时呼叫会失败,因为主机正忙于做其他事情。也许它正在重新计算工作表;或者(最常见的),也许它正在显示一个模式对话框并等待用户输入才能继续。
如果您未在加载项中创建任何后台线程,因此在创建加载项的同一线程上进行所有 OM 调用,则您的调用不会失败,直到主机才会调用它已畅通无阻。然后,将按顺序处理。这是正常情况,建议您在大多数情况下采用这种方式设计 Office 解决方案,即不创建任何新线程。
但是,如果您确实创建了额外的线程,并尝试对这些线程中的任何一个进行 OM 调用,那么如果主机被阻塞,调用就会失败。你会得到一个 COMException,通常是这样的:System.Runtime.InteropServices.COMException, Exception from HRESULT: 0x800AC472。
要解决此问题,您可以在加载项中实现 IMessageFilter,并在附加线程上注册消息过滤器。如果您这样做,并且当您在该线程上进行调用时 Excel 正忙,那么 COM 将回调您的 IMessageFilter.RetryRejectedCall 实现。这使您有机会处理失败的呼叫 - 通过重试和/或采取其他一些缓解措施,例如显示一个消息框,告诉用户如果他们希望您的操作继续,则关闭所有打开的对话框。
请注意,通常定义了 2 个 IMessageFilter 接口。一个在 System.Windows.Forms 中——你不想要那个。相反,您需要在 objidl.h 中定义的那个,您需要像这样导入它:
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct INTERFACEINFO
{
[MarshalAs(UnmanagedType.IUnknown)]
public object punk;
public Guid iid;
public ushort wMethod;
}
[ComImport, ComConversionLoss, InterfaceType((short)1), Guid("00000016-0000-0000-C000-000000000046")]
public interface IMessageFilter
{
[PreserveSig, MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
int HandleInComingCall(
[In] uint dwCallType,
[In] IntPtr htaskCaller,
[In] uint dwTickCount,
[In, MarshalAs(UnmanagedType.LPArray)] INTERFACEINFO[] lpInterfaceInfo);
[PreserveSig, MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
int RetryRejectedCall(
[In] IntPtr htaskCallee,
[In] uint dwTickCount,
[In] uint dwRejectType);
[PreserveSig, MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
int MessagePending(
[In] IntPtr htaskCallee,
[In] uint dwTickCount,
[In] uint dwPendingType);
}
然后,在您的 ThisAddIn 类中实现此接口。请注意,IMessageFilter 也在服务器上实现(即,在我们的示例中,在 Excel 中),并且 IMessageFilter.HandleInComingCall 调用仅在服务器上进行。其他 2 个方法将在客户端(即本例中的加载项)上调用。在应用程序进行 COM 方法调用后,我们将收到 MessagePending 调用,并且在调用返回之前出现 Windows 消息。重要的方法是 RetryRejectedCall。在下面的实现中,我们显示一个消息框,询问用户是否要重试该操作。如果他们说“是”,我们返回 1,否则返回 -1。COM 期望此调用的以下返回值:
- -1:呼叫应该被取消。COM 然后从原始方法调用返回 RPC_E_CALL_REJECTED。
- 值 >= 0 且 <100:将立即重试调用。
- 值 >= 100:COM 将等待这么多毫秒,然后重试调用。
public int HandleInComingCall([In] uint dwCallType, [In] IntPtr htaskCaller, [In] uint dwTickCount,
[In, MarshalAs(UnmanagedType.LPArray)] INTERFACEINFO[] lpInterfaceInfo)
{
Debug("HandleInComingCall");
return 1;
}
public int RetryRejectedCall([In] IntPtr htaskCallee, [In] uint dwTickCount, [In] uint dwRejectType)
{
int retVal = -1;
Debug.WriteLine("RetryRejectedCall");
if (MessageBox.Show("retry?", "Alert", MessageBoxButtons.YesNo) == DialogResult.Yes)
{
retVal = 1;
}
return retVal;
}
public int MessagePending([In] IntPtr htaskCallee, [In] uint dwTickCount, [In] uint dwPendingType)
{
Debug("MessagePending");
return 1;
}
最后,使用 CoRegisterMessageFilter 向 COM 注册您的消息过滤器。消息过滤器是每个线程的,因此您必须在创建的后台线程上注册过滤器以进行 OM 调用。在下面的示例中,加载项提供了一个方法 InvokeAsyncCallToExcel,该方法将从功能区按钮调用。在这种方法中,我们创建一个新线程并确保这是一个 STA 线程。在我的示例中,线程过程 RegisterFilter 完成了注册过滤器的工作——然后它休眠 3 秒,让用户有机会做一些会阻塞的事情——例如在 Excel 中弹出一个对话框。这显然只是为了演示目的,这样您就可以看到当 Excel 在后台线程调用之前阻塞时会发生什么。CallExcel 方法在 Excel 的 OM 上进行调用。
[DllImport("ole32.dll")]
static extern int CoRegisterMessageFilter(IMessageFilter lpMessageFilter, out IMessageFilter lplpMessageFilter);
private IMessageFilter oldMessageFilter;
internal void InvokeAsyncCallToExcel()
{
Thread t = new Thread(this.RegisterFilter);
t.SetApartmentState(ApartmentState.STA);
t.Start();
}
private void RegisterFilter()
{
CoRegisterMessageFilter(this, out oldMessageFilter);
Thread.Sleep(3000);
CallExcel();
}
private void CallExcel()
{
try
{
this.Application.ActiveCell.Value2 = DateTime.Now.ToShortTimeString();
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
注意我已将返回类型从 uint 更改为 int,因为原始代码没有编译。我已经在 Word 中尝试过,它确实有效,但我没有将它包含在我的软件中,主要是因为我不确定它会以何种方式爆炸。作者没说。
推荐阅读
- typescript - 打字稿未检测到类型不匹配
- python - 如何创建一个类来管理从 api 请求的其他类的令牌?
- php - 如何使用 docker 的官方 php-fpm debian 映像查看或记录 PHP 错误?
- angularjs - 条件显示样式
- animation - Adobe 社区论坛有其他选择吗?
- reactjs - 将函数传递给 onClick 参数
- color-picker - HTML5 颜色选择器 - 颜色未更改
- php - 基于使用 php in_array 从数据库中获取值选择的多个选项
- javascript - Nuxt/Gitlab:警告:公共:没有匹配的文件错误:没有要上传的文件
- php - 将 PHP 变量传递给 sql 查询