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;
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;
int idx = this.FindBeginningOfCurrentWord();
Rect rect = base.GetRectFromCharacterIndex( idx );
this.adorner.SetOffsets( rect.Left, rect.Bottom );
layer.Add( this.adorner );
this.suggestionList.Measure(
new Size( Double.PositiveInfinity, Double.PositiveInfinity ) );
this.suggestionList.Arrange(
new Rect( new Point(), this.suggestionList.DesiredSize ) );
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();
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.