WPF 的 AutoSuggest 和 AutoComplete 控件






4.75/5 (12投票s)
在 WPF 中创建遵循 MVVM 模式的 AutoSuggest/AutoComplete 控件
引言
在本文中,我将分享我在 WPF 中创建遵循 MVVM 模式的 AutoSuggest/AutoComplete 控件的经验。
为什么我们需要 AutoSuggest 控件?
Web 浏览器的一体化发展首次突显了 AutoSuggest 控件功能的真正好处——用户不仅需要浏览,还需要帮助,在输入单词或短语的一部分后,需要以建议列表的形式获得即时反馈。另一方面,呈现数据量的增加使得在没有应用程序帮助的情况下从列表中选择简单的任务变得乏味。最后,在难以使用下拉列表的触摸屏界面中缩短查找和选择时间,使得 AutoSuggest 控件不可或缺。
背景
我目前正在参与一个 WPF 桌面应用程序开发,该应用程序管理合同。
为了改善用户体验并加快表单录入速度,我决定在 TextBox 中使用控件,通过键盘输入文本。虽然有些控件需要从选项列表中选择,而实现这些控件最自然的方式(不使用 ComboBox)是使用 AutoSuggest/AutoComplete 控件。
我曾尝试查找现成的用户控件,但不幸的是,我找到的几乎所有控件都基于 Microsoft 的 ComboBox 进行实现,并且只添加了简单的项过滤功能。在找不到我想要的东西之后,我开始与一位好朋友,杰出的软件开发者 Orlin Petrov 讨论构建此类 AutoSuggest/AutoComplete 控件的最佳方法。就这样,我们决定一起合作——他负责主要的架构角色,我负责大部分实现。KOControls 就这样诞生了。
我提供的代码包含一个 KOControls 库的包,该库(截至本文撰写时)包含通用的 WPF 实用程序、WPF 中 AutoSuggest/AutoComplete 控件的实现以及一个演示如何使用它的示例项目。
代码的实现和使用
用户要求
我们首先创建了一个高级要求列表,我们的控件应满足这些要求。
- 控件必须符合 MVVM 标准,并且 ViewModel应该完全功能化,无需用户界面。
- 它应该能够连接到任何 TextBox并提供AutoSuggest/AutoComplete功能。
- 控件应该有一个选项允许提交“自由文本”,并在确认后将“自由文本”转换为有效值。
- 它应该有一种方式将命令注入建议列表(当您希望允许用户在建议控件内编辑或创建新建议时,这很有用)。
- 它应该能够处理复制粘贴。
- 呈现建议的控件应该是 WPF DataGrid或ListView。
- 控件的功能应该通过 ViewModel的属性来控制。(请注意,只有当控件的DataContext是特定ViewModel类型时,控件才能正常工作)。
- 它应该有一个插入过滤算法的选项。这将允许第三方用户实现一个从webservices或数据库中获取建议值的算法,以进行更智能的过滤,克服拼写错误和自动纠正等问题。应该提供过滤算法的默认实现。
- 控件应该支持使用箭头键在建议列表中上下导航,并且在这样做时不会失去 TextBox的焦点。
- 它应该支持以下“取消”和“选择”行为选项:- 空格键可以作为选择命令或退出命令。如果它作为选择命令,它应该仅在它是建议列表中的最后一个项目时才选择一个项目,并且关闭允许“自由文本”值的选项。
- Tab 键可以作为“选择”或“取消”命令。失去焦点也被视为 Tab 键。
- Enter 键可以作为“选择”或“取消”命令。
- 箭头键可以作为“选择”命令。
 
