Excel VSTO异步按钮-用户交互的异常行为?

时间:2018-08-06 17:23:07

标签: c# excel multithreading asynchronous vsto

我通过VSTO有一个Excel功能区。单击一个按钮时,将进行一些处理,并在当前工作表上填充行。在此过程中,Excel锁定-用户无法继续使用其程序。我的解决方法涉及实现异步解决方案,如下所示:

<Subject>
<NameID>S40rgb3XjhFTv6EQTETkEzcgVmToHKRkZUIsJlmLdVc</NameID>
<SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer" />
</Subject> 

此方法的一个大问题是,当用户单击时,会弹出以下错误:

  

System.Runtime.InteropServices.COMException:'来自HRESULT的异常:   0x800AC472'

但是,与键盘的交互不会触发此类事件。

我不确定如何调试此错误,但这使我提出了一些问题:

  • 我遵循异步技术的良好实践吗? 互动?
  • 异步有一些限制吗? VSTO上下文中的交互?我知道有人在讨论 但是,过去,2018年的最新讨论将是 值得。

1 个答案:

答案 0 :(得分:-1)

可能是2018年,但基础架构没有改变,仍然不建议使用多线程。

尽管如此,还是有办法的。 Here's是我所知道的关于正确执行操作的最佳资源……但是它会提前警告您:

  

首先警告:这是一种高级情况,除非您确定知道自己在做什么,否则请勿尝试使用此技术。发出此警告的原因是,尽管此处描述的技术非常简单,但也很容易以可能会严重干扰主机应用程序的方式出错。

其余:

  

问题描述:您构建了一个Office加载项,该加载项会定期向宿主对象模型中调用。有时呼叫会失败,因为主机正忙于做其他事情。也许它正在重新计算工作表;或(最常见),它可能正在显示一个模式对话框,并等待用户输入才能继续。

     

如果您没有在外接程序中创建任何后台线程,因此在您创建外接程序的同一个线程上进行所有OM调用,则您的调用不会失败,只会被调用直到主机被解除阻止。然后,将按顺序对其进行处理。这是正常情况,建议在大多数情况下,这是设计Office解决方案的方式–即,不创建任何新线程。

     

但是,如果您确实创建了其他线程,并尝试在这些线程中的任何一个上进行OM调用,则如果主机被阻止,则调用将仅失败。您将获得一个COMException,通常是这样的:System.Runtime.InteropServices.COMException,来自HRESULT的异常:0x800AC472。

     

要解决此问题,您可以在外接程序中实现IMessageFilter,然后在其他线程上注册消息过滤器。如果执行此操作,并且在该线程上进行调用时Excel处于繁忙状态,则COM将回调您的IMessageFilter.RetryRejectedCall实现。这使您有机会处理失败的呼叫-通过重试和/或采取其他缓解措施,例如显示消息框,告诉用户如果他们希望继续操作,请关闭任何打开的对话框。 / p>      

请注意,通常定义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方法调用并且在调用返回之前出现Windows消息之后,我们将获得MessagePending调用。重要的方法是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中尝试过此方法,并且它确实可以工作,但是我没有将其包含在软件中,主要是因为我不确定该方法的爆发方式。作者没有说。