Текстовое поле с выпадающими подсказками при вводе текста C# WPF XAML

Очень часто, при написании приложений, возникает необходимость сделать текстовое поле с выпадающими подсказками (например, как у яндекса или гугла при вводе в строку поиска). В статье представлен один из способов реализации текстового поля с выпадающими подсказками на языке программирования C# WPF.

Структура проекта будет следующей:

C# WPF XAML Текстовое поле с выпадающим списком - Структура проекта

В директории "Controls" будет находится пользовательский элемент управления, реализующий поставленную задачу, Window1 - главное окно приложения, TestModel - тестовая модель.

Итак, сначала создадим пользовательский элемент управления, назовем его TextBoxDropDownHintControl, поместим его в каталог Controls. XAML разметка будет следующией:

<UserControl x:Class="TextBoxDropDownHint.Controls.TextBoxDropDownHintControl"
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
	<Grid>
		<TextBox Panel.ZIndex="2" 
		         x:Name="tbxInputData"></TextBox>
		<ComboBox 
			SelectionChanged="LbList_SelectionChanged"
			x:Name="lbList"
			Height="25"
			Panel.ZIndex="1">
			<ComboBox.ItemContainerStyle>
				<Style TargetType="ComboBoxItem">
					<Setter Property="Template">
			            <Setter.Value>
			                <ControlTemplate TargetType="ComboBoxItem">
			                    <Grid x:Name="gd" Width="Auto" ShowGridLines="False">
			                    	<Grid.ColumnDefinitions>
			                            <ColumnDefinition Width="*"/>
			                            <ColumnDefinition Width="100"/>
			                        </Grid.ColumnDefinitions>
			                        <TextBlock Grid.Column="0" Text="{Binding DisplayMemberPath}"/>
			                        <TextBlock 
			                        	Margin="0, 0, 5, 0"
			                        	Grid.Column="1"
										Text="{Binding SecondColumnValue}"
			                            HorizontalAlignment="Right"/>
			                    	<ContentPresenter></ContentPresenter>
			                    </Grid>
			                	<ControlTemplate.Triggers>
	                                <Trigger Property="ComboBoxItem.IsSelected" Value="True">
	                                    <Setter TargetName="gd"  Property="Background" Value="Gray"></Setter>
	                                    <Setter TargetName="gd"  Property="TextElement.Foreground" Value="White"></Setter>
	                                </Trigger>
	                                <Trigger Property="ComboBoxItem.IsMouseOver" Value="True">
	                                    <Setter TargetName="gd"  Property="Background" Value="Blue"></Setter>
	                                    <Setter TargetName="gd"  Property="TextElement.Foreground" Value="White"></Setter>
	                                </Trigger>
									<Trigger Property="ComboBoxItem.IsHighlighted" Value="True">
	                                    <Setter TargetName="gd"  Property="Background" Value="Blue"></Setter>
	                                    <Setter TargetName="gd"  Property="TextElement.Foreground" Value="White"></Setter>
	                                </Trigger>
								</ControlTemplate.Triggers>
			                </ControlTemplate>
			            </Setter.Value>
					</Setter>
				</Style>
			</ComboBox.ItemContainerStyle>
		</ComboBox>
	</Grid>
</UserControl>

В ней мы создаем текстовое поле (TextBox) и Выпадающий список (ComboBox), которы спрятан за текстовым полем.

C# код будет следующим:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;

namespace TextBoxDropDownHint.Controls
{

	public class SelectionChanged
	{
		public object Value
		{
			get;
			private set;
		}
		public SelectionChanged(object v)
		{
			Value = v;
		}
	}
	
	
	public partial class TextBoxDropDownHintControl : UserControl
	{
		
		public delegate void SelectedValueChanged (object sender, SelectionChanged e);
		public event SelectedValueChanged OnSelect;
		
		public delegate void _TextChanged (object sender, TextChangedEventArgs e);
		public event _TextChanged TextChanged;
		
		IEnumerable<object> itemsSource = null;
		ObservableCollection<object> ISCollection = new ObservableCollection<object>();
		
