Background Operations
Converting a folder of contracts, merging a batch of letters or printing a long report can take minutes, and none of it needs the window. Run such work on threads of the thread pool with ServerTextControl, and the WPF window keeps responding: the user sees progress, can cancel and goes on editing. The patterns below build on the threading model of Dispatcher and UI Thread Access and show where threads stop helping.
Choose Where the Work Runs
| Work | Where it runs | Page |
|---|---|---|
| A change the user makes or watches in the editor | The UI thread, in the WPF TextControl | Large Documents |
| One long task, such as a PDF export or a conversion | One background thread with one ServerTextControl | Dispatcher and UI Thread Access |
| Many independent files | Several background threads, each with its own ServerTextControl | Convert Many Files With Progress and Cancel |
| A merge with many records | A background thread with ServerTextControl and MailMerge | Mail Merge Performance |
| Hundreds or thousands of merges | Several processes | Mail Merge Performance |
A ServerTextControl is made for the background: it has no user interface, and Text Control names support for several threads, better performance and a smaller memory footprint as its advantages over a visible control. Every thread creates, initializes and disposes its own instance.
Convert Many Files With Progress and Cancel
The following window converts all DOCX files of the folder Documents to PDF on up to two threads at a time, shows the progress and stops when the user clicks Cancel. The buttons and the progress bar sit above the editor in MainWindow.xaml:
<Window x:Class="SampleApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:WPF="clr-namespace:TXTextControl.WPF;assembly=TXTextControl.WPF"
Title="MainWindow" Height="450" Width="800">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<Button x:Name="convertFolderButton" Content="Convert Folder" Click="ConvertFolder_Click" />
<Button x:Name="cancelButton" Content="Cancel" Click="Cancel_Click" />
<ProgressBar x:Name="progressBar1" Width="200" Height="16" />
</StackPanel>
<WPF:TextControl x:Name="textControl1" />
</DockPanel>
</Window>
The handlers go into MainWindow.xaml.cs:
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using TXTextControl;
private CancellationTokenSource cancellation;
private async void ConvertFolder_Click(object sender, RoutedEventArgs e)
{
string[] files = Directory.GetFiles("Documents", "*.docx");
int converted = 0;
progressBar1.Maximum = files.Length;
progressBar1.Value = 0;
convertFolderButton.IsEnabled = false;
// Created on the UI thread, so its handler runs there
IProgress<int> progress = new Progress<int>(count => progressBar1.Value = count);
cancellation = new CancellationTokenSource();
ParallelOptions options = new ParallelOptions
{
MaxDegreeOfParallelism = 2,
CancellationToken = cancellation.Token
};
try
{
await Task.Run(() => Parallel.ForEach(files, options, file =>
{
// One instance per file, never shared between threads
using (ServerTextControl tx = new ServerTextControl())
{
tx.Create();
tx.Load(file, StreamType.WordprocessingML);
tx.Save(System.IO.Path.ChangeExtension(file, ".pdf"), StreamType.AdobePDF);
}
progress.Report(Interlocked.Increment(ref converted));
}));
}
catch (OperationCanceledException)
{
MessageBox.Show(this, $"Canceled after {converted} documents.");
}
catch (AggregateException exception)
{
// Parallel.ForEach collects the exceptions of its threads
MessageBox.Show(this, exception.InnerExceptions[0].Message, "Conversion failed");
}
finally
{
convertFolderButton.IsEnabled = true;
cancellation.Dispose();
cancellation = null;
}
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
cancellation?.Cancel();
}
The code works as follows:
Parallel.ForEachconverts several files at the same time, at most as many asMaxDegreeOfParallelismallows. It runs insideTask.Run, so the dispatcher stays free while the loop waits for its threads.- Every iteration creates, initializes and disposes its own
ServerTextControl. Load reads the DOCX file, and Save writes it with StreamTypeAdobePDF. Interlocked.Incrementcounts the finished files safely from several threads, andProgress<int>passes each count to the UI thread, where theProgressBarmay be changed.Cancel_Clickruns on the UI thread while the conversion goes on, because the dispatcher is free.CancelmakesParallel.ForEachstart no further files and end with anOperationCanceledException; files that have started are finished first.- An exception in any file, such as a FilterException for a file that can't be loaded, ends the loop.
Parallel.ForEachcollects it in anAggregateException, whichawaitthrows again on the UI thread. To convert the other files anyway, catchFilterExceptioninside the loop body instead.
How Far Threads Take You
Several threads don't make document processing proportionally faster. TX Text Control is thread safe because it protects resources that all its instances in a process share with critical sections, code that one thread runs at a time. Threads that need such a resource at the same time wait for each other: safe, but slower for specific applications, as Text Control puts it.
- Measure the number of threads. Text Control publishes no fixed value. Start with a small
MaxDegreeOfParallelism, raise it as long as the total time goes down and keep the value where it stops improving. - Use processes for large batches. For hundreds or thousands of merges, Text Control recommends several processes instead of several threads, and states that its approach applies to WPF applications as well. Merge Large Batches in Separate Processes describes it.
- Set cultures explicitly. MailMerge.MergeCulture formats numbers and dates and defaults to the culture of the current thread. A thread of the thread pool may have another culture than the one your window uses, so set it in each merge that runs in the background.
Print in the Background
Printing a long document is background work as well: a ServerTextControl loads the document and prints it on another thread. Print a File in the Background in Printing shows the code.
Next Steps
- Dispatcher and UI Thread Access — which objects belong to which thread.
- Mail Merge Performance — merge in the background with progress, and in separate processes.
- Exceptions and Error Handling — catch errors of background work.
- Memory Usage and Resource Disposal — dispose every instance a thread creates.