Ary Borenszweig

Ary Borenszweig

Autocomplete in Silverlight

8 min
Sep 26 2008
coding
8 min
Sep 26 2008

I've written a simple class to allow adding autocomplete to a TextBox. An example of how to use it is:

In the XAML file:

<TextBox x:Name="uiText" Width="100" Height="30"
    manas:Autocomplete.Suggest="DoSuggest" />

In the class file:

// The texts we want to suggest
private string[] options = new string[]
{
    "Al", "Amiko", "Angla", "Anglujo", "Ankaux", "Antaux", "Atomo", "Auxto",
    "Bebo", "Bela", "Birdo",
}

// The method we have to implement to offer suggestions
public void DoSuggest(string text, SuggestCallback callback)
{
    // Don't suggest if there's no text
    if (text.Length == 0)
    {
        callback(null);
        return;
    }

    var result = new List();

    // See which options have as a prefix the text entered by the user
    foreach (var option in options)
    {
        if (option.StartsWith(text, StringComparison.InvariantCultureIgnoreCase))
        {
            result.Add(new Suggestion() { DisplayString = option,
                                                      ReplaceString = option });
        }
    }

    callback(result.ToArray());
}

As in a previous post, I use the technique to specify an action to happen in the XAML, and it's implementation in the class file, just like an event handler.

Of course, instead of using an hardcoded options array, these could be requested in the DoSuggest method to a web service, or requested from another class.

The suggestions are shown in an unstyled ListBox, but it should be easy to style, and even to improve the code to support icons in the suggestions, or any other control.

For this to work, the RootVisual of your application must be a Canvas, since otherwise you can't place arbitrary floating elements on top of it.

Here’s the full code in case someone finds it useful.


using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Diagnostics;

namespace Manas.Silverlight
{

    /// <summary>
    /// Allows reporting suggestions asynchronously.
    /// </summary>
    public delegate void SuggestCallback(Suggestion[] suggestions);

    /// <summary>
    /// Invoked when suggestions are needed.
    /// </summary>
    /// <param name="text">the text to autocomplete</param>
    /// <param name="callback">where to report the suggestions</param>
    public delegate void SuggestHandler(string text, SuggestCallback callback);

    /// <summary>
    /// Each suggestion to report for the autocompletion.
    /// </summary>
    public class Suggestion
    {

        /// <summary>
        /// What to show in the autocomplete popup.
        /// </summary>
        public string DisplayString { get; set; }

        /// <summary>
        /// What to insert as a replacement if this suggestion is selected.
        /// </summary>
        public string ReplaceString { get; set; }

        public override string ToString()
        {
            return DisplayString;
        }

    }

    /// <summary>
    /// Allows adding autocomplete capabilities to a TextBox.
    /// </summary>
    public class Autocomplete
    {

        #region XAML support
        public static readonly DependencyProperty Suggest = DependencyProperty.RegisterAttached("Suggest", typeof(string), typeof(Autocomplete), null);

        public static void SetSuggest(DependencyObject obj, string handler)
        {
            obj.SetValue(Suggest, handler);

            EventSupport.AttachEvent(obj, handler,
                new Type[] { typeof(string), typeof(SuggestCallback) },
                (sender, target, info) =>
                {
                    Autocomplete autocomplete = new Autocomplete(sender as TextBox);
                    autocomplete.SuggestHandler = (text, callback) =>
                    {
                        info.Invoke(target, new object[] { text, callback });
                    };
                });
        }

        public static string GetSuggest(DependencyObject obj)
        {
            return (string)obj.GetValue(Suggest);
        }
        #endregion

        private TextBox textBox;
        private ListBox listBox;

        internal Autocomplete(TextBox textBox)
        {
            this.textBox = textBox;

            textBox.LostFocus += textBox_LostFocus;
            textBox.GotFocus += (s, e) =>
            {
                OfferSuggestions();
            };
            textBox.KeyDown += textBox_KeyDown;
            textBox.KeyUp += textBlock_KeyUp;
        }

