UI Threading and Background Operations
A Windows Forms application has one thread that draws the form and handles every click, and the TextControl lives on it. Document work that takes seconds, such as exporting a long document or converting a folder of files, freezes the form when it runs there. The rules below decide which TX Text Control class may run on which thread, and the samples move work to the background, report progress, cancel it and bring the result back to the editor.
Which Thread Runs What
| Class | Thread | Why |
|---|---|---|
TextControl, ButtonBar, RulerBar, StatusBar, the ribbon and the sidebar | The UI thread that created the form | They are Windows Forms controls, derived from System.Windows.Forms.Control |
| ServerTextControl | Any thread, one instance per thread | A component without a window, meant for work in the background |
| MailMerge | The thread of the text component it is connected to | It works on the document of its TextComponent |
TextControl inherits the members that Windows Forms provides for threads from System.Windows.Forms.Control: InvokeRequired tells whether the caller is on another thread than the one the control was created on, and Invoke and BeginInvoke run a delegate on the control's thread. Everything else of the TextControl, from Load to Selection, is called on the UI thread only.
ServerTextControl was made for the other threads. Text Control lists support for several threads among its advantages over the visible control, together with better performance and a smaller memory footprint, because it keeps no undo history and renders nothing for the screen. Each thread creates, uses and disposes its own instance.
Load, Save and the merge methods of MailMerge are synchronous. They keep the calling thread busy until they return, so the thread you call them on decides whether the form responds in the meantime.
Save the Editor's Document in the Background
The document of the editor can't go to another thread, but a copy of it can. The following handler saves the editor's document as a byte array on the UI thread, creates the PDF in a ServerTextControl on a thread of the thread pool and returns to the form when it's done. It belongs in Form1.cs, with a button named exportPdfButton on the form:
using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
using TXTextControl;
private async void exportPdfButton_Click(object sender, EventArgs e)
{
// The editor belongs to the UI thread: copy its document here
textControl1.Save(out byte[] document, BinaryStreamType.InternalUnicodeFormat);
exportPdfButton.Enabled = false;
try
{
await Task.Run(() =>
{
using (ServerTextControl tx = new ServerTextControl())
{
if (!tx.Create())
{
throw new InvalidOperationException("TX Text Control could not be initialized.");
}
tx.Load(document, BinaryStreamType.InternalUnicodeFormat);
tx.Save("document.pdf", StreamType.AdobePDF);
}
});
MessageBox.Show(this, "document.pdf created.");
}
catch (FilterException exception)
{
MessageBox.Show(this, $"{exception.Reason}: {exception.Message}", "Export failed");
}
finally
{
// After await, the handler runs on the UI thread again
exportPdfButton.Enabled = true;
}
}
The code works as follows:
- TextControl.Save with BinaryStreamType
InternalUnicodeFormatcopies the complete document into a byte array. The user can go on editing while the copy is exported. - The lambda passed to
Task.Runuses only the byte array and its ownServerTextControl. Create returnsfalseif the instance couldn't be initialized. awaitreturns to the UI thread, so the message box and the button are used on the right thread. An exception from the task is thrown again there, and thecatchblock handles a FilterException of the export.
Handle Errors of Background Work in Exceptions describes the error handling in more detail.
Bring a Result Back to the Editor
A document created in the background goes into the editor on the UI thread. After await, the code already runs there. Code that isn't awaited, such as a callback that another component calls on its own thread, uses InvokeRequired and BeginInvoke of the TextControl:
private void ShowDocument(byte[] document)
{
// Called from another thread: repeat the call on the UI thread
if (textControl1.InvokeRequired)
{
textControl1.BeginInvoke(new Action<byte[]>(ShowDocument), document);
return;
}
textControl1.Load(document, BinaryStreamType.InternalUnicodeFormat);
}
BeginInvoke queues the call and returns at once, so the background thread doesn't wait for the editor. TextControl.Load then replaces the document of the editor on the UI thread.
Convert Many Files With Progress and Cancel
Several documents can be processed on several threads at the same time, each with its own ServerTextControl. The following handlers convert all DOCX files of the folder Documents to PDF on up to two threads, move a ProgressBar named progressBar1 and stop when the user clicks a button named cancelButton:
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using TXTextControl;
private CancellationTokenSource cancellation;
private async void convertFolderButton_Click(object sender, EventArgs e)
{
string[] files = Directory.GetFiles("Documents", "*.docx");
int converted = 0;
progressBar1.Maximum = files.Length;
progressBar1.Value = 0;
convertFolderButton.Enabled = false;
// Reports arrive on the UI thread, which created the Progress object
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 =>
{
// Each thread works with an instance of its own
using (ServerTextControl tx = new ServerTextControl())
{
tx.Create();
tx.Load(file, StreamType.WordprocessingML);
tx.Save(Path.ChangeExtension(file, ".pdf"), StreamType.AdobePDF);
}
progress.Report(Interlocked.Increment(ref converted));
}));
}
catch (OperationCanceledException)
{
MessageBox.Show(this, $"Canceled after {converted} documents.");
}
finally
{
convertFolderButton.Enabled = true;
cancellation.Dispose();
cancellation = null;
}
}
private void cancelButton_Click(object sender, EventArgs e)
{
cancellation?.Cancel();
}
The code works as follows:
Parallel.ForEachruns the loop body for several files at the same time, andMaxDegreeOfParallelismlimits how many. The whole loop runs insideTask.Run, so the UI thread stays free while it waits.- Every loop body creates, initializes and disposes its own
ServerTextControl; no instance is shared between threads. Interlocked.Incrementcounts the finished files safely from several threads, andProgress<int>hands each count to the UI thread.cancelButton_Clickruns on the UI thread while the conversion goes on.CancelmakesParallel.ForEachstart no further files and end with anOperationCanceledException; the files already started are finished first.
How Far Threads Take You
More threads don't make document processing proportionally faster. TX Text Control is thread safe because it guards resources that all instances of a process share with critical sections, code that only one thread runs at a time. Threads that need such a resource at the same time wait for each other, which Text Control describes as safe but, for specific applications, slower.
- Measure the number of threads. Text Control publishes no fixed number for desktop applications. Start with a small
MaxDegreeOfParallelism, raise it while the total time goes down and keep the value where it stops improving. - Use processes for large batches. For merges of hundreds or thousands of documents, Text Control recommends several processes instead of several threads, as Mail Merge Performance describes.
- Set cultures explicitly. MailMerge.MergeCulture formats numbers and dates, and its default is the culture of the current thread. On a thread of the thread pool, that's not necessarily the culture your form uses, so set it in each merge that runs in the background.
Next Steps
- Using ServerTextControl in Desktop Applications — the lifecycle of
ServerTextControland a first background task. - Mail Merge Performance — merge in the background with progress, and in separate processes.
- Memory Usage and Resource Disposal — dispose every instance a thread creates.
- Printing — print on a background thread.