- 它应该支持在调用过滤算法之前延迟指定的毫秒数。这在过滤算法很重,您不想每次用户键入字符时都调用它,而是希望用户暂停并放慢速度后再调用算法时很有用。
- 控件应该有一个允许或禁止空值的选项。此外,默认情况下,空值是“null”值,但用户可以提供不同的空值。
- 应该有一个 AutoComplete功能,可以在用户停止输入后突出显示已完成的文本。
下面是 AutoSuggestControl 实际运行的屏幕截图。
项目结构
- KO.Controls.Core– 通用的非 UI 实用程序以及核心类和接口。
- KO.Controls.GUI.Core– 通用的 UI 实用程序,如- ICommand实现、转换器和实用程序扩展方法。
- KO.Controls.GUI–- KO.Controls库的 UI 用户控件和- ViewModels。目前包含- AutoSuggest用户控件的实现。
- KO.Controls.AutoSuggestTest– 演示用法并帮助我们测试- AutoSuggestControl功能的测试项目(将有基于当前控件的其他项目)。
- KO.Controls.Samples.Core– 一个包含模拟测试数据的通用库,将与其他测试项目共享。
- KOControls.GUI.Tests- 一个锦上添花的项目。
类结构和实现概述
在开始之前,我想感谢 Orlin Petrov,他的指导和对构建这个出色解决方案的投入是无价的。
在讨论了应该扩展/重用哪些现有的 .NET 控件以满足我们的需求后,我们决定使用 WPF 的 TextBox、Selector(ListView、DataGrid 等)和弹出控件,因为它们提供了最大的灵活性。AutoSuggestControl 是一个您放置在弹出控件内的用户控件。AutoSuggestControl 包含一个选择器控件,连接到任何 TextBox,监听用户的输入,操作选择器控件的项,并驱动 AutoSuggestViewModel。
下面是 AutoSuggestControl 默认模板的视图。
<ResourceDictionary
	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" mc:Ignorable="d"
	xmlns:GUI="clr-namespace:KOControls.GUI"
	xmlns:Core="clr-namespace:KOControls.GUI.Core;assembly=KOControls.GUI.Core">
	<!--AutoSuggestControl_Default_SuggestionsTemplate-->
	<ControlTemplate x:Key="AutoSuggestControl_Default_SuggestionsTemplate">
		<DataGrid x:Name="PART_Selector" 
		ItemsSource="{Binding Suggestions}" SelectionMode="Single">
		</DataGrid>
	</ControlTemplate>
	<!--AutoSuggestControl_Default_CommandsTemplate-->
	<ControlTemplate x:Key="AutoSuggestControl_Default_CommandsTemplate">
		<ItemsControl IsTabStop="False" ItemsSource="{Binding}">
			<ItemsControl.ItemTemplate>
				<DataTemplate>
					<Button Height="25"
						Command="{Binding}"
						Content="{Binding Header}">
					</Button>
				</DataTemplate>
			</ItemsControl.ItemTemplate>
			<ItemsControl.ItemsPanel>
				<ItemsPanelTemplate>
					<StackPanel x:Name="buttonMenuPanel" 
					Orientation="Horizontal" 
					VerticalAlignment="Top">
					</StackPanel>
				</ItemsPanelTemplate>
			</ItemsControl.ItemsPanel>
		</ItemsControl>
	</ControlTemplate>
	<!--AutoSuggestControl_DefaultTemplate-->
	<ControlTemplate x:Key="AutoSuggestControl_DefaultTemplate" 
			TargetType="{x:Type GUI:AutoSuggestControl}">
		<Border
			Background="{TemplateBinding Background}"
			BorderBrush="{TemplateBinding BorderBrush}"
			BorderThickness="{TemplateBinding BorderThickness}"
			>
			<Grid>
				<Grid.RowDefinitions>
					<RowDefinition Height="*"></RowDefinition>
					<RowDefinition Height="Auto"></RowDefinition>
				</Grid.RowDefinitions>
				<Control x:Name="_suggestionsContentPresenter" 
					Grid.Row="0"
					Template="{TemplateBinding 
						SuggestionsTemplate}"
					DataContext="{Binding Suggestions}"/>
				<Control x:Name="_commandsContentPresenter" 
					Grid.Row="1"
					Template="{TemplateBinding CommandsTemplate}"
					DataContext="{Binding Commands}"/>
			</Grid>
		</Border>
	</ControlTemplate>
</ResourceDictionary>
视图由两行网格组成。在第一行中,有一个建议呈现器模板,它有一个默认实现的 DataGrid,该 DataGrid 绑定到 ViewModel 中的建议集合。第二行包含命令模板,它有一个默认实现,将命令列为按钮。自定义命令允许用户为 AutoSuggestControl 提供的任何建议添加“新建”、“编辑”和“详细信息”功能。
AutoSuggestControl.cs 类包含驱动并同步 ViewModel、Popup、TextBox 和呈现建议的 Selector(DataGrid 或 ListView)的逻辑。AutoSuggestControl 仅与 AutoSuggestViewModel 一起工作,您不能为其提供任何其他 DataContext。
AutoSuggestViewModel 包含查找建议的所有业务逻辑,以及用户注入的所有选项和命令。实际查找建议是通过插入一个实现下面所示的简单 ISelector 接口的类来完成的。
public interface ISelector
{
IEnumerable Select(object filter);
}
过滤器是 TextBox 中输入的文本,IEnumerable 是找到的建议。我们提供了一个 ISelector 接口的默认实现,该实现使用城市集合,并根据过滤器文本返回匹配的城市。
AutoSuggestViewModel 还有两个重要的依赖属性:Suggestion 和 SuggestionPreview 属性。Suggestion 属性表示当前选定的 Suggestion。SuggestionPreview 属性表示用户即将选择(已获得焦点)但尚未选择的建议。
如何在您的应用程序中使用该控件
为了全面了解 AutoSuggestControl 的用法,我建议您查看 http://code.google.com/p/kocontrols/downloads/list 上的最新源代码。
下面我将描述三个使用 AutoSuggestControl 非常容易实现的场景。
- 您有一个城市列表,并希望为用户提供一种简单快捷的方式,只需输入城市的前几个字母即可选择城市。
- 您有一个城市列表,并希望为用户提供一种简单快捷的方式,通过输入城市的前几个字母来选择城市。如果城市不存在于您的列表中,您希望自动添加它,或者弹出一个输入窗口允许用户将城市添加到列表中。
- 您有一个城市列表,并希望为用户提供一种简单快捷的方式,通过输入城市的前几个字母来选择城市。如果城市不存在于您的列表中,您希望调用“添加新城市”窗口,其中可以添加新城市。如果您想编辑现有城市,您希望快速调用“编辑城市”窗口,可以在其中编辑城市。
我将详细介绍如何实现第一个场景。
模型
创建一个名为“city”的类,它具有如下的“Name”和“Country”属性。
private string name = "";
private Country country = null;
public string Name { get { return name; } set { if(name != value) { name = value;} } }
public Country Country { get { return country; } set { if(country != value) 
	{ country = value; } } }
