Passing Wpf Objects Between Threads (With Source Code)
When working on yaTImer's new report engine I got myself into a bit of a problem, I'm blogging about it because I couldn't find an answer on the web and I can't believe I'm the only one with this problem, so I hope someone will find my solution helpful (or maybe suggest a better one).
My reports engine generates a FixedDocument that I can print or show to the user, because the report can contain a lot of information generating the document can potentially take some time, so I went for the easy solution and dropped a BackgroundWorker into the code.
So this:
void Window_Loaded(object sender, RoutedEventArgs e)
{
_documentViewer.Document = GenerateDocument();
}
Becomes this:
// WARNING: THIS CODE DOESN'T WORK
private BackgroundWorker _backgroundWorker;
private FixedDocument _result;
void Window_Loaded(object sender, RoutedEventArgs e)
{
_backgroundWorker = new BackgroundWorker();
_backgroundWorker.DoWork += DoGenerateDocument;
_backgroundWorker.RunWorkerCompleted += FinishedGenerating;
_backgroundWorker.RunWorkerAsync();
}
void DoGenerateDocument(object sender, DoWorkEventArgs e)
{
_result = GenerateDocument(); // this line throws an exception
}
void FinishedGenerating(object sender, RunWorkerCompletedEventArgs e)
{
_documentViewer.Document = _result;
}
While the code was very elegant it doesn't work, I got an exception with a very nice error message: " The calling thread must be STA, because many UI components require this."
Fortunately the error message is very clear and it's easy to find the solution on the web, you can't use Wpf objects from a thread-pool thread (or from a BackgroundWorker) you have to create your own thread, the code to create the thread is very simple:
Thread _backgroundThread;
_backgroundThread = new Thread(DoGenerateDocument);
_backgroundThread.SetApartmentState(ApartmentState.STA);
_backgroundThread.Start();
Now because we no longer have BackgroundWorker events we have to write our own code to pass data between threads, this is easy to do with the Dispacher class
// WARNING: THIS CODE DOESN'T WORK
private Thread _backgroundThread;
void Window_Loaded(object sender, RoutedEventArgs e)
{
_backgroundThread = new Thread(DoGenerateDocument);
_backgroundThread.SetApartmentState(ApartmentState.STA);
_backgroundThread.Start();
}
void DoGenerateDocument()
{
FixedDocument result = GenerateDocument();
Dispatcher.BeginInvoke(
DispatcherPriority.Normal,
(Action<FixedDocument>
)FinishedGenerating,
result);
}
void FinishedGenerating(FixedDocument result)
{
_documentViewer.Document = result; // now this line throws an exception
}
This code is written inside a Wpf window, and the Window class has a Dispacher property that returns the dispatcher for the thread that created the window, if you're code doesn't use a window then you need to get the dispatcher associated with the thread you want to communicate with, this is done by reading Dispacher.CurrentDispacher from that thread.
But this code also doesn't work, when I pass the FixedDocument into the document viewer I get this lovely exception: "The calling thread cannot access this object because a different thread owns it."
This is where things get complicated, I couldn't find any way to marshal the document into the UI thread.
Since this is a fixed document the natural thing to do is to save it into an XPS file (XPS is the Wpf version of PDF) and then read it from the other thread, I've tried doing this with a MemoryStream (I don't want to create an actual file) and I discovered a problem with this approach – apparently you can't read an XPS from a memory stream, you need an actual file.
At that point I got the clever idea of using XAML, you can read XAML from a memory stream and the code to read and write XAML is actually simpler then the XPS code, here is the final code using XamlReader and XamlWriter:
private Thread _backgroundThread;
void Window_Loaded(object sender, RoutedEventArgs e)
{
_backgroundThread = new Thread(DoGenerateDocument);
_backgroundThread.SetApartmentState(ApartmentState.STA);
_backgroundThread.Start();
}
void DoGenerateDocument()
{
FixedDocument result = GenerateDocument();
MemoryStream stream = new MemoryStream();
XamlWriter.Save(result, stream);
Dispatcher.BeginInvoke(
DispatcherPriority.Normal,
(Action<MemoryStream>
)FinishedGenerating,
stream);
}
void FinishedGenerating(MemoryStream stream)
{
stream.Seek(0, SeekOrigin.Begin);
FixedDocument result = (FixedDocument)XamlReader.Load(stream);
_documentViewer.Document = result;
}
This trick should use with any Wpf object, not just FixedDocument.