A WinRT TextBox control with a Dynamic AutoComplete Dictionary

Posted by: Sumit Maitra , on 2/4/2013, in Category WinRT
Views: 12464
Abstract: In this sample, we will create a Custom WinRT Component that adds an AutoComplete like functionality to a TextBox

The WinRT runtime provides spell check and auto complete functions in controls like the TextBox and the RichTextBox out of the box. To do so, it hooks into a Windows 8 default system Dictionary that’s installed based on the language you choose for your Windows 8 setup. This provides red squiggles under incorrectly spelt words and provides a right click context menu with suggestions for fixing the error. This also provides an ‘Add to Dictionary’ functionality that adds new words to the Dictionary. However, there is no way to hook a custom dictionary into this setup. To enable this, all you have to do is set two properties on the XAML Control

 

Recently I received a feature request for my Twitter client. The requested behavior was while typing a Tweet, when user types in @, a list of Friends should come up allowing user to select from the DropDown list. This was an interesting challenge and I decided to convert my attempt into this post.

The problem at hand

I broke down the problem as follows:

1. Identify screen location of Cursor in a textbox

2. Show Popup menu control at the above location

3. Populate popup menu with relevant menu items

4. Update text when menu item is selected.

Hacking up a Solution

First thing I did was attacked the problem in-place. I hacked the following code together by adding a code behind event to a text box directly.

1.    if (updateText.SelectionStart > 0)
2.    {
3.     if (updateText.Text.Substring(updateText.SelectionStart-1, 1).EndsWith("@"))
4.     {
5.      Rect position = updateText.GetRectFromCharacterIndex(updateText.SelectionStart-1, true);
6.      var transform = updateText.TransformToVisual(this);
7.      var point = transform.TransformPoint(new Point(position.Left, position.Bottom));
8.      PopupMenu popupMenu = new PopupMenu();
9.      popupMenu.Commands.Add(new UICommand("@Owned", (IUICommand command) =>
10.      {
11.       var oldSelectionStart = updateText.SelectionStart;
12.       var newSelectionStart = oldSelectionStart + command.Label.TrimStart('@').Length;
13.       updateText.Text = updateText.Text.Insert(updateText.SelectionStart, command.Label.TrimStart('@'));
14.       updateText.SelectionStart = newSelectionStart;
15.      }));
16.      await popupMenu.ShowForSelectionAsync(new Rect(point, point), Placement.Below);
17.     }
18.    }

Here ‘updateText’ is the Name of a TextBox control in XAML. In the above code, line 5 gets position of the cursor with respect to the top left corner of the TextBox. As you can see, the text box’s SelectionStart property always return the current cursor position. We pick the last typed character by moving back one character.

Next in line 6 we get the Transformation matrix for the text box control with respect to the form (this).

In line 7, we get the point location below the cursor with respect to the entire screen.

Line 8 through 15 we are hardcoding one Menu Item in a popup menu.

Line 10 through 14 is an anonymous function that we assign as the handler for the Menu Item selection command. The event handler captures the current cursor position, calculates the final cursor position and inserts the text from the selected command’s label.

Finally in Line 16, we show the popup menu at the point and set the placement position as Below, this shows the Popup Menu below the text (default position for popup menu is above the point specified).

This is what it looks like, it pops up as soon as I press @.

prototype-functionality-in-action
With the hacked up Proof-of-concept done, let us now build a reusable component that can be just dropped in and configured.

Building a Reusable TextBox Control

Step 1: We start off with new Application using a Standard Windows 8 Blank Application template

create-new-solution

This is our Sample Application that hosts our Custom Control.

Step 2: We add a Windows 8 Runtime Component project called DynamicAutoCompleteTextBox so that our XAML control is usable in VB.NET and other .NET languages.

add-new-windows-runtime-component-project

Step 3: Setting up the AutoCompleteTextBox control - Add a new cs file called AutoCompleteTextBox.cs. The code is split into three parts

The Declarations

fields-and-properties

Our control inherits the behavior of the TextBox control completely and it also implements the INotifyPropertyChange interface.

We have two Properties

  • MenuTrigger : Holds the character that triggers the popup menu
  • Dictionary: The IEnumerable that holds list of all possible Auto Complete options. In my Twitter client case, it would hold list of all the followers

Other Fields: Other fields, _isTriggered, _triggerDistance, _triggerLocation are used to calculate and track the current position of the cursor as well as the last invoked MenuTrigger.

