◆ HTML の datalist みたいなの
◆ 入力に応じて補完候補だしてくれる

HTML の datalist みたいな感じで入力サポートしてくれるのが WPF にないなーと思ってたので作ってみました

[TextBoxDataList.xaml]
<UserControl x:Class="liblib.wpf.TextBoxDataList"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:liblib.wpf"
             xmlns:s="clr-namespace:System;assembly=mscorlib"
             mc:Ignorable="d">
    <Popup HorizontalOffset="0" VerticalOffset="1" PlacementTarget="{Binding RelativeSource={RelativeSource AncestorType=UserControl},Path=target}" IsOpen="{Binding RelativeSource={RelativeSource AncestorType=UserControl},Path=is_shown}" AllowsTransparency="True" StaysOpen="False">
        <Border BorderBrush="#2E84FF" BorderThickness="1" Background="#fff">
            <ListBox x:Name="select_listbox" ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl},Path=available_data_items}" SelectionMode="Single" BorderThickness="0" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" MaxHeight="150" ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.CanContentScroll="False" AlternationCount="{x:Static s:Int32.MaxValue}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Button Content="{Binding}" Click="item_Click" MouseEnter="item_MouseEnter" Focusable="False">
                            <Button.Template>
                                <ControlTemplate TargetType="Button">
                                    <TextBlock Text="{Binding}" Margin="8 1" />
                                </ControlTemplate>
                            </Button.Template>
                        </Button>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ListBox>
        </Border>
    </Popup>
</UserControl>

