TX Text Control .NET for WPF

Background Operations

Topic

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

WorkWhere it runsPage
A change the user makes or watches in the editorThe UI thread, in the WPF TextControlLarge Documents
One long task, such as a PDF export or a conversionOne background thread with one ServerTextControlDispatcher and UI Thread Access
Many independent filesSeveral background threads, each with its own ServerTextControlConvert Many Files With Progress and Cancel
A merge with many recordsA background thread with ServerTextControl and MailMergeMail Merge Performance
Hundreds or thousands of mergesSeveral processesMail 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.ForEach converts several files at the same time, at most as many as MaxDegreeOfParallelism allows. It runs inside Task.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 StreamType AdobePDF.
  • Interlocked.Increment counts the finished files safely from several threads, and Progress<int> passes each count to the UI thread, where the ProgressBar may be changed.
  • Cancel_Click runs on the UI thread while the conversion goes on, because the dispatcher is free. Cancel makes Parallel.ForEach start no further files and end with an OperationCanceledException; 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.ForEach collects it in an AggregateException, which await throws again on the UI thread. To convert the other files anyway, catch FilterException inside 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.

Next Steps