简体   繁体   English

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

[英]Excel VSTO async button - strange behaviour with user interaction?

I have an Excel Ribbon via VSTO. 我有一个通过VSTO的Excel功能区。 When a button is clicked, some processing happens and rows are populated on current sheet. 单击一个按钮时,将进行一些处理,并在当前工作表上填充行。 During this process, Excel locks - the user cannot keep working with their program. 在此过程中,Excel锁定-用户无法继续使用其程序。 My workaround involves implementing an async solution as follows: 我的解决方法涉及实现异步解决方案,如下所示:

 // 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";
 }

One big problem with this approach is that when a user is clicking, the following error pops up: 这种方法的一个大问题是,当用户单击时,会弹出以下错误:

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

however, interactions with keyboard do not trigger such an event. 但是,与键盘的交互不会触发此类事件。

I am unsure how to debug this error, but it leads me into asking a few questions: 我不确定如何调试此错误,但这使我提出了一些问题:

  • Am I following good practice in my technique for asynchronous interactions? 我在异步交互技术方面遵循良好实践吗?
  • Are there some limitations with asynchronous interactions in the VSTO context? VSTO上下文中的异步交互是否有一些限制? I know there were some discussions in the past, however, an updated discussion in 2018 would be worthwhile. 我知道过去曾进行过一些讨论,但是,在2018年进行更新的讨论是值得的。

It may be 2018 but the underlying architecture has not changed, multi-threading is still not recommended. 可能是2018年,但基础架构没有改变,仍然不建议使用多线程。

Now, despite that, there is a way. 现在,尽管如此,还是有一种方法。 Here's is the best resource I know of with respect to doing it correctly... but it warns you up front: 是我所知道的有关正确执行操作的最佳资源...但是它会提前警告您:

First a warning: this is an advanced scenario, and you should not attempt to use this technique unless you're sure you know what you're doing. 首先警告:这是一个高级方案,除非确定您知道自己在做什么,否则不要尝试使用此技术。 The reason for this warning is that while the technique described here is pretty simple, it's also easy to get wrong in ways that could interfere significantly with the host application. 发出此警告的原因是,尽管此处描述的技术非常简单,但也很容易以可能严重干扰主机应用程序的方式出错。

And the rest: 其余的:

Problem description: you build an Office add-in that periodically makes calls back into the host object model. 问题描述:您构建了一个Office加载项,该加载项会定期向宿主对象模型中调用。 Sometimes the calls will fail, because the host is busy doing other things. 有时呼叫会失败,因为主机正忙于做其他事情。 Perhaps it is recalculating the worksheet; 也许它正在重新计算工作表; or (most commonly), perhaps it is showing a modal dialog and waiting for user input before it can continue. 或(最常见),它可能正在显示一个模式对话框,并等待用户输入才能继续。

If you don't create any background threads in your add-in, and therefore make all OM calls on the same thread your add-in was created on, your call won't fail, it simply won't be invoked until the host is unblocked. 如果您没有在外接程序中创建任何后台线程,并因此在创建外接程序的同一线程上进行所有OM调用,则您的调用不会失败,仅在主机之前它不会被调用畅通无阻。 Then, it will be processed in sequence. 然后,将按顺序对其进行处理。 This is the normal case, and it is recommended that this is how you design your Office solutions in most scenarios – that is, without creating any new threads. 这是正常情况,建议在大多数情况下,这是设计Office解决方案的方式-即,不创建任何新线程。

However, if you do create additional threads, and attempt to make OM calls on any of those threads, then the calls will simply fail if the host is blocked. 但是,如果您确实创建了其他线程,并尝试在这些线程中的任何一个上进行OM调用,则如果主机被阻止,则调用将仅会失败。 You'll get a COMException, typically something like this: System.Runtime.InteropServices.COMException, Exception from HRESULT: 0x800AC472. 您将获得一个COMException,通常是这样的:System.Runtime.InteropServices.COMException,来自HRESULT的异常:0x800AC472。

