在后台进程中访问 WPF FlowDocument
在后台访问 WPF FlowDocument
我的问题涉及在 WPF 后台访问 UI 对象。我见过几十个示例应用程序,它们都很简单、易于理解,其中 95% 告诉你如何显示进度条。这不完全是我想要的......
我的问题是这样的:我想通过访问 RichTextBox 中的 FlowDocument 来执行一项长任务(或许多长任务)。确切的任务与这里无关,但一个例子可能是扫描文档,并计算特定单词出现的次数,或者有多少个红色字符……。在一个很长的文档中,这些任务可能相当耗时,如果在前台完成,将会极大地占用 UI 并使其无响应。我只想解析 FlowDocument;我不想对其进行任何更改。
这就是我正在尝试做的事情。显而易见的解决方案是在后台执行此操作,但问题是……如何执行?我已经实现了似乎的答案,但我只是“感觉不对”,这就是我来这里寻求帮助的原因。
我的“解决方案”
接下来的我的“解决方案”使用一个BackgroundWorker,它调用UI对象的Dispatcher来确保访问正确的线程......并且它“看起来”可以完成这项工作。但真的吗?............ 我已经大大简化了我的“解决方案”,以便(我希望)能够轻松地遵循我正在做的事情......
WithEvents worker As BackgroundWorker
Private Delegate Sub DelegateSub()
Private theDocument As FlowDocument
''' <summary>
''' Triggers the background task. Can call from anywhere in main code blocks
''' </summary>
Private Sub StartTheBackgroundTask()
worker = New BackgroundWorker
worker.RunWorkerAsync()
End Sub
''' <summary>
''' In the background, hands the job over to the UI object's Dispatcher
''' </summary>
Private Sub HandleWorkerDoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs) Handles worker.DoWork
Dim priority As System.Windows.Threading.DispatcherPriority
Dim theLongRunningTask As DelegateSub
'(1) Define a delegate for the Dispatcher to work with
theLongRunningTask = New DelegateSub(AddressOf DoTheTimeConsumingTask)
'(2) Set Dispatcher priority as required
priority = System.Windows.Threading.DispatcherPriority.Background
'(3) Add the job to the FlowDocument's Dispatcher's tasks
theDocument.Dispatcher.BeginInvoke(theLongRunningTask, priority)
End Sub
''' <summary>
''' Sub whose logic accesses, but does not change, the UI object
''' </summary>
Private Sub DoTheTimeConsumingTask()
'For example......
For Each bl As Block In theDocument.Blocks
'......do something
Next
End Sub
虽然这似乎有效,但我看到的问题是,除了使用 BackgroundWorker 触发任务之外,几乎所有长时间运行的任务都是由 UI 对象的 Dispatcher 处理的。所以BackgroundWorker实际上并不做任何工作。这就是我关心的部分;如果调度员忙于完成所有工作,我看不到我如何获得任何东西
选项 2
因此,对我来说,我最好“稍微扭转一下”,这似乎更合乎逻辑” 并将 Dispatcher 的委托设置为指向实例化并启动 BackGroundWorker 的子进程(我的想法是 Dispatcher 线程将拥有 BackgroundWorker 的线程),并在 BackgroundWorker 的 DoWork 中完成所有工作 事件。那“感觉”是对的……
所以我尝试了这个:
WithEvents worker As BackgroundWorker
Private Delegate Sub DelegateSub()
Private theDocument As FlowDocument
''' <summary>
''' Triggers the background task. Can call from anywhere in main code blocks
''' </summary>
Private Sub StartTheBackgroundTask()
Dim priority As System.Windows.Threading.DispatcherPriority
Dim theTask As DelegateSub
'(1) Define a delegate for the Dispatcher to work with
theTask = New DelegateSub(AddressOf RunWorker)
'(2) Set Dispatcher priority as required
priority = System.Windows.Threading.DispatcherPriority.Normal
'(3) Add the job to the Dispatcher's tasks
theDocument.Dispatcher.BeginInvoke(theTask, priority)
End Sub
''' <summary>
''' Creates and starts a new BackGroundWorker object
''' </summary>
Private Sub RunWorker()
Worker = New BackgroundWorker
Worker.RunWorkerAsync()
End Sub
''' <summary>
''' Does the long task in the DoWork event
''' </summary>
Private Sub HandleWorkerDoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs) Handles worker.DoWork
DoTheTimeConsumingTask()
End Sub
''' <summary>
''' Sub whose logic accesses, but does not change, the UI object
''' </summary>
Private Sub DoTheTimeConsumingTask()
'For example......
For Each bl As Block In theDocument.Blocks
'......do something
Next
End Sub
对我来说,这一切似乎更合乎逻辑。我推测 Dispatcher 将拥有 BackgroundWorker,而后者将完成所有长时间的工作,并且所有内容都将在 UI 线程上进行。好吧……逻辑思维就这么多了……(对于 WPF 通常是致命的!)……事实并非如此。它会因常见的“不同线程”错误而崩溃。所以,再想一想,这个似乎是一个更优雅的解决方案,结果却是一个失败者!
那么我的问题如下:
- 我的“解决方案”是解决方案吗?
- 我哪里错了?
- 如何改进“解决方案”,使调度员不被长期任务束缚......这正是我试图避免的情况?
进一步的问题。请注意,我必须使用 FlowDocument 的调度程序才能完成这项工作。如果我使用 System.Windows.Threading.Dispatcher.CurrentDispatcher 代替,则不会调用 Delegate sub (DoTheTimeConsumingTask ),因此 – 出于所有意图和目的 – 什么也不会发生。有人可以解释一下为什么不可以吗?
我来找你并不是第一个停靠点。我已经尝试了几十个选项,但还没有找到任何感觉完全正确的选项(除了我的第二个选项不起作用,哈哈),所以我请求一些指导。
Accessing a WPF FlowDocument in the BackGround
My question relates to accessing a UI object, in the background in WPF. I have seen dozens of sample apps, that all are simple, easy to follow, and 95% of which tell you how display a progress bar. That’s not quite what I want…..
My problem is this: I want to perform a long task (or many long tasks) by accessing a FlowDocument in a RichTextBox. The exact task isn’t relevant here, but an example might be to scan through the document, and count the number of times a particular word appears, or how many red characters there are……. In a long document, those could be fairly time consuming tasks, and if done in the foreground, would tie up the UI considerably and make it unresponsive. I only want to parse the FlowDocument; I do not want to make any changes to it.
So that’s the kind of thing I’m trying to do. The obvious solution is to do it in the background, but the question is…how? I have achieved what appears to be an answer, but it just doesn’t “feel right” to me, which is why I am here asking for help.
My “Solution”
My “solution” which follows uses a BackgroundWorker which calls the UI object’s Dispatcher to ensure the right thread is accessed…and it “appears” to do the job. But does it?..............
I have considerably abbreviated my “solution” to make it easy (I hope) to follow what I am doing….
WithEvents worker As BackgroundWorker
Private Delegate Sub DelegateSub()
Private theDocument As FlowDocument
''' <summary>
''' Triggers the background task. Can call from anywhere in main code blocks
''' </summary>
Private Sub StartTheBackgroundTask()
worker = New BackgroundWorker
worker.RunWorkerAsync()
End Sub
''' <summary>
''' In the background, hands the job over to the UI object's Dispatcher
''' </summary>
Private Sub HandleWorkerDoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs) Handles worker.DoWork
Dim priority As System.Windows.Threading.DispatcherPriority
Dim theLongRunningTask As DelegateSub
'(1) Define a delegate for the Dispatcher to work with
theLongRunningTask = New DelegateSub(AddressOf DoTheTimeConsumingTask)
'(2) Set Dispatcher priority as required
priority = System.Windows.Threading.DispatcherPriority.Background
'(3) Add the job to the FlowDocument's Dispatcher's tasks
theDocument.Dispatcher.BeginInvoke(theLongRunningTask, priority)
End Sub
''' <summary>
''' Sub whose logic accesses, but does not change, the UI object
''' </summary>
Private Sub DoTheTimeConsumingTask()
'For example......
For Each bl As Block In theDocument.Blocks
'......do something
Next
End Sub
Although that seems to work, the problem as I see it is that apart from triggering the task with the BackgroundWorker, pretty much ALL the long running task is handled by the UI object’s Dispatcher. So the BackgroundWorker doesn’t actually do any work. That’s the part that concerns me; I can’t see how I am gaining anything, if the Dispatcher is tied up doing all the work
Option 2
So, it seemed more logical to me that I would be better to “twist this around a little” and set the Dispatcher’s delegate to point to the sub that instantiates and starts the BackGroundWorker (my thinking being that the Dispatcher thread would then own the BackgroundWorker’s thread), and to do all the work in the BackgroundWorker’s DoWork event. That “felt” right….
So I tried this, instead :
WithEvents worker As BackgroundWorker
Private Delegate Sub DelegateSub()
Private theDocument As FlowDocument
''' <summary>
''' Triggers the background task. Can call from anywhere in main code blocks
''' </summary>
Private Sub StartTheBackgroundTask()
Dim priority As System.Windows.Threading.DispatcherPriority
Dim theTask As DelegateSub
'(1) Define a delegate for the Dispatcher to work with
theTask = New DelegateSub(AddressOf RunWorker)
'(2) Set Dispatcher priority as required
priority = System.Windows.Threading.DispatcherPriority.Normal
'(3) Add the job to the Dispatcher's tasks
theDocument.Dispatcher.BeginInvoke(theTask, priority)
End Sub
''' <summary>
''' Creates and starts a new BackGroundWorker object
''' </summary>
Private Sub RunWorker()
Worker = New BackgroundWorker
Worker.RunWorkerAsync()
End Sub
''' <summary>
''' Does the long task in the DoWork event
''' </summary>
Private Sub HandleWorkerDoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs) Handles worker.DoWork
DoTheTimeConsumingTask()
End Sub
''' <summary>
''' Sub whose logic accesses, but does not change, the UI object
''' </summary>
Private Sub DoTheTimeConsumingTask()
'For example......
For Each bl As Block In theDocument.Blocks
'......do something
Next
End Sub
To me, that all seemed far more logical. I surmised the Dispatcher would own the BackgroundWorker, which in turn would do all the long work, and everything would be on the UI thread. Well….so much for logical thinking…(usually fatal with WPF!)....It doesn’t. It crashes with the usual “Different Thread” error. So, what seemed on second thoughts to be a far more elegant solution, turned out to be a loser!
My questions then are as follows:
- Is my “solution” a solution, or not?
- Where am I going wrong?
- How can the “solution” be improved so that the Dispatcher is not tied up with the long task ….which is exactly the situation I was trying to avoid?
A further question. Please note that I had to use the FlowDocument’s Dispatcher to make this work. If I used the System.Windows.Threading.Dispatcher.CurrentDispatcher instead, then the Delegate sub (DoTheTimeConsumingTask ) does not get invoked so – to all intents and purposes – nothing happens. Can someone explain why not, please?
I have not come to you as a first port of call. I’ve tried dozens of options, and haven’t found anything yet that feels totally right (apart from my second option that doesn’t work LOL) so I’m asking for some guidance, please.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
您面临的主要问题是
FlowDocument
派生自DispatcherObject
,因此您必须使用其Dispatcher
才能访问它。您尝试对此进行的所有操作都将采取将项目放入调度程序的工作队列中并等待它执行它们的形式。如果Dispatcher
是处理 UI 的,那么将会导致您想要避免的情况:当Dispatcher
正在执行您的工作项时,所有剩余的鼠标点击和击键都堆积在调度程序的工作队列中,并且 UI 将不负责任。从
FlowDocument
作为DispatcherObject
中您得到的是,当您的长时间运行的任务正在处理它时,它的内容无法更改。一旦您的任务完成,队列中的鼠标点击和击键可能会改变它,但在它运行时,它们只是积累。这实际上很重要;如果您能够使用Dispatcher
,那么您将面临这样的情况:当您的任务正在执行时,UI 中的某些内容更改了FlowDocument
跑步。那么你就会遇到通常所说的“问题”。即使您可以克隆
FlowDocument
并断开克隆与 UI 调度程序的连接,它仍然是一个DispatcherObject
,并且您在尝试执行时仍然会遇到相同的问题同时执行多项任务;您可以选择序列化对它的访问或观察后台线程崩溃。为了解决这个问题,您需要做的是制作某种非
DispatcherObject
的FlowDocument
冻结快照。然后在快照上运行您的任务。这样,如果实时 UI 在您的任务运行时更改FlowDocument
,它不会扰乱您的游戏。我会做什么:使用
XamlWriter
并将FlowDocument
序列化为XDocument
。序列化任务涉及到 Dispatcher,但一旦完成,您就可以根据需要对数据运行任意数量的古怪并行分析,并且 UI 中的任何内容都不会影响它。 (此外,一旦它是一个XDocument
,您就可以使用 XPath 来查询它,这是一个非常好的锤子,只要您的问题实际上是钉子。)The primary issue you're facing is that
FlowDocument
derives fromDispatcherObject
, and so you have to engage itsDispatcher
to access it. Everything you try to do with this thing is going to take the form of putting items in theDispatcher
's work queue and waiting for it to get around to executing them. Which, if theDispatcher
is the one that's handling the UI, is going to result in exactly what you're trying to avoid: while theDispatcher
is executing your work item, all the remaining mouse clicks and keystrokes are piling up in theDispatcher
's work queue, and the UI will be unresponsible.What you get out of the
FlowDocument
being aDispatcherObject
is that its content can't change while your long-running task is processing it. Those mouse clicks and keystrokes in the queue may change it once your task is done, but while it's running, they're just accumulating. This is actually kind of important; if you were able to get around using theDispatcher
, you'd face the scenario where something in the UI changed theFlowDocument
while your task was running. Then you would have what are commonly known as "problems."Even if you could clone the
FlowDocument
and disconnect the clone from the UI's dispatcher, it would still be aDispatcherObject
, and you'd still run into the same problem trying to execute multiple tasks on it simultaneously; you'd have the options of serializing your access to it or watching your background thread crash.To get around this, what you need to do is make some kind of non-
DispatcherObject
frozen snapshot of theFlowDocument
. Then run your task on the snapshot. That way, if the UI, being live, changes theFlowDocument
while your task is running, it won't mess up your game.What I'd do: use a
XamlWriter
and serialize theFlowDocument
into anXDocument
. The serialization task involves theDispatcher
, but once it's done, you can run as many wacky parallel analyses of the data as you want and nothing in the UI will affect it. (Also once it's anXDocument
you query it with XPath, which is a pretty good hammer, so long as your problems are actually nails.)