		public TextBoxDropDownHintControl()
		{
			InitializeComponent();
			
			lbList.ItemsSource = ISCollection;
			
			tbxInputData.TextChanged += delegate(object sender, TextChangedEventArgs e) 
			{ 
				ISCollection.Clear();
				lbList.IsDropDownOpen = false;
				if(tbxInputData.Text == "") return;
				if(FilterItems())
				{
					lbList.IsDropDownOpen = true;
				}
				if(TextChanged != null)
				{
					TextChanged(this, e);
				}
			};
			
			tbxInputData.PreviewKeyDown += delegate(object sender, KeyEventArgs e) 
			{ 
				if(e.Key == Key.Down && lbList.IsDropDownOpen == true)
				{
					lbList.Focus();
				}
			};
			
			lbList.PreviewKeyDown += delegate(object sender, KeyEventArgs e)
			{ 
				if(e.Key == Key.Escape)
				{
					tbxInputData.Focus();
					e.Handled = true;
				}
			};
		}
		
		private bool FilterItems ()
		{
			if(itemsSource == null) return false;
			List<object> collection = (itemsSource).ToList();				
			for(int i = 0; i < collection.Count; i++)
			{
				var propertyInfo = collection[i].GetType().GetProperty(lbList.DisplayMemberPath);		
				string val = propertyInfo.GetValue(collection[i], null).ToString();
				if(val.ToLower().IndexOf(tbxInputData.Text.ToLower()) != -1)
				{
					ISCollection.Add(collection[i]);
				}
			}
			if(ISCollection.Count > 0)
			{
				return true;
			}
			return false;
		}
		
		void LbList_SelectionChanged(object sender, SelectionChangedEventArgs e)
		{
			if(OnSelect != null)
			{
				OnSelect(this, new SelectionChanged(lbList.SelectedItem));
			}
			tbxInputData.Text = lbList.Text;
			tbxInputData.Focus();
			tbxInputData.CaretIndex = tbxInputData.Text.Length;
			lbList.IsDropDownOpen = false;
		}
		
		public string DisplayMemberPath
		{
			get
			{
				return lbList.DisplayMemberPath;
			}
			set
			{
				lbList.DisplayMemberPath = value;
			}
		}
		
		public string SelectedValuePath
		{
			get
			{
				return lbList.SelectedValuePath;
			}
			set
			{
				lbList.SelectedValuePath = value;
			}
		}

		public Style TextFieldStyle
		{
			get
			{
				return tbxInputData.Style;
			}
			set
			{
				tbxInputData.Style = value;
			}
		}
		
		public Style ListStyle
		{
			get
			{
				return lbList.Style;
			}
			set
			{
				lbList.Style = value;
			}
		}
		
		public string Text
		{
			get
			{
				return tbxInputData.Text;
			}
			set
			{
				tbxInputData.Text = value;
				lbList.IsDropDownOpen = false;
			}
		}
		
		public bool IsReadOnly
		{
			get
			{
				return tbxInputData.IsReadOnly;
			}
			set
			{
				tbxInputData.IsReadOnly = value;
			}
		}
		

		public new Brush Background
		{
			get
			{
				return tbxInputData.Background;
			}
			set
			{
				tbxInputData.Background = value;
			}
		}
		
		public void Clear ()
		{
			lbList.SelectedIndex = -1;
			tbxInputData.Text = "";
		}
		
		public TextBox getTextBoxItem ()
		{
			return tbxInputData;
		}

		
		public string CurrentText
		{
			get
			{
				return GetValue(CurrentTextProperty).ToString();
			}
			set
			{
				SetCurrentTextDependencyProperty(CurrentTextProperty, value);
				Text = value;
			}
		}
		
        public static readonly DependencyProperty CurrentTextProperty = DependencyProperty.Register("CurrentText", typeof(string),
                typeof(TextBoxDropDownHintControl), new FrameworkPropertyMetadata(default(string),
                FrameworkPropertyMetadataOptions.None,
				new PropertyChangedCallback(OnCurrentTextChanged)));
		
        public event PropertyChangedEventHandler CurrentTextChanged;
        
        void SetCurrentTextDependencyProperty (DependencyProperty property, string value)
        {
            SetValue(property, value);
            if(CurrentTextChanged != null)
            {
            	CurrentTextChanged(this, new PropertyChangedEventArgs(property.ToString()));
            }
        }
        
        private static void OnCurrentTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
        	TextBoxDropDownHintControl control = d as TextBoxDropDownHintControl;
            if (control == null) return;
            control.CurrentText = e.NewValue.ToString();
		}
        
		
		public IEnumerable<object> ItemsSource
		{
			get
			{
				return (IEnumerable<object>)GetValue(ItemsSourceProperty);
			}
			set
			{
				SetItemsSourceDependencyProperty(ItemsSourceProperty, value);
				itemsSource = value;
			}
		}
		