[TextBoxDataList.xaml.cs]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace liblib.wpf
{
    /// <summary>
    /// TextBoxDataList.xaml の相互作用ロジック
    /// </summary>
    public partial class TextBoxDataList : UserControl
    {
        /// <summary>
        /// ターゲットの TextBox
        /// </summary>
        public TextBox target
        {
            get { return (TextBox)GetValue(targetProperty); }
            set { SetValue(targetProperty, value); }
        }

        // Using a DependencyProperty as the backing store for target.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty targetProperty =
            DependencyProperty.Register("target", typeof(TextBox), typeof(TextBoxDataList), new PropertyMetadata(null, (d, e) =>
            {
                var self = (TextBoxDataList)d;
                if (e.OldValue != null)
                {
                    // remove listeners
                    var old_textbox = (TextBox)e.OldValue;

                    old_textbox.LostFocus -= self.target_LostFocus;
                    old_textbox.PreviewKeyDown -= self.target_PreviewKeyDown;
                }

                // add listeners
                var new_textbox = (TextBox)e.NewValue;

                new_textbox.LostFocus += self.target_LostFocus;
                new_textbox.PreviewKeyDown += self.target_PreviewKeyDown;
            }));



        /// <summary>
        /// 表示するかどうか
        /// </summary>
        public bool is_shown
        {
            get { return (bool)GetValue(is_shownProperty); }
            set { SetValue(is_shownProperty, value); }
        }

        // Using a DependencyProperty as the backing store for is_shown.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty is_shownProperty =
            DependencyProperty.Register("is_shown", typeof(bool), typeof(TextBoxDataList), new PropertyMetadata(false));



        /// <summary>
        /// 全候補データ
        /// </summary>
        public IEnumerable<string> data_items
        {
            get { return (IEnumerable<string>)GetValue(data_itemsProperty); }
            set { SetValue(data_itemsProperty, value); }
        }

        // Using a DependencyProperty as the backing store for data_items.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty data_itemsProperty =
            DependencyProperty.Register("data_items", typeof(IEnumerable<string>), typeof(TextBoxDataList), new PropertyMetadata(null));



        /// <summary>
        /// 表示する候補データ
        /// </summary>
        public IEnumerable<string> available_data_items
        {
            get { return (IEnumerable<string>)GetValue(availble_data_itemsProperty); }
            set { SetValue(availble_data_itemsProperty, value); }
        }

        // Using a DependencyProperty as the backing store for availble_data_items.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty availble_data_itemsProperty =
            DependencyProperty.Register("available_data_items", typeof(IEnumerable<string>), typeof(TextBoxDataList), new PropertyMetadata(null));



        /// <summary>
        /// 自動で表示する項目を絞り込むか
        /// </summary>
        public bool auto_filtering
        {
            get { return (bool)GetValue(auto_filteringProperty); }
            set { SetValue(auto_filteringProperty, value); }
        }

        // Using a DependencyProperty as the backing store for auto_filtering.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty auto_filteringProperty =
            DependencyProperty.Register("auto_filtering", typeof(bool), typeof(TextBoxDataList), new PropertyMetadata(true));



        /// <summary>
        /// キータイプから操作更新までの遅延時間
        /// </summary>
        public int delay_msec
        {
            get { return (int)GetValue(delay_msecProperty); }
            set { SetValue(delay_msecProperty, value); }
        }

        // Using a DependencyProperty as the backing store for delay_msec.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty delay_msecProperty =
            DependencyProperty.Register("delay_msec", typeof(int), typeof(TextBoxDataList), new PropertyMetadata(500));



        /// <summary>
        /// 表示数
        /// </summary>
        public int display_number
        {
            get { return (int)GetValue(display_numberProperty); }
            set { SetValue(display_numberProperty, value); }
        }

        // Using a DependencyProperty as the backing store for display_number.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty display_numberProperty =
            DependencyProperty.Register("display_number", typeof(int), typeof(TextBoxDataList), new PropertyMetadata(5));



        private CancellationTokenSource token_source = null;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public TextBoxDataList()
        {
            InitializeComponent();
        }

        /// <summary>
        /// ターゲットでキー操作したときに候補を更新
        /// 絞り込みを更新してポップアップをオープン
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void target_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            var items_num = this.available_data_items?.Count() ?? 0;

            switch (e.Key)
            {
                case Key.Escape:
                    this.is_shown = false;
                    break;

                case Key.Up:
                    if (items_num > 0)
                    {
                        this.select_listbox.SelectedIndex = (this.select_listbox.SelectedIndex + items_num - 1) % items_num;
                        this.select_listbox.ScrollIntoView(this.select_listbox.SelectedItem);
                    }
                    break;

                case Key.Down:
                    if (items_num > 0)
                    {
                        this.select_listbox.SelectedIndex = (this.select_listbox.SelectedIndex + 1) % items_num;
                        this.select_listbox.ScrollIntoView(this.select_listbox.SelectedItem);
                    }
                    break;

                case Key.Tab:
                case Key.Enter:
                    if (0 <= this.select_listbox.SelectedIndex && this.select_listbox.SelectedIndex < items_num)
                    {
                        this.complement((string)this.select_listbox.SelectedItem);
                    }
                    break;

                default:
                    this.deferredUpdate();
                    return;
            }

            e.Handled = true;
        }

        /// <summary>
        /// フォーカスはずれたときに閉じる
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void target_LostFocus(object sender, RoutedEventArgs e)
        {
            this.is_shown = false;
        }

        /// <summary>
        /// クリック
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void item_Click(object sender, RoutedEventArgs e)
        {
            var button = (Button)sender;
            this.complement((string)button.Content);
        }

        private void item_MouseEnter(object sender, MouseEventArgs e)
        {
            var button = (Button)sender;
            this.select_listbox.SelectedItem = button.Content;
        }

        /// <summary>
        /// テキストボックス更新
        /// </summary>
        /// <param name="text"></param>
        private void complement(string text)
        {
            this.target.Text = text;
            this.target.CaretIndex = text.Length;
            this.is_shown = false;
        }

        /// <summary>
        /// 遅延アップデート
        /// </summary>
        private void deferredUpdate()
        {
            // cancel previous scheduled updating
            this.token_source?.Cancel();

            this.token_source = new CancellationTokenSource();
            var token = token_source.Token;

            var delay_msec = this.delay_msec;

            // update with a little delay
            Task.Run(() =>
            {
                Thread.Sleep(delay_msec);
                token.ThrowIfCancellationRequested();
                this.Dispatcher.Invoke(() =>
                {
                    var text = this.target.Text;

                    this.available_data_items = this.data_items?.Where(x => x.Contains(text)).Distinct().Take(this.display_number).ToList();

                    this.is_shown = (this.available_data_items?.Count() ?? 0) > 0;
                    this.select_listbox.SelectedIndex = 0;
                });
            }, token);
        }
    }
}

Gist

サンプル

<StackPanel Margin="10">
    <TextBox x:Name="textbox" Text="{Binding text}"/>
    <liblib:TextBoxDataList target="{Binding ElementName=textbox}" data_items="{Binding items}" display_number="10"/>
</StackPanel>
private class BindingData
{
    public List<string> items { get; set; }
        = new List<string> {
            "aa", "bbb", "acb", "abac",
            "abc", "cabac", "ddaeb", "cab",
            "bab", "abab", "abcab", "ab",
        };
}

public MainWindow()
{
    this.DataContext = new BindingData();
}

wpfsmp09


target プロパティに対象の TextBox を設定して data_items プロパティに候補の string のコレクションを入れます

あとは 自動で入力に応じて候補が出てきます

自動絞込をオフにして自分で表示候補をセットすることもできます
(auto_filtering を false にして data_items の代わりに available_data_items を Binding)

あとは 表示数と入力から表示までのディレイも調節できます