简体   繁体   中英

VSTO Async/Await - unable to cancel a long running operation

I'm developing a search tool for Word in C# with VSTO and WPF (MVVM).

I'm using the Microsoft.Office.Interop.Word.Find() method and iterating through the document to find matches. Some of the document I need to process are in excess of 300,000 characters and therefore the search can take 10+ seconds. I would like to give my users the option to cancel the operation.

The problem I'm facing is that the button, to cancel the long running operation, is inaccessible as the UI/Main thread is kept busy due to the Find operation triggering the marshalling back to the Main Thread - the relay command is never triggered. My data bindings are correct and have tested the button using a long running operation that didn't use the UI/Main thread.

public class SmartFindViewModel : 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<SmartFindResultViewModel>> 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;

    }
}

My question is simply, how could one allow a button to remain operational if the UI Thread is kept busy by an interop method? Or is just a limitation of VSTO or something wrong with my code?

Whenever your run the code on the main thread, make sure the thread is pumping Windows messages, await operators rely on it. But the real solution would be to avoid using Word objects on a secondary thread.

        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);

how could one allow a button to remain operational if the UI Thread is kept busy by an interop method?

Short answer: you can't. If the UI thread is kept busy doing tons of UI updates, then it can't also be properly responsive.

The only real answer is to not interrupt the UI thread as much. I would look into batching the updates and not applying updates more often than, say, every 100ms. I have an ObservableProgress that may help with the timing aspect.

The solution I have constructed, based on several posts here on stackoverflow is to combine System.Threading.Tasks.Task.Run with opening a progress form in the main thread and have the running thread respond to a task cancellation being triggered when the user presses the (non-blocked) progress form.

It works as follows:

Initiate the long running task in the main thread as follows:

ThreadWriterTaskWithCancel(AddressOf _LoadData)

ThreadWriterTaskWithCancel is the following:

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

In the progress bar this is the code that is triggered when the X is clicked on it (I don't fully understand this but it works :-) ):

    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 is the task cancellation token source passed in via the Thread function

_ts.cancel will trigger a task cancellation request. Let the long-running task monitor it in it's looping of the documents it processes:

        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

Because the interaction between the long-running task and the progress form is in 2 different threads, use the following construct:

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

Note that the main thread can be locked if you add in your threaded task:

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

But if that is not needed then don't bother.

This works very stable and repeatable but the threaded operation is slower then when it runs in the main thread (same thread as the UI). Roughly half as slow. I have not yet cracked that ...

when running this is shown: 在此处输入图像描述

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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