在后台进程中访问 WPF FlowDocument

发布于 2024-11-08 14:45:31 字数 4442 浏览 0 评论 0原文

在后台访问 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 通常是致命的!)……事实并非如此。它会因常见的“不同线程”错误而崩溃。所以,再想一想,这个似乎是一个更优雅的解决方案,结果却是一个失败者!

那么我的问题如下:

  1. 我的“解决方案”是解决方案吗?
  2. 我哪里错了?
  3. 如何改进“解决方案”,使调度员不被长期任务束缚......这正是我试图避免的情况?

进一步的问题。请注意,我必须使用 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:

  1. Is my “solution” a solution, or not?
  2. Where am I going wrong?
  3. 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

べ繥欢鉨o。 2024-11-15 14:45:31

您面临的主要问题是 FlowDocument 派生自 DispatcherObject,因此您必须使用其 Dispatcher 才能访问它。您尝试对此进行的所有操作都将采取将项目放入调度程序的工作队列中并等待它执行它们的形式。如果 Dispatcher 是处理 UI 的,那么将会导致您想要避免的情况:当 Dispatcher 正在执行您的工作项时,所有剩余的鼠标点击和击键都堆积在调度程序的工作队列中,并且 UI 将不负责任。

FlowDocument 作为 DispatcherObject 中您得到的是,当您的长时间运行的任务正在处理它时,它的内容无法更改。一旦您的任务完成,队列中的鼠标点击和击键可能会改变它,但在它运行时,它们只是积累。这实际上很重要;如果您能够使用Dispatcher,那么您将面临这样的情况:当您的任务正在执行时,UI 中的某些内容更改了FlowDocument跑步。那么你就会遇到通常所说的“问题”。

即使您可以克隆 FlowDocument 并断开克隆与 UI 调度程序的连接,它仍然是一个 DispatcherObject,并且您在尝试执行时仍然会遇到相同的问题同时执行多项任务;您可以选择序列化对它的访问或观察后台线程崩溃。

为了解决这个问题,您需要做的是制作某种非 DispatcherObjectFlowDocument 冻结快照。然后在快照上运行您的任务。这样,如果实时 UI 在您的任务运行时更改 FlowDocument,它不会扰乱您的游戏。

我会做什么:使用 XamlWriter 并将 FlowDocument 序列化为 XDocument。序列化任务涉及到 Dispatcher,但一旦完成,您就可以根据需要对数据运行任意数量的古怪并行分析,并且 UI 中的任何内容都不会影响它。 (此外,一旦它是一个 XDocument,您就可以使用 XPath 来查询它,这是一个非常好的锤子,只要您的问题实际上是钉子。)

The primary issue you're facing is that FlowDocument derives from DispatcherObject, and so you have to engage its Dispatcher to access it. Everything you try to do with this thing is going to take the form of putting items in the Dispatcher's work queue and waiting for it to get around to executing them. Which, if the Dispatcher is the one that's handling the UI, is going to result in exactly what you're trying to avoid: while the Dispatcher is executing your work item, all the remaining mouse clicks and keystrokes are piling up in the Dispatcher's work queue, and the UI will be unresponsible.

What you get out of the FlowDocument being a DispatcherObject 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 the Dispatcher, you'd face the scenario where something in the UI changed the FlowDocument 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 a DispatcherObject, 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 the FlowDocument. Then run your task on the snapshot. That way, if the UI, being live, changes the FlowDocument while your task is running, it won't mess up your game.

What I'd do: use a XamlWriter and serialize the FlowDocument into an XDocument. The serialization task involves the Dispatcher, 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 an XDocument you query it with XPath, which is a pretty good hammer, so long as your problems are actually nails.)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文