5

I'm having a bit of an issue with a RTB and document generation in regards to threads.

When the TextChanged event fires on the RTB, a new thead is created, and the document generation is offloaded to this. This can take a couple of seconds, with blocking calls, so it really needs to be on another thread to keep the UI responsive.

The problem I'm having is an exception when I try to add the newly generated document to the Document property of the RTB. ( The calling thread cannot access this object because a different thread owns it.) This is not due to forgetting to use Dispatcher.Invoke, as thats where the exception is generated, but because I'm creating the FlowDocument/Paragraph/Run instances on a thread other than the UI thread(I think??).

Is there a way to achieve what I'm looking for here?

Update

    private void rtbQuery_TextChanged(object sender, TextChangedEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Requires update; on thread:" + System.Threading.Thread.CurrentThread.ManagedThreadId);

        backgroundWorker.RunWorkerAsync();
    }

    private void backgroundWorker_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Generating; on thread:" + System.Threading.Thread.CurrentThread.ManagedThreadId);

        DocumentGenerator dgen = new DocumentGenerator();
        string queryText = getQueryText();

        e.Result = dgen.GenerateDocument(queryText);
    }

    private void backgroundWorker_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Assigning; on thread:" + System.Threading.Thread.CurrentThread.ManagedThreadId);

        FlowDocument doc = (FlowDocument)e.Result;
        txtQuery.Document = doc; // ! The calling thread cannot access this object because a different thread owns it
    }

>Requires update; on thread:9
>Generating; on thread:10
>Assigning; on thread:9

Update #2 - A solution

(of sorts)

So, as @Jon Mitchell pointed out, I cant update my RTB on the UI thread, with an object created on another thread. There is a very simple solution, that requires minimal code change, to work around this though, and i'm posting it up to save future people the hassle. Briefly explained, an object graph is created on the other thread, and then converted to XAML. The UI thread then converts this XAML back to the object graph, in its own thread, and everything works a-ok.

    private void backgroundWorker_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
    {
        DocumentGenerator dgen = new DocumentGenerator();
        string queryText = getQueryText();
        
        dgen.GenerateDocument(queryText); // start generation
        e.Result = dgen; // note, i'm passing the generator, not the document
    }

    private void backgroundWorker_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
    {
        DocumentGenerator dgen = (DocumentGenerator)e.Result;
        txtQuery.Document = dgen.GetFlowDocument(); 
    }

In DocumentGenerator class

    public void GenerateDocument(string data)
    {
        ... // build up the document DOM

        // return documentDOM; // used to return the generated item here.
        documentXAML = System.Windows.Markup.XamlWriter.Save(documentDOM); // serialize the DOM to XAML
    }
    public FlowDocument GetDocument()
    {
        object result = System.Windows.Markup.XamlReader.Parse(documentXAML); // build DOM from XAML
        return (FlowDocument)result;
    }
Community
  • 1
  • 1
jasper
  • 3,424
  • 1
  • 25
  • 46

3 Answers3

4

I think the issue is because the FlowDocument is a DependencyObject which isn't freezable and therefore can't be created on one thread and then used on a different one. I think its because when the FlowDocument is created on the other thread it has a different dispatcher, to the RTB.

A workaround for this can be found on AAron's blog:

FlowDocument doc = new FlowDocument(new Paragraph(new Run("hi")));
System.IO.MemoryStream stream = new System.IO.MemoryStream();
XamlWriter.Save(doc, stream);
stream.Position = 0;
      
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ZeroDelegate)delegate ()
{
    flowDoc = (FlowDocument)XamlReader.Load(stream);
});
Martin Schneider
  • 14,263
  • 7
  • 55
  • 58
Jon Mitchell
  • 3,349
  • 5
  • 27
  • 37
  • ahhh.... thats for the link. that is a pain, but i'd be happy enough with the XAML method. appreciate all the help. hard earned points ;) – jasper Apr 08 '11 at 11:15
0

I was looking for a solution to the same problem and eventually came up with an alternative solution.

The trick is to:

  1. Create FlowDocument and all inner elements on the background thread
  2. Using reflection change the Dispatcher of the FlowDocument and all inner elements possibly including styles and resources to the UI dispatcher.
  3. Use the FlowDocument in the UI

The solution keeps all the work in the background thread and does not involve background thread serialization and UI thread deserialization which will improve responsiveness even more.

Please find the code in my blog post.

[Edit] A much needed note about this solution: it basically is a hack and one has yet to solve an issue of handling images in FlowDocument as images need to be handled on a foreground (UI) thread which seems to be a limitation of .Net itself.

For the project I worked on which required foreground report generation I decided to handle as much as possible on background thread and sacrifice some GUI responsiveness for a time of building FlowDocument (about 20% of total report preparation time).

too
  • 3,009
  • 4
  • 37
  • 51
0

try this:

    private void backgroundWorker_RunWorkerCompleted(object sender,
        System.ComponentModel.RunWorkerCompletedEventArgs e)    
    {        
        System.Diagnostics.Debug.WriteLine(
            "Assigning; on thread:" + 
            System.Threading.Thread.CurrentThread.ManagedThreadId);

        Dispatcher.BeginInvoke(new Action(
            delegate()
            {
                FlowDocument doc = (FlowDocument)e.Result;
                txtQuery.Document = doc;
            }
        ), null);
    }
obenjiro
  • 3,665
  • 7
  • 44
  • 82
  • same error as above. as mentioned, its not a Dispatcher issue, Jon seems to have tracked down the issue(check his link) – jasper Apr 08 '11 at 11:19