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
- Get
s/set
s 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 Style
s 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. Adorner
s 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.