我正在用VSTO和WPF (MVVM)在C#中开发一个Word搜索工具。
我使用Microsoft.Office.Interop.Word.Find()方法并遍历文档来查找匹配项。我需要处理的一些文档超过30万个字符,因此搜索需要花费10+秒。我想给我的用户取消操作的选项。
我面临的问题是,要取消长时间运行的操作,按钮是不可访问的,因为UI/主线程由于Find操作而一直处于忙碌状态,触发回主线程的编组--中继命令从未被触发。我的数据绑定是正确的,并且使用了一个不使用UI/主线程的长时间运行操作来测试按钮。
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;
}
}我的问题很简单,如果UI线程通过互操作方法保持忙碌,那么如何允许按钮保持操作呢?或者仅仅是VSTO的一个限制或者我的代码有什么问题?
发布于 2022-07-11 23:47:38
如果用户界面线程被互操作方法占用,那么
如何允许按钮保持操作呢?
简短的回答:你不能。如果UI线程一直忙于做大量的UI更新,那么它也不能正确地响应。
唯一真正的答案是不要中断UI线程。我会考虑批次更新,而不是应用更新比,比方说,每100毫秒。我有一个ObservableProgress,它可以在时间方面有所帮助。
发布于 2022-07-11 17:07:35
每当您在主线程上运行代码时,请确保线程正在释放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);发布于 2022-07-14 18:08:28
基于这里在堆栈溢出上的几个帖子,我构建的解决方案是将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是通过线程函数传入的任务取消令牌源。
_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相同的线程)时,线程操作要慢一些。大约一半的速度。我还没破解那个..。
在运行时,显示如下:

https://stackoverflow.com/questions/72941891
复制相似问题