The Constructor, initializes the collections and sets up the TextChanged event handler. There is no OnTextChanged method that can be overridden for the TextChanged event hence we use the handler. Alternately we can override the OnKeyUp method for finer grained control.

The EventHandler

1.    void AutoCompleteTextBox_TextChanged(object sender, TextChangedEventArgs e)
2.    {
3.        CheckIfTriggered();
4.        Rect position = this.GetRectFromCharacterIndex(this.SelectionStart - 1, true);
5.        if (_isTriggered)
6.        {
7.            popupMenu.Commands.Clear();
8.            string searchStr = this.Text.Substring(_triggerLocation, _triggerDistance)
9.                .TrimStart(MenuTrigger.ToCharArray()[0]);
10.            IEnumerable<string> items = _dictionary.Where(f => f.StartsWith(searchStr)).Take<string>(6);
11.            foreach (var item in items)
12.            {
13.                popupMenu.Commands.Add(new UICommand(item, (IUICommand command) =>
14.                {
15.                    var oldSelectionStart = this.SelectionStart;
16.                    string remainder = command.Label.Substring(searchStr.Length);
17.                    var newSelectionStart = oldSelectionStart + remainder.Length;
18.                    this.Text = this.Text.Insert(this.SelectionStart, remainder);
19.                    this.SelectionStart = newSelectionStart;
20.                    _triggerLocation = 0;
21.                    _triggerDistance = 1;
22.                }));
23.            }
24.            var transform = this.TransformToVisual((UIElement)this.Parent);
25.            var point = transform.TransformPoint(new Point(position.Left, position.Bottom));
26.            try
27.            {
28.                popupMenu.ShowForSelectionAsync(new Rect(point, point), Placement.Below).AsTask();
29.            }
30.            catch (InvalidOperationException ex)
31.            {
32.            }
33.        }
34.    }

 

In the above code, we first check if the PopupMenu has already been triggered, for example user typed @ and then went ahead and typed @Su. The Popup Menu should shorten the list appropriately. Based on how much the user has typed after the Trigger Character, we extract the searchStr and filter our data source accordingly at line 10.

Based on the results returned, we take the top 6 and convert them into popup MenuItem commands.

The Trigger Logic

Final piece of code is the Trigger Logic in the method CheckIfTriggered()

1.    if (this.SelectionStart > 0)
2.    {
3.     if (!_isTriggered)
4.     {
5.      _isTriggered = this.Text.Substring(this.SelectionStart -
_triggerDistance, 1).EndsWith(MenuTrigger.ToString());
6.      if (_isTriggered)
7.      {
8.       _triggerLocation = this.SelectionStart - _triggerDistance;
9.      }
10.     }
11.     else
12.     {
13.      _isTriggered = !this.Text.Substring(this.SelectionStart - 1, 1).Equals(" ");
14.      if (_isTriggered)
15.      {
16.       _triggerDistance = this.SelectionStart - _triggerLocation;
17.      }
18.      else
19.      {
20.       _triggerLocation = 0;
21.       _triggerDistance = 1;
22.      }
23.     }
24.    }

It starts off by checking if atleast one character has been typed (or if the caret is at the beginning of the textbox). If the popup has not yet been triggered, it checks if the last character was a trigger character. If so, the _triggerLocation value is set to the current position.

If the popup has already been triggered, it checks if the current character is a space, that cancels the trigger. If the current character is not space, it calculates the number of characters typed since the trigger character and saves it in _triggerDistance.

That pretty much covers the ‘logic’, now let’s see how we can use the Control.

Using the Custom Control

In the SampleApp created earlier, add reference to the DynamicAutoCompleteTextBox project.

xaml-updates

In the MainPage.xaml file add reference to the DynamicAutoCompleteTextBox’s namespace. Next we have a very simple XAML Grid layout. It has only two items, the Header Text and a DynamicAutoCompleteTextBox

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
    <Grid.RowDefinitions>
        <RowDefinition Height="140"></RowDefinition>
        <RowDefinition Height="1*"></RowDefinition>
        <RowDefinition Height="2*"></RowDefinition>
    </Grid.RowDefinitions>
    <TextBlock HorizontalAlignment="Left"
               Style="{StaticResource PageHeaderTextStyle}"
               Margin="120,54,0,0"
               TextWrapping="Wrap"
               Text="Dynamic AutoComplete TextBox Sample"
               VerticalAlignment="Top"
               Height="68" Width="1003"/>
    <dtb:AutoCompleteTextBox x:Name="DynamicAutoCompleteTextControl"
                             Grid.Row="1"
                             Margin="120,20,120,20"
                             Text=""
                             MenuTrigger="@">
    </dtb:AutoCompleteTextBox>
