TX Spell .NET for Windows Forms

Spelling Correction Modes and UI Provider

Topic

The spell check dialog is one way to correct words; a panel docked beside the editor, a correction bar or a dialog in your own design are others. For those, TX Spell .NET provides the SpellingCorrectionUIProvider. It keeps track of the word being corrected, its suggestions and which commands make sense for it, and your controls bind to these values with Windows Forms data binding. You write the layout; the provider decides when Change is enabled and what the list of suggestions holds.

What the Provider Does

You get the provider from the SpellingCorrectionUIProvider property of the TXSpellChecker. It works through the misspelled words of a TextControl or a text, one at a time, and it always knows the state of the correction: whether there is a word to correct, whether suggestions were found, whether the user is editing the word. This state is its mode.

Five properties of the provider split the work. Each returns an object of a nested class with a different name:

PropertyReturnsHolds
StateManagingStateManagerThe connected control, the current word and the mode
CorrectionHandlingCorrectionHandlerThe commands, such as Change and Ignore, and whether each is enabled
SuggestionsHandlingSuggestionsHandlerThe suggestions for the current word
SuggestionDictionariesHandlingSuggestionDictionariesHandlerThe dictionaries the suggestions come from
PreviewHandlingPreviewHandlerThe position of the sentence that contains the current word

Code uses the property names: txSpellChecker1.SpellingCorrectionUIProvider.CorrectionHandling.Change(...). The five objects raise PropertyChanged when one of their values changes, which is what data binding needs. Every change of the current word updates the mode, the suggestions and the enabled states, and bound controls follow without an event handler of yours.

The Correction Modes

Mode returns a value of TXSpell.SpellingCorrectionMode:

ModeMeaning
InitialStateNo TextControl and no text is connected. This is the state before the first SetCurrent, and after Cancel or SetCurrent with null
NoWordsToCorrectFoundThe connected control or text has no words to correct
NoWordToCorrectSelectedSetCurrent with a text position found no word to correct at that position; CurrentWordToCorrect is null
SuggestionsCreatedThere is a word to correct, and suggestions were created for it
NoSuggestionsCreatedThere is a word to correct, but no suggestions were found
WordToCorrectIsDuplicateThe word to correct repeats the word in front of it
WordToCorrectEditingThe user is editing the word by hand, for example in a preview
OptionsSettingsThe options are being changed, for example in the options dialog
CorrectionCompletedEvery word was handled; nothing is left to correct

The mode decides which commands of the CorrectionHandler are enabled:

CommandEnabled in
Change, ChangeAllSuggestionsCreated, WordToCorrectEditing
Delete, IgnoreSuggestionsCreated, NoSuggestionsCreated, WordToCorrectIsDuplicate
IgnoreAllSuggestionsCreated, NoSuggestionsCreated
AddToDictionarySuggestionsCreated, NoSuggestionsCreated, when an editable user dictionary of the word's language exists

A duplicated word can be deleted or ignored but not changed: there is nothing to replace it with. Each command has a matching property, such as IsChangeEnabled, IsIgnoreEnabled and IsDeleteEnabled, to bind the Enabled property of a button to.

Build a Correction Panel

This form has a TextControl, and to its right a list of suggestions with three buttons: Change, Ignore and Delete. It creates its controls in code and has its own Main method, so in a new Windows Forms project with the TX Text Control and TX Spell .NET packages it replaces the generated Form1.cs, Form1.Designer.cs and Program.cs:

using System;
using System.ComponentModel;
using System.Windows.Forms;
using TXTextControl;
using TXTextControl.Proofing;

public class Form1 : Form
{
    private readonly TextControl textControl1 = new TextControl();
    private readonly TXSpellChecker txSpellChecker1 = new TXSpellChecker();
    private readonly ListBox listBoxSuggestions = new ListBox();

    // System.Windows.Forms.Button, because TXTextControl has a Button class, too
    private readonly System.Windows.Forms.Button buttonChange = new System.Windows.Forms.Button { Text = "Change" };
    private readonly System.Windows.Forms.Button buttonIgnore = new System.Windows.Forms.Button { Text = "Ignore" };
    private readonly System.Windows.Forms.Button buttonDelete = new System.Windows.Forms.Button { Text = "Delete" };