To fix this, you could implement IMessageFilter in your add-in, and register the message filter on your additional thread. 要解决此问题,您可以在外接程序中实现IMessageFilter,然后在其他线程上注册消息过滤器。 If you do this, and Excel is busy when you make a call on that thread, then COM will call back on your implementation of IMessageFilter.RetryRejectedCall. 如果执行此操作,并且在该线程上进行调用时Excel处于繁忙状态,则COM将回调您的IMessageFilter.RetryRejectedCall实现。 This gives you an opportunity to handle the failed call – either by retrying it, and/or by taking some other mitigating action, such as displaying a message box to tell the user to close any open dialogs if they want your operation to continue. 这使您有机会处理失败的呼叫-通过重试和/或采取其他缓解措施,例如显示消息框,告诉用户如果他们希望您的操作继续进行,请关闭所有打开的对话框。

Note that there are 2 IMessageFilter interfaces commonly defined. 请注意,通常定义2个IMessageFilter接口。 One is in System.Windows.Forms – you don't want that one. 一个在System.Windows.Forms中–您不想要那个。 Instead, you want the one defined in objidl.h, which you'll need to import like this: 相反,您需要在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);
}

Then, implement this interface in your ThisAddIn class. 然后,在您的ThisAddIn类中实现此接口。 Note that IMessageFilter is also implemented on the server (that is, in Excel, in our example), and that the IMessageFilter.HandleInComingCall call is only made on the server. 请注意,IMessageFilter也是在服务器上实现的(在本例中为Excel),并且IMessageFilter.HandleInComingCall调用仅在服务器上进行。 The other 2 methods will be called on the client (that is, our add-in, in this example). 其他2个方法将在客户端上调用(在此示例中,即我们的外接程序)。 We'll get MessagePending calls after an application has made a COM method call and a Windows message occurs before the call has returned. 在应用程序进行COM方法调用并且在调用返回之前发生Windows消息之后,我们将获得MessagePending调用。 The important method is RetryRejectedCall. 重要的方法是RetryRejectedCall。 In the implementation below, we display a message box asking the user whether or not they want to retry the operation. 在下面的实现中,我们显示一个消息框,询问用户是否要重试该操作。 If they say “Yes”, we return 1, otherwise -1. 如果他们说“是”,则返回1,否则返回-1。 COM expects the following return values from this call: COM期望此调用返回以下值:

  • -1: the call should be canceled. -1:呼叫应被取消。 COM then returns RPC_E_CALL_REJECTED from the original method call. 然后,COM从原始方法调用返回RPC_E_CALL_REJECTED。
  • Value >= 0 and <100: the call is to be retried immediately. 值> = 0和<100:呼叫将立即重试。
  • Value >= 100: COM will wait for this many milliseconds and then retry the call. 值> = 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;
}

Finally, register your message filter with COM, using CoRegisterMessageFilter. 最后,使用CoRegisterMessageFilter向COM注册消息过滤器。 Message filters are per-thread, so you must register the filter on the background thread that you create to make the OM call. 消息筛选器是每个线程的,因此必须在创建的用于调用OM的后台线程上注册筛选器。 In the example below, the add-in provides a method InvokeAsyncCallToExcel, which will be invoked from a Ribbon Button. 在下面的示例中,外接程序提供一个方法InvokeAsyncCallToExcel,将从功能区按钮中调用该方法。 In this method, we create a new thread and make sure this is an STA thread. 在此方法中,我们创建一个新线程并确保这是一个STA线程。 In my example, the thread procedure, RegisterFilter, does the work of registering the filter – and it then sleeps for 3 seconds to give the user a chance to do something that will block – such as pop up a dialog in Excel. 在我的示例中,线程过程RegisterFilter完成了注册过滤器的工作,然后休眠3秒钟,使用户有机会进行可能会阻止的操作,例如在Excel中弹出对话框。 This is clearly just for demo purposes, so that you can see what happens when Excel blocks just before a background thread call is made. 显然,这只是出于演示目的,因此您可以看到Excel在进行后台线程调用之前阻塞时会发生什么。 The CallExcel method makes the call on Excel's OM. 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());
    }
}

Note I've changed the return types from uint to int as the original code did not compile. 注意我将返回类型从uint更改为int,因为原始代码未编译。 I've tried this in Word and it does work, but I have not included it in my software, mainly because I'm not sure of the ways in which this can blow up. 我已经在Word中尝试过此方法,并且它确实可以工作,但是我没有将其包含在软件中,主要是因为我不确定该方法的爆发方式。 The author doesn't say. 作者没有说。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM