Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF RichTextBox Document Creation Threading Issue

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;
    }
like image 633
jasper Avatar asked Oct 12 '22 08:10

jasper


1 Answers

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);
});
like image 60
Jon Mitchell Avatar answered Oct 15 '22 10:10

Jon Mitchell