c# - Excel VSTO 异步按钮 - 用户交互的奇怪行为?

标签 c# excel multithreading asynchronous 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: 'Exception from HRESULT: 0x800AC472'



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

我不确定如何调试这个错误,但它让我问了几个问题:
  • 我是否遵循异步技术的良好实践
    互动?
  • 异步是否有一些限制
    VSTO 上下文中的交互?我知道有一些讨论
    然而,在过去,2018 年的更新讨论将是
    值得。
  • 最佳答案

    可能是 2018 年了,但是底层架构没变,还是不推荐多线程。

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

    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.



    其余的:

    Problem description: you build an Office add-in that periodically makes calls back into the host object model. 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. 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.

    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. You’ll get a COMException, typically something like this: System.Runtime.InteropServices.COMException, Exception from HRESULT: 0x800AC472.

    To fix this, you could implement IMessageFilter in your add-in, and register the message filter on your additional thread. 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. 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. One is in System.Windows.Forms – you don’t want that one. Instead, you want the one defined in objidl.h, which you’ll need to import like this:


    [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. 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. The other 2 methods will be called on the client (that is, our add-in, in this example). We’ll get MessagePending calls after an application has made a COM method call and a Windows message occurs before the call has returned. The important method is 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. COM expects the following return values from this call:

    • -1: the call should be canceled. COM then returns RPC_E_CALL_REJECTED from the original method call.
    • Value >= 0 and <100: the call is to be retried immediately.
    • Value >= 100: COM will wait for this many milliseconds and then retry the call.

    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. Message filters are per-thread, so you must register the filter on the background thread that you create to make the OM call. In the example below, the add-in provides a method InvokeAsyncCallToExcel, which will be invoked from a Ribbon Button. In this method, we create a new thread and make sure this is an STA thread. 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. 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. The CallExcel method makes the call on Excel’s 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 中尝试过,它确实有效,但我没有将它包含在我的软件中,主要是因为我不确定它会以何种方式爆炸。作者没说。

    关于c# - Excel VSTO 异步按钮 - 用户交互的奇怪行为?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51712753/

    相关文章:

    c# - Azure 上 IdentityServer 的部署问题

    vba - 根据单元格值更新文件路径

    vba - 是否可以检查床单的名称是否在附近?

    C# - 将数据从 ThreadPool 线程传回主线程

    c++ - 好的多线程指南?

    c# - 如何使用 CertEnroll 生成扩展验证自签名证书?

    c# - RuntimeBinderException 因为 Razor 中的空引用

    c# - 在 VS2010 c# 中调试 EXCHANGE 传输代理

    python - 从 excel 中读取专栏并进行特定的数学运算

    c++ - 混合 C++11 原子和 OpenMP