        /// <summary>
        /// Adds autocomplete support to the given TextBox.
        /// </summary>
        public Autocomplete(TextBox textBox, SuggestHandler handler)
            : this(textBox)
        {
            this.SuggestHandler = handler;
        }

        internal SuggestHandler SuggestHandler { get; set; }

        void textBox_LostFocus(object sender, RoutedEventArgs e)
        {
            object elem = FocusManager.GetFocusedElement();
            if (elem == listBox)
            {
                return;
            }

            ListBoxItem item = elem as ListBoxItem;
            if (item != null)
            {
                object parent = item.Parent;
                while (!(parent is ListBox) && parent is FrameworkElement)
                {
                    parent = (parent as FrameworkElement).Parent;
                }

                if (parent == listBox)
                {
                    InsertSuggestion();
                }
            }

            HideListBox();
        }

        void textBox_KeyDown(object sender, KeyEventArgs e)
        {
            if (listBox == null) return;

            if (e.Key == Key.Down)
            {
                listBox.SelectedIndex = (listBox.SelectedIndex + 1) % listBox.Items.Count;
            }
            else if (e.Key == Key.Up)
            {
                if (listBox.SelectedIndex == 0)
                {
                    listBox.SelectedIndex = listBox.Items.Count - 1;
                }
                else
                {
                    listBox.SelectedIndex--;
                }
            }
            else if (e.Key == Key.Enter)
            {
                InsertSuggestion();
            }
        }

        void textBlock_KeyUp(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Down || e.Key == Key.Up) return;

            OfferSuggestions();
        }

        void OfferSuggestions()
        {
            SuggestHandler(textBox.Text, Callback);
        }

        void Callback(Suggestion[] suggestions)
        {
            // If there are no suggestions, do nothing
            //                    or
            // If there is only one suggestion but it's the text that we have in the
            // text box, do nothing
            if (suggestions == null ||
                suggestions.Length == 0 ||
                (suggestions.Length == 1 && suggestions[0].ReplaceString == textBox.Text))
            {
                HideListBox();
                return;
            }

            ShowListBox(suggestions.Length);

            listBox.Items.Clear();
            foreach (var suggestion in suggestions)
            {
                listBox.Items.Add(suggestion);
            }

            this.listBox.Dispatcher.BeginInvoke(() =>
            {
                listBox.SelectedIndex = 0;
            });
        }

        void ShowListBox(int count)
        {
            if (listBox == null)
            {
                listBox = new ListBox();
                ScrollViewer.SetVerticalScrollBarVisibility(listBox, ScrollBarVisibility.Visible);
                listBox.SelectionChanged += (s, e) =>
                {
                    if (listBox.SelectedItem != null && listBox.Items.Contains(listBox.SelectedItem))
                    {
                        try
                        {
                            listBox.ScrollIntoView(listBox.SelectedItem);
                        }
                        catch { }
                    }
                };
                listBox.MinWidth = textBox.RenderSize.Width;

                var canvas = (Canvas)System.Windows.Application.Current.RootVisual;
                var transform = textBox.TransformToVisual(canvas);
                var topLeft = transform.Transform(new Point(0, 0));

                Canvas.SetLeft(listBox, topLeft.X);
                Canvas.SetTop(listBox, topLeft.Y + textBox.RenderSize.Height);
                canvas.Children.Add(listBox);
            }

            if (count >= 5)
            {
                count = 5;
            }
            listBox.MaxHeight = count * 21;
        }

        private void InsertSuggestion()
        {
            Suggestion suggestion = listBox.SelectedItem as Suggestion;
            if (suggestion != null)
            {
                textBox.Text = suggestion.ReplaceString;
                textBox.Select(textBox.Text.Length, 0);
            }
        }

        void HideListBox()
        {
            if (listBox != null)
            {
                var canvas = (Canvas)System.Windows.Application.Current.RootVisual;
                canvas.Children.Remove(listBox);
                listBox = null;
            }
        }

    }

}