Home > front end >  VSTO Async/Await - unable to cancel a long running operation
VSTO Async/Await - unable to cancel a long running operation

Time:07-12

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?

CodePudding user response:

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

CodePudding user response:

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.

  • Related