</Grid>

As you can see, we have setup the MenuTrigger to be ‘@’ in the XAML itself. In the code behind, all we have to do is assign the DataSource for the picker. We do this in the OnNavigatedTo event as follows

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    DynamicAutoCompleteTextControl.Dictionary =
        DynamicAutoCompleteTextBox.AutoCompleteTextBox.GetSortedDictionaryFromString(
        @"Suprotim,Minal,Sumit,Pravin,Mahesh");
}

We use the helper method to get a SortedList from the comma separated string input that we provide.

We are all set. Now let’s run the application. As soon as we press @, the complete list comes up.

top-6-auto-suggest

When we continue typing, the list gets reduced

filtered-auto-suggest

And when we select a Name and hit enter, the word is completed

final-result

Gotchas to be aware of

- If you look closely, we are loading only the top 6 strings that match our results. You’ll be wondering why only 6. Well like it or not, 6 is the hard set limit for number of Popup Menu Items you can have. If you try to add more, you’ll get a runtime exception.

- An empty try… catch block suppressing InvalidOperationException. Well this one is kind of goofy, the ShowForSelectAsync method is (as the name implies) Async. And if you try to hit the MenuTrigger character (e.g. @) too many times quickly, you have overlapping Async calls and WinRT throws an InvalidOperationException. The exception handler is a temp workaround.

Conclusion

In this sample, we created a Custom WinRT Component that adds an AutoComplete like functionality to a TextBox. Overriding functionality of framework controls often reduce confidence in the customization. However, as you can see there is no code that interferes with the working of a normal TextBox so our ‘wrapper’ is a pretty lightweight one.

The code for this is up on GitHub at https://github.com/dotnetcurry/winrt-autocomplete-textbox-component. Feel free to improve upon it and share feedback if you feel like it. Pull requests are welcome.

Give me a +1 if you think it was a good article. Thanks!
Recommended Articles
Sumit is a .NET consultant and has been working on Microsoft Technologies since his college days. He edits, he codes and he manages content when at work. C# is his first love, but he is often seen flirting with Java and Objective C. You can follow him on twitter at @sumitkm or email him at sumitkm [at] gmail


Page copy protected against web site content infringement by Copyscape


User Feedback
Comment posted by bobolik on Monday, February 4, 2013 10:35 AM
Terrible source code formatting. Impossible to read. And why watermarks in images? Someone may grab your image of Visual Studios new projects dialog image.
Comment posted by msdotnet on Friday, February 15, 2013 3:41 AM
great post,Thank you.
http://www.bestdotnettraining.com/Courses/online-webservices-and-wcf-training.aspx
Comment posted by Gautam on Saturday, March 23, 2013 7:24 PM
Thanks for the info. This is of great help.

I am running into a bug. When i type '@' the entire list shows up. When i type 's', the popup disappears, when i type 'u', i get two items  "Sumit" and "Suprotim". What i am seeing is that for every alternate characters i type, i hit the exception "A method was called at an unexpected time. (Exception from HRESULT: 0x8000000E)".

Are you aware of this ?
Comment posted by Gautam on Saturday, March 23, 2013 7:32 PM
It works if i add a delay, before the ShowForSelectionAsync call
await Task.Delay(100);
Comment posted by Sumit on Wednesday, March 27, 2013 6:31 PM
Hello Gautam,
Are you omitting an await somewhere? Applying a Task.Delay is sign of a incorrectly awaited code somewhere so it is also a temporary fix. Let me dig a little deeper into this, I'll post my findings here.
Thanks and Regards,
Sumit.
Comment posted by youtube touch windows 8 on Tuesday, July 16, 2013 8:34 AM
http://www.windows8app.net
Comment posted by Johan Danforth on Sunday, November 17, 2013 2:36 PM
This code doesn't work with multiline textboxes (AcceptsReturn = "true") when the textbox is scrolled down a page or more. The popup is drawn "outside" the textbox control.

Post your comment
Name:  
E-mail: (Will not be displayed)
Comment:
Insert Cancel