创建一个名为 Country 的类,它具有如下的 name 属性。
private string name = "";
public string Name { get { return name; } set { if (name != value) { name = value;} } }
创建 AutoSuggestConsumerViewModel,它有一个城市集合和一个 AutoSuggestViewModel 属性,如下所示。
public class AutoSuggestConsumerViewModelBase : DependencyObject
{
	public AutoSuggestViewModel AutoSuggestVM { get; protected set; }
	public IList<City> AllCities { get; set; }
	public AutoSuggestConsumerViewModelBase()
	{
		AllCities = TestDataService.GetCities();
		IValueConverter valueConverter = 
			new ValueConverter(x => x == null ? "" : ((City)x).Name);
		ISelector selector = new AutoSuggestViewModel.DefaultSelector
					(valueConverter, AllCities);
		AutoSuggestVM = new AutoSuggestViewModel(selector, valueConverter);
	}	
}
创建一个视图作为 UserControl,其中包含以下 XAML 代码片段。
	...
<Label Grid.Column="0" Content="City:" HorizontalAlignment="Right" />
<TextBox Grid.Column="1" Width="140" Height="22" Padding="0, 3, 0, 0" 
	VerticalAlignment="Top"
			x:Name="_cityTextBox"/>
	<GUI:Popup x:Name="_popup" Placement="Bottom" 
		PlacementTarget="{Binding ElementName=_cityTextBox}">
		<GUI:AutoSuggestControl x:Name="autoSuggest" Focusable="False" 
			OwnerPopup="{Binding ElementName=_popup}"
			TargetTextBox="{Binding ElementName=_cityTextBox, 
			Mode=OneTime}"
			FrameworkElement.DataContext="{Binding AutoSuggestVM}"
			TaboutTrigger="All" 
			ConfirmTrigger="SpaceTabArrows">
			<GUI:AutoSuggestControl.SuggestionsTemplate>
				<ControlTemplate>
					<DataGrid x:Name="PART_Selector"
					CanUserReorderColumns="False" 
					CanUserSortColumns="False" 
					CanUserAddRows="False" 
					CanUserDeleteRows="False" 
					CanUserResizeColumns="True"
					AutoGenerateColumns="False" 
					IsReadOnly="True" 
					HeadersVisibility="None">
					    <DataGrid.Columns>
						<DataGridTextColumn Header="Name" 
						Binding="{Binding Name}" />
						<DataGridTextColumn 
						Header="Country Name" 
						Binding="{Binding Country.Name}" />
					    </DataGrid.Columns>
					</DataGrid>
				</ControlTemplate>
				</GUI:AutoSuggestControl.SuggestionsTemplate>
			</GUI:AutoSuggestControl>
		</GUI:Popup>
…
上面的 XAML 创建了一个 label、一个 TextBox 和一个具有 AutoSuggestControl 功能的 popup(即,当用户输入城市的前几个字母时,弹出窗口会显示可用的选项)。
KOControls 库的最新源代码以及 WPF DataGrid 中 AutoSuggestControl 的实现信息可以在 http://code.google.com/p/kocontrols 找到。
附加文件中包含一个具有文本应用程序的二进制文件,演示了 AutoSuggestControl 的工作原理,以及 KOControls 的最新源代码(截至本文撰写时)。
希望您喜欢这篇文章。如果您对这个控件感兴趣,我们强烈建议您下载源代码并进行尝试。源代码非常容易理解。
我将非常感谢您的评论和建议,特别是关于如何改进用户控件的想法和反馈。
哦,如果您发现错误,请务必告诉我。
历史
- 2011年11月30日:初版



 
 