VSTO 异步/等待 - 无法取消长时间运行的操作

问题描述 投票:0回答:3

我正在使用 VSTO 和 WPF (MVVM) 使用 C# 开发 Word 搜索工具。

我正在使用 Microsoft.Office.Interop.Word.Find() 方法并迭代文档以查找匹配项。我需要处理的一些文档超过 300,000 个字符,因此搜索可能需要 10 秒以上。我想为我的用户提供取消操作的选项。

我面临的问题是,用于取消长时间运行的操作的按钮无法访问,因为 UI/主线程由于查找操作触发编组回主线程而保持忙碌 - 中继命令永远不会被触发。我的数据绑定是正确的,并且已使用不使用 UI/主线程的长时间运行操作测试了按钮。

public class ViewModel : BindableBase
{
   ctor()
   {
            FindCommand = new RelayCommand(o => Find(), o => CanFindExecute());
   }

   private async void Find()
   {
            var token = cancellationTokenSource.Token;
            **Update user here and show progress view**
            
            try
            {
                await System.Threading.Tasks.Task.Run(async() => { 
                        var searchResults = await SearchRange(token);
                        System.Windows.Application.Current.Dispatcher.Invoke(() =>
                        {
                            **Update results on UI Thread**
                        });
                        return;
                    }
                });
            }
            catch (OperationCanceledException)
            {
                ...
            }
            catch(Exception ex)
            {
                ...
            }
            finally
            {
                **Hide progress view**
            }
            
    }

    public async Task<List<ResultViewModel>> SearchRange(CancellationToken cancellationToken)
    {
            ** Get Word range**
            await System.Threading.Tasks.Task.Run(() =>
            {
                do
                {
                    range.Find.Execute();
                    if (!range.Find.Found) return;
                    **
                } while (range.Find.Found && !cancellationToken.IsCancellationRequested);
            });

            return Results;

    }
}

我的问题很简单,如果 UI 线程通过互操作方法保持忙碌,如何允许按钮保持运行状态?或者只是 VSTO 的限制或者我的代码有问题?

c# wpf vsto office-interop office-addins
3个回答
1
投票

如果 UI 线程通过互操作方法保持忙碌,如何允许按钮保持运行状态?

简短的回答:你不能。如果 UI 线程一直忙于执行大量 UI 更新,那么它就无法正确响应。

唯一真正的答案是不要过多地中断 UI 线程。我会考虑批量更新,并且应用更新的频率不要超过每 100 毫秒一次。我有一个

ObservableProgress
可能有助于计时。


1
投票

每当您在主线程上运行代码时,请确保该线程正在发送 Windows 消息,

await
操作员依赖它。但真正的解决方案是避免在辅助线程上使用 Word 对象。

        public static void DoEvents(bool OnlyOnce = false)
        {
            MSG msg;
            while (PeekMessage(out msg, IntPtr.Zero, 0, 0, 1/*PM_REMOVE*/))
            {
                TranslateMessage(ref msg);
                DispatchMessage(ref msg);
                if (OnlyOnce) break;
            }
        }

       [StructLayout(LayoutKind.Sequential)]
        public struct POINT
        {
            public int X;
            public int Y;
            public POINT(int x, int y)
            {
                this.X = x;
                this.Y = y;
            }
            public static implicit operator System.Drawing.Point(POINT p)
            {
                return new System.Drawing.Point(p.X, p.Y);
            }
            public static implicit operator POINT(System.Drawing.Point p)
            {
                return new POINT(p.X, p.Y);
            }
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct MSG
        {
            public IntPtr hwnd;
            public uint message;
            public UIntPtr wParam;
            public IntPtr lParam;
            public int time;
            public POINT pt;
            public int lPrivate;
        }
        [DllImport("user32.dll")]
        static extern sbyte GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
        [DllImport("user32.dll")]
        static extern bool TranslateMessage([In] ref MSG lpMsg);
        [DllImport("user32.dll")]
        static extern IntPtr DispatchMessage([In] ref MSG lpmsg);
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg);

0
投票

我基于 stackoverflow 上的几篇文章构建的解决方案是将 System.Threading.Tasks.Task.Run 与在主线程中打开进度表单相结合,并让正在运行的线程响应用户触发的任务取消按下(非阻塞)进度表。

其工作原理如下:

在主线程中启动长时间运行的任务,如下所示:

ThreadWriterTaskWithCancel(AddressOf _LoadData)

ThreadWriterTaskWithCancel 如下:

Protected Function ThreadWriterTaskWithCancel(mytask As TaskDelegateWithCancel) As Boolean

    userCancelled = False
    Dim ts As New CancellationTokenSource
    Dim tsk As Task(Of Boolean) = Nothing
    Try
        tsk =
            System.Threading.Tasks.Task.Run(Of Boolean)(
                Function()
                    'Thread.CurrentThread.Priority = ThreadPriority.Highest
                    ' Run lenghty task
                    Dim userCancelled As Boolean = mytask(ts.Token)

                    ' Close form once done (on GUI thread)
                    If progressBarFrm.Visible Then progressBarFrm.Invoke(New System.Action(Sub() progressBarFrm.Close()))

                    Return userCancelled
                End Function, ts.Token)

        ' Show the form - pass the task cancellation token source that is also passed to the long-running task
        progressBarFrm.ShowForm(ts, True)

    Catch ex As Exception
        WorkerErrorLog.AddLog(ex, Err)
    Finally
        ts.Dispose()
    End Try

在进度栏中,这是单击 X 时触发的代码(我不完全理解这一点,但它可以工作:-)):

    Private Sub UserFormProgressBar_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
    ' only cancel the long running task when the closing is triggered by the user pressing the X
    Dim closedByUser As Boolean = Not (New StackTrace().GetFrames().Any(Function(x) x.GetMethod().Name = "Close"))
    If closedByUser Then
        If _ts IsNot Nothing Then
            _ts.Cancel()
        Else
            e.Cancel = True
        End If
    End If
End Sub

_ts 是通过Thread函数传入的任务取消令牌源

_ts.cancel 将触发任务取消请求。让长时间运行的任务在其处理的文档循环中监视它:

        For Each file As Document In documentList
            Try
                ' do some processing
                ct.ThrowIfCancellationRequested()

            Catch ex As OperationCanceledException
                userCancelled = True
                Return userCancelled
            End Try

            fileCount += 1
            ProgressBar.BeginInvoke(Sub() ProgressBar.Value = pctProgress
        Next

因为长时间运行的任务和进度表单之间的交互是在两个不同的线程中,所以使用以下构造:

ProgressBar.BeginInvoke(Sub() ProgressBar.Value = pctProgress

请注意,如果您添加线程任务,主线程可能会被锁定:

        Application.System.Cursor = WdCursorType.wdCursorWait
        Application.ScreenUpdating = False

但如果不需要那就别费心了。

这工作起来非常稳定且可重复,但线程操作比在主线程(与 UI 相同的线程)中运行时要慢。大约慢一半。我还没有破解...

运行时显示:

© www.soinside.com 2019 - 2024. All rights reserved.