    public Form1()
    {
        Text = "Spelling Correction";
        Width = 800;
        Height = 500;

        FlowLayoutPanel panel = new FlowLayoutPanel { Dock = DockStyle.Right, Width = 200, FlowDirection = FlowDirection.TopDown };
        listBoxSuggestions.Width = 180;
        listBoxSuggestions.Height = 200;
        panel.Controls.AddRange(new Control[] { listBoxSuggestions, buttonChange, buttonIgnore, buttonDelete });

        textControl1.Dock = DockStyle.Fill;
        textControl1.SpellChecker = txSpellChecker1;
        textControl1.IsSpellCheckingEnabled = true;

        Controls.Add(textControl1);
        Controls.Add(panel);

        buttonChange.Click += buttonChange_Click;
        buttonIgnore.Click += buttonIgnore_Click;
        buttonDelete.Click += buttonDelete_Click;
        Load += Form1_Load;
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        textControl1.Text = "This is a sampke text with a mistkae.";

        SpellingCorrectionUIProvider provider = txSpellChecker1.SpellingCorrectionUIProvider;

        // Select each new word to correct in the editor
        provider.StateManaging.PropertyChanged += StateManaging_PropertyChanged;

        // Connect the editor: its first misspelled word becomes the word to correct
        provider.StateManaging.SetCurrent(textControl1);

        // The list shows the suggestions for the current word
        listBoxSuggestions.DataBindings.Add(new Binding("DataSource", provider.SuggestionsHandling, "Suggestions"));
        listBoxSuggestions.DataBindings.Add(new Binding("Enabled", provider.SuggestionsHandling, "IsSuggestionListEnabled"));

        // Each button is enabled when its command makes sense
        buttonChange.DataBindings.Add(new Binding("Enabled", provider.CorrectionHandling, "IsChangeEnabled"));
        buttonIgnore.DataBindings.Add(new Binding("Enabled", provider.CorrectionHandling, "IsIgnoreEnabled"));
        buttonDelete.DataBindings.Add(new Binding("Enabled", provider.CorrectionHandling, "IsDeleteEnabled"));
    }

    private void StateManaging_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName != "CurrentWordToCorrect") return;

        if (txSpellChecker1.SpellingCorrectionUIProvider.StateManaging.CurrentWordToCorrect is MisspelledWord word)
        {
            word.Select();
        }
        else
        {
            textControl1.Select(0, 0);
        }
    }

    private void buttonChange_Click(object sender, EventArgs e) =>
        txSpellChecker1.SpellingCorrectionUIProvider.CorrectionHandling.Change(listBoxSuggestions.Text);

    private void buttonIgnore_Click(object sender, EventArgs e) =>
        txSpellChecker1.SpellingCorrectionUIProvider.CorrectionHandling.Ignore();

    private void buttonDelete_Click(object sender, EventArgs e) =>
        txSpellChecker1.SpellingCorrectionUIProvider.CorrectionHandling.Delete();

    [STAThread]
    private static void Main()
    {
        Application.EnableVisualStyles();
        Application.Run(new Form1());
    }
}

The code works as follows:

  • SetCurrent connects the TextControl. Its first misspelled word becomes CurrentWordToCorrect, a MisspelledWord. If the text has none, the mode becomes NoWordsToCorrectFound. Another overload takes a string, for text that is not in a TextControl.
  • The first binding fills the list with Suggestions. The others bind the Enabled property of each control to the provider, so a button is grayed out whenever its command is not available in the current mode.
  • PropertyChanged of the StateManager reports each new word; the handler selects it, so the user sees which word the panel is about.
  • Change, Ignore and Delete handle the current word, and the provider moves on to the next one. After the last word, the mode is CorrectionCompleted and all buttons are disabled.

Add More Commands and Settings

The SpellingCorrectionUIProvider Advanced sample of the setup, in the folder TextControl\SpellingCorrectionUIProvider\SpellingCorrectionUIProvider Advanced, extends this panel with four features: commands for every occurrence of a word, a choice of the dictionary, a preview of the sentence and a box to correct the word by hand. Each binds to other members of the provider. The code below adds them to the Form1 class above; declare each new control as a field and add it to the FlowLayoutPanel in the constructor, as the panel does with the first three buttons.

Change All, Ignore All and Add to Dictionary

Three more buttons handle the current word and every other occurrence of it. Their Enabled properties bind to IsChangeAllEnabled, IsIgnoreAllEnabled and IsAddToDictionaryEnabled, and their Click events call the commands:

// In Form1_Load
buttonChangeAll.DataBindings.Add(new Binding("Enabled", provider.CorrectionHandling, "IsChangeAllEnabled"));
buttonIgnoreAll.DataBindings.Add(new Binding("Enabled", provider.CorrectionHandling, "IsIgnoreAllEnabled"));
buttonAddToDictionary.DataBindings.Add(new Binding("Enabled", provider.CorrectionHandling, "IsAddToDictionaryEnabled"));

// Methods of Form1
private void buttonChangeAll_Click(object sender, EventArgs e) =>
    txSpellChecker1.SpellingCorrectionUIProvider.CorrectionHandling.ChangeAll(listBoxSuggestions.Text);

