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.