TX Spell .NET for WPF

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 WPF 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 IsEnabled property of a button to.

Build a Correction Panel

This window has a TextControl, and to its right a list of suggestions with three buttons: Change, Ignore and Delete. The bindings are written in XAML, against the spell checker resource of the window:

<Window x:Class="SpellingCorrectionSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:tx="clr-namespace:TXTextControl.WPF;assembly=TXTextControl.WPF"
        xmlns:spell="clr-namespace:TXTextControl.WPF.Proofing;assembly=TXSpell.WPF"
        Title="Spelling Correction" Height="450" Width="800">
    <Window.Resources>
        <spell:TXSpellChecker x:Key="txSpellChecker1" />
    </Window.Resources>
    <DockPanel>
        <StackPanel DockPanel.Dock="Right" Width="200" Margin="7">
            <ListBox Name="listBoxSuggestions" Height="200"
                ItemsSource="{Binding Source={StaticResource txSpellChecker1}, Path=SpellingCorrectionUIProvider.SuggestionsHandling.Suggestions}"
                IsEnabled="{Binding Source={StaticResource txSpellChecker1}, Path=SpellingCorrectionUIProvider.SuggestionsHandling.IsSuggestionListEnabled}" />
            <Button Name="buttonChange" Content="Change" Margin="0,7,0,0" Click="buttonChange_Click"
                IsEnabled="{Binding Source={StaticResource txSpellChecker1}, Path=SpellingCorrectionUIProvider.CorrectionHandling.IsChangeEnabled}" />
            <Button Name="buttonIgnore" Content="Ignore" Margin="0,7,0,0" Click="buttonIgnore_Click"
                IsEnabled="{Binding Source={StaticResource txSpellChecker1}, Path=SpellingCorrectionUIProvider.CorrectionHandling.IsIgnoreEnabled}" />
            <Button Name="buttonDelete" Content="Delete" Margin="0,7,0,0" Click="buttonDelete_Click"
                IsEnabled="{Binding Source={StaticResource txSpellChecker1}, Path=SpellingCorrectionUIProvider.CorrectionHandling.IsDeleteEnabled}" />
        </StackPanel>
        <tx:TextControl Name="textControl1" SpellChecker="txSpellChecker1" IsSpellCheckingEnabled="True"
            Loaded="textControl1_Loaded" />
    </DockPanel>
</Window>

The code-behind connects the editor once it is loaded and handles the three buttons:

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

namespace SpellingCorrectionSample
{
    public partial class MainWindow : Window
    {
        private readonly TXSpellChecker txSpellChecker1;

        public MainWindow()
        {
            InitializeComponent();
            txSpellChecker1 = (TXSpellChecker)FindResource("txSpellChecker1");
        }

        private void textControl1_Loaded(object sender, RoutedEventArgs 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);
        }

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

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

        private void buttonChange_Click(object sender, RoutedEventArgs e)
        {
            if (listBoxSuggestions.SelectedItem != null)
            {
                txSpellChecker1.SpellingCorrectionUIProvider.CorrectionHandling.Change(listBoxSuggestions.SelectedItem.ToString());
            }
        }

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

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

The code works as follows:

  • The bindings take the spell checker resource as their Source and walk the path to the provider: ItemsSource fills the list with Suggestions, and each IsEnabled follows the provider, so a button is grayed out whenever its command is not available in the current mode.
  • SetCurrent connects the TextControl in its Loaded event. 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.
  • 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 extends this panel. Each addition binds to another member:

  • Change All, Ignore All and Add To Dictionary. Bind buttons to IsChangeAllEnabled, IsIgnoreAllEnabled and IsAddToDictionaryEnabled, and call ChangeAll, IgnoreAll and AddToDictionary from them.
  • A dictionary choice. Bind the ItemsSource of a ComboBox to SuggestionDictionariesHandling.SuggestionDictionaries, the dictionaries in the language of the current word, and its IsEnabled to SuggestionDictionariesHandling.IsSuggestionDictionariesEnabled, which tells whether suggestion dictionaries are available. In its SelectionChanged event, assign an array with the chosen dictionary to SuggestionDictionariesHandling.SelectedSuggestionDictionaries. The suggestions update on their own.
  • The sentence of the word. A preview shows the sentence that contains the current word; Show the Sentence of the Word builds it.
  • Correcting by hand. When no suggestion fits, the user types the word into a text box; Edit the Word by Hand shows how.
  • The number of suggestions. MaxSuggestions limits the list; the default is 10.

The dictionary choice takes these lines in the StackPanel of the window:

<ComboBox Name="comboBoxSuggestionDictionaries" Margin="0,7,0,0"
    SelectionChanged="comboBoxSuggestionDictionaries_SelectionChanged"
    ItemsSource="{Binding Source={StaticResource txSpellChecker1}, Path=SpellingCorrectionUIProvider.SuggestionDictionariesHandling.SuggestionDictionaries}"
    IsEnabled="{Binding Source={StaticResource txSpellChecker1}, Path=SpellingCorrectionUIProvider.SuggestionDictionariesHandling.IsSuggestionDictionariesEnabled}" />

Its handler goes into MainWindow.xaml.cs, which needs using System.Windows.Controls; for the event arguments:

private void comboBoxSuggestionDictionaries_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (comboBoxSuggestionDictionaries.SelectedItem is Dictionary dictionary)
    {
        // Create the suggestions from the chosen dictionary only
        txSpellChecker1.SpellingCorrectionUIProvider.SuggestionDictionariesHandling.SelectedSuggestionDictionaries =
            new Dictionary[] { dictionary };
    }
}

Show the Sentence of the Word

A word is easier to correct in its context. PreviewSentenceBounds of the PreviewHandler is an array of two integers: the start position and the length of the sentence that contains the current word. The provider sets it again for each new word. It holds positions, not text, so a value converter of the binding turns it into the sentence.

Add the converter class to the project, in the namespace of the window:

using System;
using System.Globalization;
using System.Windows.Data;
using TXTextControl.WPF.Proofing;

namespace SpellingCorrectionSample
{
    public class PreviewBoundsConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is int[] bounds && parameter is TXSpellChecker spellChecker &&
                spellChecker.SpellingCorrectionUIProvider.StateManaging.CurrentWordToCorrect is TXTextControl.MisspelledWord word)
            {
                // The text part of the TextControl that holds the current word
                var textPart = (TXTextControl.IFormattedText)spellChecker.SpellingCorrectionUIProvider.StateManaging.CurrentIFormattedText;
                TXTextControl.Paragraph paragraph = textPart.Paragraphs.GetItem(word.Start);

                // Paragraph.Start is one-based: subtract Start - 1 to get the offset in the paragraph
                return paragraph.Text.Substring(bounds[0] - (paragraph.Start - 1), bounds[1]);
            }

            return "";
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
            throw new NotSupportedException();
    }
}

Declare the converter as a resource beside the spell checker, and bind a TextBlock above the list of suggestions to the bounds. The prefix local maps the namespace of the project; add xmlns:local="clr-namespace:SpellingCorrectionSample" to the Window element:

<Window.Resources>
    <spell:TXSpellChecker x:Key="txSpellChecker1" />
    <local:PreviewBoundsConverter x:Key="previewBoundsConverter" />
</Window.Resources>
<TextBlock Name="textBlockSentence" Height="50" TextWrapping="Wrap"
    IsEnabled="{Binding Source={StaticResource txSpellChecker1}, Path=SpellingCorrectionUIProvider.PreviewHandling.IsPreviewSentenceBoundsEnabled}"
    Text="{Binding Source={StaticResource txSpellChecker1}, Path=SpellingCorrectionUIProvider.PreviewHandling.PreviewSentenceBounds,
           Converter={StaticResource previewBoundsConverter}, ConverterParameter={StaticResource txSpellChecker1}}" />

The code works as follows:

  • IsPreviewSentenceBoundsEnabled tells whether PreviewSentenceBounds provides bounds for the current word; the TextBlock is disabled when it doesn't.
  • The binding passes the spell checker to the converter as ConverterParameter, so that the converter reaches the provider.
  • CurrentIFormattedText is the text part of the TextControl that holds the current word: the main text, a header, a footer or a text frame. Its type is object, and the converter casts it to IFormattedText.
  • Paragraphs and GetItem find the paragraph at the word. GetItem takes a zero-based text position; the one-based Start of the word points into the word, so the paragraph is the same.
  • The offset arithmetic is the part to get right. Paragraph.Start is the one-based number of the paragraph's first character, so paragraph.Start - 1 is its zero-based position. Subtracting it from the start of the sentence gives the index of the sentence in Paragraph.Text, and the second value of the array is the length to cut out.

Edit the Word by Hand

