Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Spelling Suggestions in a WPF TextBox

0.00/5 (No votes)
25 Feb 2007 1  
Examines an intuitive way to correct typos in a TextBox.
Screenshot - SmartTextBox.png

Introduction

This article reviews a TextBox subclass which provides support for helping the user easily fix typos. The SmartTextBox extends WPF's built-in support for spellchecking by popping up a customizable list of "suggestions" for a misspelled word. The list of suggestions can be shown manually via the F1 key, or programmatically with one simple method call. When the list of suggestions is shown, its height is briefly animated to provide a subtle visual cue for the user.

SmartTextBox can give a WPF application that extra little touch of convenience and professionalism, which most users really appreciate. It might be appropriate for an application which allows the user to enter notes or comments that will be read by others.

Background

WPF provides support for spellchecking the text in a TextBox and RichTextBox. The SpellCheck class exposes the primary APIs used for spellchecking. As of WPF v1 the spellchecking support is still not full-featured, but it is expected to be more mature in subsequent releases.

For example, only a few languages are supported (I believe only English, German, and French), and you cannot use a custom dictionary. For more information about the spellchecking support, read more about it here.

The TextBox control is integrated with WPF's spellchecking feature. When spellchecking is enabled, a misspelled word is displayed with a red squiggly line beneath it. Also, if you right-click on a misspelled word the ContextMenu will provide spelling suggestions, as seen below:

The SmartTextBox's features complement this standard functionality.

Using the SmartTextBox

The API

SmartTextBox offers several public members you can use to control its behavior and appearance. Here is its public API:

Properties

  • AreSuggestionsVisible - Returns true if the list of suggestions is currently displayed.
  • IsCurrentWordMisspelled - Returns true if the word at the caret index is misspelled.
  • SuggestionListBoxStyle - Gets/sets the Style applied to the ListBox which displays spelling suggestions. This is a dependency property.

Methods

  • GetSpellingError() - Returns a SpellingError for the word at the current caret index, or null if the current word is not misspelled.
  • HideSuggestions() - Hides the list of suggestions and returns focus to the input area. If the list of suggestions is not already displayed, nothing happens.
  • ShowSuggestions() - Shows the list of suggestions for the word at the caret index. If the current word is not misspelled, this method does nothing.

The basics

You can create an instance of SmartTextBox in XAML the same way as any other element:

<jas:SmartTextBox>I contian a typo!</jas:SmartTextBox>

By default the spellchecking features are enabled. If you need to disable that functionality, you can do it several ways:

this.smartTextBox.SpellCheck.IsEnabled = false;

or

SpellCheck.SetIsEnabled( this.smartTextBox, false );

or (in XAML)

<jas:SmartTextBox SpellCheck.IsEnabled="false" />

Styling the list of suggestions

The list of spelling suggestions is shown in the SmartTextBox's adorner layer. The list itself is a ListBox control, which lives in an Adorner. If you want to stylize the list of suggestions, set the SmartTextBox's SuggestionListBoxStyle dependency property to a Style.

For example, here are the Styles used in the demo application, as seen in the screenshot at the top of the article:

<Style x:Key="SuggestionListBoxStyle" TargetType="ListBox">
  <Setter Property="BitmapEffect">
    <Setter.Value>
      <DropShadowBitmapEffect ShadowDepth="1" Softness="0.5" />
    </Setter.Value>
  </Setter>
  <Setter Property="BorderBrush" Value="Gray" />
  <Setter Property="ItemContainerStyle">
    <Setter.Value>
      <Style TargetType="ListBoxItem">
        <Setter Property="Border.BorderBrush" Value="LightGray" />
        <Setter Property="Border.BorderThickness" Value="0,0,0,0.5" />
        <Setter Property="FontWeight" Value="Bold" />
        <Setter Property="Margin" Value="2,4" />
        <Setter Property="FocusVisualStyle" Value="{x:Null}" />
      </Style>
    </Setter.Value>
  </Setter>
</Style>

<Style TargetType="jas:SmartTextBox">
  <Setter Property="AcceptsReturn" Value="True" />
  <Setter Property="BorderBrush" Value="Gray" />
  <Setter Property="Margin" Value="4" />
  <Setter
    Property="SuggestionListBoxStyle"
    Value="{StaticResource SuggestionListBoxStyle}" />
  <Setter Property="TextWrapping" Value="Wrap" />
</Style>

How it works

The remainder of this article discusses how the SmartTextBox works. You do not need to read further in order to use the control in your applications.

Adorner vs. Popup

When I first designed this class I tried to host the suggestions ListBox in a Popup. I ran into a couple of problems with using the Popup in this situation, so I decided to use a custom Adorner to host the suggestions instead.

The Popup did not "follow" the SmartTextBox when the window was moved. If I changed the window's state to 'Maximized' or 'Normal' the Popup would snap back into place, but not when the window moved. Adorners do not have that problem because they are not separate top-level windows, like the Popup.