private void buttonIgnoreAll_Click(object sender, EventArgs e) =>
    txSpellChecker1.SpellingCorrectionUIProvider.CorrectionHandling.IgnoreAll();

private void buttonAddToDictionary_Click(object sender, EventArgs e) =>
    txSpellChecker1.SpellingCorrectionUIProvider.CorrectionHandling.AddToDictionary();

Connect the three handlers to the Click events in the constructor. AddToDictionary adds the word to an editable user dictionary; see User Dictionaries.

Choose the Dictionary of the Suggestions

When several dictionaries of the word's language are loaded, the user can pick the one the suggestions come from. SuggestionDictionaries holds the dictionaries in the language of the current word, and IsSuggestionDictionariesEnabled says whether it provides any. A ComboBox binds to both:

// In Form1_Load
comboBoxDictionaries.DataBindings.Add(new Binding("Enabled", provider.SuggestionDictionariesHandling, "IsSuggestionDictionariesEnabled"));
comboBoxDictionaries.DataBindings.Add(new Binding("DataSource", provider.SuggestionDictionariesHandling, "SuggestionDictionaries"));
comboBoxDictionaries.SelectedIndexChanged += comboBoxDictionaries_SelectedIndexChanged;

// A method of Form1
private void comboBoxDictionaries_SelectedIndexChanged(object sender, EventArgs e)
{
    if (comboBoxDictionaries.SelectedItem is TXTextControl.Proofing.Dictionary dictionary)
    {
        // Create the suggestions from the chosen dictionary only
        txSpellChecker1.SpellingCorrectionUIProvider.SuggestionDictionariesHandling.SelectedSuggestionDictionaries = new TXTextControl.Proofing.Dictionary[] { dictionary };
    }
}

SelectedSuggestionDictionaries is an array of Dictionary objects, so it can hold more than one. A change creates new suggestions and updates Mode, and the bound ListBox shows the new list without further code.

Show the Sentence of the Word

A word is often easier to correct when the user sees it in context. PreviewSentenceBounds is an int array of two values, the start and the length of the sentence that contains the current word. It is set again whenever CurrentWordToCorrect changes, and it is null unless the mode is SuggestionsCreated, NoSuggestionsCreated, WordToCorrectIsDuplicate or WordToCorrectEditing.

The array holds positions, not text. To show the sentence in a RichTextBox, bind its Text to PreviewSentenceBounds and turn the bounds into text in the Format event of the binding:

// In Form1_Load
Binding sentenceBinding = new Binding("Text", provider.PreviewHandling, "PreviewSentenceBounds");
sentenceBinding.Format += sentenceBinding_Format;
richTextBoxSentence.DataBindings.Add(sentenceBinding);

// A method of Form1
private void sentenceBinding_Format(object sender, ConvertEventArgs e)
{
    SpellingCorrectionUIProvider provider = txSpellChecker1.SpellingCorrectionUIProvider;
    int[] bounds = provider.PreviewHandling.PreviewSentenceBounds;

    if (bounds == null || !(provider.StateManaging.CurrentWordToCorrect is MisspelledWord word))
    {
        e.Value = "";
        return;
    }

    // The main text, a header, a footer or a text frame
    IFormattedText textPart = (IFormattedText)provider.StateManaging.CurrentIFormattedText;

    // MisspelledWord.Start is one-based, GetItem expects a zero-based position
    Paragraph paragraph = textPart.Paragraphs.GetItem(word.Start - 1);

    // Paragraph.Start is one-based, too
    e.Value = paragraph.Text.Substring(bounds[0] - (paragraph.Start - 1), bounds[1]);
}

The code works as follows:

  • The binding reads PreviewSentenceBounds each time the provider reports a change of it, and Format replaces the array with the text the RichTextBox shows.
  • CurrentIFormattedText is the part of the document that contains the current word, as an IFormattedText: the main text, a header or footer, or a text frame. Its type is object, so it is cast.
  • Paragraphs.GetItem returns the paragraph at a zero-based text position; the Start of the misspelled word is one-based, hence - 1.
  • The start of the bounds counts from the beginning of the text part, the way the sample of the setup uses it. Subtracting the zero-based start of the paragraph, Paragraph.Start - 1, gives the position of the sentence in Paragraph.Text, and the second value is its length.

Correct the Word by Hand

When no suggestion fits, the user types the correction into a TextBox. The text box shows the current word, is marked as being edited on the first key press and passes its text to Change:

// In Form1_Load: show the current word, but never write the box back to the provider
Binding wordBinding = new Binding("Text", provider.StateManaging, "CurrentWordToCorrect", true, DataSourceUpdateMode.Never);
wordBinding.Format += wordBinding_Format;
textBoxEdit.DataBindings.Add(wordBinding);
buttonUndoEdit.DataBindings.Add(new Binding("Enabled", provider.CorrectionHandling, "IsUndoEditEnabled"));