        public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable<object>),
                typeof(TextBoxDropDownHintControl), new FrameworkPropertyMetadata(default(IEnumerable<object>),
                FrameworkPropertyMetadataOptions.None,
				new PropertyChangedCallback(OnItemsSourceChanged)));
		
        public event PropertyChangedEventHandler ItemsSourceChanged;
        
        void SetItemsSourceDependencyProperty (DependencyProperty property, IEnumerable<object> value)
        {
            SetValue(property, value);
            if(ItemsSourceChanged != null)
            {
            	ItemsSourceChanged(this, new PropertyChangedEventArgs(property.ToString()));
            }
        }
        
        private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
        	TextBoxDropDownHintControl control = d as TextBoxDropDownHintControl;
            if (control == null) return;
            control.ItemsSource = (IEnumerable<object>)e.NewValue;
		}
		
	}
}

В нем мы настраиваем поведение элементов и привязки данных.

Далее создадим класс модели:

using System;

namespace TextBoxDropDownHint
{
	public class TestModel
	{
		public int Id {get;set;}
		public string Name {get;set;}
		public string Description {get;set;}
		
		public TestModel() {	}
	}
}

XAML разметка главного окна приложения будет следующей:

<Window x:Class="TextBoxDropDownHint.Window1"
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	Title="TextBoxDropDownHint" Height="300" Width="300"
	xmlns:controls="clr-namespace:TextBoxDropDownHint.Controls">
	<Grid Margin="5">
		<Grid.RowDefinitions>
			<RowDefinition Height="30"></RowDefinition>
			<RowDefinition Height="35"></RowDefinition>
			<RowDefinition Height="35"></RowDefinition>
			<RowDefinition Height="35"></RowDefinition>
		</Grid.RowDefinitions>
		<Grid.ColumnDefinitions>
			<ColumnDefinition Width="100"></ColumnDefinition>
			<ColumnDefinition></ColumnDefinition>
		</Grid.ColumnDefinitions>
		<controls:TextBoxDropDownHintControl 
			Grid.Row="0" Grid.ColumnSpan="2"
			ItemsSource="{Binding TestCollection}"
			HorizontalAlignment="Stretch"
			SelectedValuePath="Id"
			DisplayMemberPath="Name"
			OnSelect="TextBoxDropDownHintControl_OnSelect"
			Height="30" VerticalAlignment="Top">
		</controls:TextBoxDropDownHintControl>
	
		<Label Content="Id" Grid.Row="1" Grid.Column="0"></Label>
		<TextBox Margin="0, 5" VerticalContentAlignment="Center" x:Name="tbxId" Grid.Row="1" Grid.Column="1"></TextBox>
		
		<Label Content="Name" Grid.Row="2" Grid.Column="0"></Label>
		<TextBox Margin="0, 5" VerticalContentAlignment="Center" x:Name="tbxName" Grid.Row="2" Grid.Column="1"></TextBox>
		
		<Label Content="Description" Grid.Row="3" Grid.Column="0"></Label>
		<TextBox Margin="0, 5" VerticalContentAlignment="Center" x:Name="tbxDescription" Grid.Row="3" Grid.Column="1"></TextBox>
		
	</Grid>
</Window>

Исходный код главного окна приложения представлен ниже:

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;

namespace TextBoxDropDownHint
{
	public partial class Window1 : Window
	{
		public Window1()
		{
			InitializeComponent();
			
			
			List<TestModel> coll = new List<TestModel>();
			coll.Add(new TestModel() {Id = 1, Name = "Москва", Description = "Описание города"});
			coll.Add(new TestModel() {Id = 2, Name = "Санкт-Петербург", Description = "Описание города"});
			coll.Add(new TestModel() {Id = 3, Name = "Екатеринбург", Description = "Описание города"});
			coll.Add(new TestModel() {Id = 4, Name = "Уфа", Description = "Описание города"});
			coll.Add(new TestModel() {Id = 5, Name = "Пенза", Description = "Описание города"});
			
			DataContext = new {TestCollection = coll};
		}
		
		void TextBoxDropDownHintControl_OnSelect(object sender, TextBoxDropDownHint.Controls.SelectionChanged e)
		{
			if(e.Value != null)
			{
				TestModel model = e.Value as TestModel;
				if(model == null) return;
				
				tbxId.Text = model.Id.ToString();
				tbxName.Text = model.Name;
				tbxDescription.Text = model.Description;
			}
		}
	}
}

Результаты работы программы представлены на рисунках 1-3

C# WPF XAML Текстовое поле с выпадающим списком
C# WPF XAML Текстовое поле с выпадающим списком
C# WPF XAML Текстовое поле с выпадающим списком