Also, I encountered some frustrating issues relating to input focus when using a Popup. Those issues disappeared when I used the adorner approach.

The only drawback I'm aware of with using adorners to show the list of suggestions is that they can be clipped. If the bottom of the window which contains a SmartTextBox is very close to a misspelled word, the list of suggestions might not be entirely visible. I don't think this is too big of an issue, though. Here's what I'm referring to:

Showing the list of suggestions

When the caret is in a misspelled word and the user presses the F1 key, the SmartTextBox needs to display a list of suggested spellings. That is accomplished by the following (much abridged) code:

protected override void OnPreviewKeyDown( KeyEventArgs e )
{
 base.OnPreviewKeyDown( e );
 if( e.Key == Key.F1 )
 {
  this.AttemptToShowSuggestions();
  if( this.AreSuggestionsVisible )
   this.suggestionList.SelectedIndex = 0;
 }
}

void AttemptToShowSuggestions()
{
 if( this.AreSuggestionsVisible )
  return;

 // If there is no spelling error, there is no

 // need to show the list of suggestions.

 SpellingError error = this.GetSpellingError();
 if( error == null )
  return;

 this.suggestionList.ItemsSource = error.Suggestions;
 this.ShowSuggestions();
}

public SpellingError GetSpellingError()
{
 int idx = this.FindClosestCharacterInCurrentWord();
 return idx < 0 ? null : base.GetSpellingError( idx );
}

public void ShowSuggestions()
{
 if( this.AreSuggestionsVisible || !this.IsCurrentWordMisspelled )
  return;

 AdornerLayer layer = AdornerLayer.GetAdornerLayer( this );
 if( layer == null )
  return;

 // Position the adorner beneath the misspelled word.

 int idx = this.FindBeginningOfCurrentWord();
 Rect rect = base.GetRectFromCharacterIndex( idx );
 this.adorner.SetOffsets( rect.Left, rect.Bottom );

 // Add the adorner into the adorner layer.

 layer.Add( this.adorner );

 // Since the ListBox might have a new set of items but has not

 // rendered yet, we force it to calculate its metrics so that

 // the height animation has a sensible target value.

 this.suggestionList.Measure(
    new Size( Double.PositiveInfinity, Double.PositiveInfinity ) );
 this.suggestionList.Arrange(
    new Rect( new Point(), this.suggestionList.DesiredSize ) );

 // Animate the ListBox's height to the natural value.

 DoubleAnimation anim = new DoubleAnimation();
 anim.From = 0.0;
 anim.To = this.suggestionList.ActualHeight;
 anim.Duration = new Duration( TimeSpan.FromMilliseconds( 200 ) );
 anim.FillBehavior = FillBehavior.Stop;
 this.suggestionList.BeginAnimation( ListBox.HeightProperty, anim );

 this.areSuggestionsVisible = true;
}

Fixing a typo

When the user has selected a suggested word to replace a typo, the SmartTextBox needs to update the text and hide the list of suggestions. The heavy-lifting for most of that task is handled by the SpellingError class, as seen below:

void suggestionList_PreviewKeyDown( object sender, KeyEventArgs e )
{
 if( this.suggestionList.SelectedIndex < 0 )
  return;

 if( e.Key == Key.Escape )
 {
  this.HideSuggestions();
 }
 else if( e.Key == Key.Space || e.Key == Key.Enter || e.Key == Key.Tab )
 {
  this.ApplySelectedSuggestion();
  // Mark the event as handled so that the keystroke

  // does not propogate to the TextBox.

  e.Handled = true;
 }
}

void ApplySelectedSuggestion()
{
 if( !this.AreSuggestionsVisible || this.suggestionList.SelectedIndex < 0 )
  return;

 SpellingError error = this.GetSpellingError();
 if( error != null )
 {
  string correctWord = this.suggestionList.SelectedItem as string;
  error.Correct( correctWord );
  base.CaretIndex = this.FindEndOfCurrentWord();
  base.Focus();
 }
 this.HideSuggestions();
}

public void HideSuggestions()
{
 if( !this.AreSuggestionsVisible )
  return;

 this.suggestionList.ItemsSource = null;

 AdornerLayer layer = AdornerLayer.GetAdornerLayer( this );
 if( layer != null )
  layer.Remove( this.adorner );

 base.Focus();
 this.areSuggestionsVisible = false;
}

Revision History

  • February 19, 2007 - Created article.
  • February 25, 2007 - Fixed a bug involving the inheritance of dependency property values into the ListBox shown in the SmartTextBox's adorner layer. Zhou Yong (aka Sheva) and Ian Griffiths helped find the solution. Sheva blogged about it here. The issue was resolved in this thread on the WPF Forum. I removed an incorrect statement about the relationship between an element's visual tree and its adorner layer from this article. I also updated the article's source code download.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here