textBoxEdit.KeyDown += textBoxEdit_KeyDown;
textBoxEdit.TextChanged += textBoxEdit_TextChanged;
buttonUndoEdit.Click += buttonUndoEdit_Click;

// Methods of Form1
private void wordBinding_Format(object sender, ConvertEventArgs e)
{
    e.Value = txSpellChecker1.SpellingCorrectionUIProvider.StateManaging.CurrentWordToCorrect is MisspelledWord word
        ? word.Text
        : "";
}

private void textBoxEdit_KeyDown(object sender, KeyEventArgs e)
{
    SpellingCorrectionUIProvider provider = txSpellChecker1.SpellingCorrectionUIProvider;

    // Switch to the WordToCorrectEditing mode on the first key press
    if (provider.StateManaging.CurrentWordToCorrect != null)
    {
        provider.StateManaging.IsWordToCorrectEditing = true;
    }
}

private void textBoxEdit_TextChanged(object sender, EventArgs e)
{
    SpellingCorrectionUIProvider provider = txSpellChecker1.SpellingCorrectionUIProvider;

    if (provider.StateManaging.Mode != TXSpell.SpellingCorrectionMode.WordToCorrectEditing) return;

    if (provider.CorrectionHandling.CheckCorrectedWord(textBoxEdit.Text, out string message))
    {
        labelEditResult.Text = "";
    }
    else
    {
        labelEditResult.Text = message;
    }
}

private void buttonUndoEdit_Click(object sender, EventArgs e)
{
    SpellingCorrectionUIProvider provider = txSpellChecker1.SpellingCorrectionUIProvider;

    // Leave the editing mode and show the misspelled word again
    provider.StateManaging.IsWordToCorrectEditing = false;

    if (provider.StateManaging.CurrentWordToCorrect is MisspelledWord word)
    {
        textBoxEdit.Text = word.Text;
    }
}

Change and Change All now take the typed word while the user edits, and the selected suggestion otherwise. Replace the Click handler of Change from the panel, and write the one of Change All the same way with ChangeAll:

private void buttonChange_Click(object sender, EventArgs e)
{
    SpellingCorrectionUIProvider provider = txSpellChecker1.SpellingCorrectionUIProvider;

    string correction = provider.StateManaging.IsWordToCorrectEditing
        ? textBoxEdit.Text
        : listBoxSuggestions.Text;

    provider.CorrectionHandling.Change(correction);
}

The code works as follows:

  • The binding of the text box uses DataSourceUpdateMode.Never: the provider fills the box, and typing never writes back to CurrentWordToCorrect. Its Format event turns the MisspelledWord into its text.
  • Setting IsWordToCorrectEditing to true switches the mode to WordToCorrectEditing, in which Change and Change All are enabled. It can only be set while there is a word to correct.
  • CheckCorrectedWord returns true when the typed word is in one of the selected suggestion dictionaries. Otherwise it returns false and a message for the user in its out parameter. It works in the modes SuggestionsCreated and WordToCorrectEditing.
  • IsUndoEditEnabled is true in the WordToCorrectEditing mode, so Undo Edit is only enabled while the user edits. Its handler ends the editing and restores the misspelled word in the box.

Limit the Number of Suggestions

MaxSuggestions limits the list of suggestions; the default is 10.

Build a Complete Spell Check Dialog

The third sample of the setup, Custom SpellCheckDialog, builds a full spell check dialog on the provider, with the same parts as the dialog of TX Spell .NET:

The Spelling dialog of TX Spell .NET, whose parts the custom dialog rebuilds: a preview of the sentence, a list of suggestions, a dictionary choice and the buttons Ignore Once, Ignore All, Add To Dictionary, Change, Change All, Options and Cancel.

Such a dialog adds three rules to the panel above:

  1. Open it only when there is something to correct. Call SetCurrent first and read Mode. If it is NoWordsToCorrectFound, show a message instead of the dialog. Either way, reset the provider with Cancel afterward, which returns it to InitialState.
  2. Switch buttons by mode. In WordToCorrectEditing, the Ignore button becomes Undo Edit. For WordToCorrectIsDuplicate, Change becomes Delete and calls Delete.
  3. Protect ignored words during the options dialog. Set IsOptionsSettings to true before you open the options dialog with OptionsDialog: this keeps the words ignored so far. Set it back to false when the dialog closes; the TextControl is then checked again with the new settings, and the stored ignored words are reset.

Spell Check Dialog describes the ready-made dialog, which is often enough, and Spell Checking Examples lists the three samples.

Next Steps