Accessing a WPF FlowDocument in a BackGround Proce

2019-05-21 04:07发布

问题:

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.

回答1:

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