When no suggestion fits, the user types the correct word into a TextBox below the list. An Undo Edit button next to it returns to the suggestions. The text box shows the current word through a second converter, which turns a MisspelledWord into its text:

using System;
using System.Globalization;
using System.Windows.Data;

namespace SpellingCorrectionSample
{
    public class MisspelledWordConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
            value is TXTextControl.MisspelledWord word ? word.Text : "";

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
            throw new NotSupportedException();
    }
}

Declare it in the resources with <local:MisspelledWordConverter x:Key="wordToCorrectConverter" />, and add the text box, the button and a TextBlock for messages to the StackPanel:

<TextBox Name="textBoxEdit" Margin="0,7,0,0"
    Text="{Binding Source={StaticResource txSpellChecker1}, Mode=OneWay, Path=SpellingCorrectionUIProvider.StateManaging.CurrentWordToCorrect,
           Converter={StaticResource wordToCorrectConverter}}"
    PreviewKeyDown="textBoxEdit_PreviewKeyDown" TextChanged="textBoxEdit_TextChanged" />
<Button Name="buttonUndoEdit" Content="Undo Edit" Margin="0,7,0,0" Click="buttonUndoEdit_Click"
    IsEnabled="{Binding Source={StaticResource txSpellChecker1}, Path=SpellingCorrectionUIProvider.CorrectionHandling.IsUndoEditEnabled}" />
<TextBlock Name="textBlockMessage" Margin="0,7,0,0" TextWrapping="Wrap" />

The handlers go into MainWindow.xaml.cs, which needs using System.Windows.Controls; and using System.Windows.Input;. The new buttonChange_Click replaces the one of the panel above:

private void textBoxEdit_PreviewKeyDown(object sender, KeyEventArgs e)
{
    // The first key the user presses starts editing the word
    txSpellChecker1.SpellingCorrectionUIProvider.StateManaging.IsWordToCorrectEditing = true;
}

private void textBoxEdit_TextChanged(object sender, TextChangedEventArgs e)
{
    TXSpell.SpellingCorrectionMode mode = txSpellChecker1.SpellingCorrectionUIProvider.StateManaging.Mode;

    // A duplicate has nothing to check, and after the last word there is no word left
    if (mode == TXSpell.SpellingCorrectionMode.WordToCorrectIsDuplicate ||
        mode == TXSpell.SpellingCorrectionMode.CorrectionCompleted) return;

    txSpellChecker1.SpellingCorrectionUIProvider.CorrectionHandling.CheckCorrectedWord(textBoxEdit.Text, out string message);
    textBlockMessage.Text = message;
}

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

    if (provider.StateManaging.IsWordToCorrectEditing)
    {
        // Replace the word with the text the user typed
        provider.CorrectionHandling.Change(textBoxEdit.Text);
    }
    else if (listBoxSuggestions.SelectedItem != null)
    {
        provider.CorrectionHandling.Change(listBoxSuggestions.SelectedItem.ToString());
    }
}

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

    // Leave the editing mode and show the misspelled word again
    provider.StateManaging.IsWordToCorrectEditing = false;
    textBoxEdit.Text = ((TXTextControl.MisspelledWord)provider.StateManaging.CurrentWordToCorrect).Text;
}

The code works as follows:

  • The binding of the text box is OneWay: it shows each new CurrentWordToCorrect, and what the user types doesn't flow back to the provider.
  • PreviewKeyDown sets IsWordToCorrectEditing to true before the first character arrives. The mode becomes WordToCorrectEditing, which enables Change, Change All and Undo Edit through their bindings. Setting the property has an effect only while there is a current word.
  • CheckCorrectedWord returns true when the typed word is in one of the selected suggestion dictionaries. Otherwise, its out parameter holds a message for the user, which the TextBlock shows; for a correct word, the message is empty. The method works only in the modes SuggestionsCreated and WordToCorrectEditing, and TextChanged also fires when the binding fills the box, so the handler skips a duplicate and the end of the correction.
  • Change takes the typed text while the user is editing, and the selected suggestion otherwise. A Change All button does the same with ChangeAll.
  • Undo Edit is enabled by IsUndoEditEnabled in the mode WordToCorrectEditing. Its handler sets IsWordToCorrectEditing back to false and puts the misspelled word back into the box.

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 ready-made dialog of TX Spell .NET shown here:

The ready-made Spelling dialog of TX Spell .NET with 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