65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 中的多选组合框

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2020 年 4 月 19 日

CPOL

5分钟阅读

viewsIcon

37098

downloadIcon

880

多选组合框 - WPF 的自定义控件

目录

概述

WPF 提供了 ListBox 控件,允许用户选择多个项目。然而,ListBox 控件的 UI 没有内置搜索/过滤功能。开发人员需要进行一些变通来实现此功能。此外,需要大量的鼠标交互。是的,您完全可以使用键盘完成所有操作。但这并不是最有效的方式。另一方面,Combobox 具有非常好的 UI,支持搜索和过滤。但是,它不支持多选。

如果我们能将 ListBox 的行为与 Combobox UI 的优点结合起来会怎样?MultiSelectCombobox 正是做了这件事。它提供了搜索/过滤功能以及多选功能。MultiSelectCombobox 试图模仿 ComboBox 的 UI 行为。

特点

  • 内置搜索和过滤支持
  • 可扩展以支持复杂数据类型的自定义搜索和过滤
  • 能够创建和添加新项目(非源集合的一部分)(通过 LookUpContract 用于复杂类型)
  • 易于使用!

设计概述

MultiSelectComboboxRichTextBoxPopupListBox 组成。在 RichTextBox 中输入的文本会被监控和处理。按键时,弹出框将显示并显示源集合中匹配搜索条件的项目。如果没有匹配的集合项,则不会显示。如果找到源集合中的合适项,它将用源集合项替换输入的文本。选定的项目显示为 TextBlock - 行内 UI 元素。

可以通过 Control template 更改各个控件的位置和行为。模板部分定义如下

    [TemplatePart(Name = "rtxt", Type = typeof(RichTextBox))]
    [TemplatePart(Name = "popup", Type = typeof(Popup))]
    [TemplatePart(Name = "lstSuggestion", Type = typeof(ListBox))]
    public sealed partial class MultiSelectCombobox : Control
    {

依赖属性

控件旨在公开使之工作所需的最小属性。

  1. ItemSource (IEnumerable) - 源集合应绑定到此属性。它支持从简单的字符串到复杂类型/实体的集合。
  2. SelectedItems (IList) - 此属性提供用户选择的项目集合。
  3. ItemSeparator (char) - 默认值为 ';'。在控件中,项目由 ItemSeparator char 分隔。如果项目包含空格,这一点很重要。应仔细选择分隔符。此外,为了在输入时指示项目结束或强制控件根据当前输入的文本创建新项目,将使用此字符。另外,如果用户输入的文本与集合中提供的任何项目都不匹配,或者 LookUpContract 不支持从给定文本创建对象,则用户输入的文本将从控件 UI 中删除。创建新项目的支持将在本文档后面讨论。
  4. DisplayMemberPath (string) - 如果 ItemSource 集合是复杂类型,开发人员可能需要覆盖类型的 ToString() 方法,或者定义 DisplayMemberPath 属性。默认值为 string.Empty
  5. LookUpContract (ILookUpContract) - 此属性用于自定义控件的搜索/过滤行为。控件提供默认实现,大多数用户都可以使用。但是,对于复杂类型和/或自定义搜索/过滤行为,用户可以提供实现并更改控件行为。

为高级场景解释 LookUpContract (ILookUpContract)

默认搜索/过滤分别基于 string.StartsWithstring.Equals。对于任何给定项目,如果未设置 DisplayMemberPath,则将 item.ToString() 值发送到过滤机制。如果提供了 DisplayMemberPath,则通过项属性反射获取路径值并发送到过滤机制。这适用于大多数用户。

但是,如果用户需要自定义这些设置/过滤机制,他/她可以提供此接口的实现并绑定到 LookUpContract 属性。控件将遵循新绑定的实现。

ILookUpContract.cs
public interface ILookUpContract
{
	// Whether contract supports creation of new object from user entered text
	bool SupportsNewObjectCreation { get; }
	
	// Method to check if item matches searchString
	bool IsItemMatchingSearchString(object sender, object item, string searchString);
	
	// Checks if item matches searchString or not
	bool IsItemEqualToString(object sender, object item, string seachString);
	
	// Creates object from provided string
	// This method need to be implemented only when SupportsNewObjectCreation is set to true
	object CreateObject(object sender, string searchString);
}
  • IsItemMatchingSearchString - 调用此函数来过滤下拉列表中的建议项。用户输入的文本作为参数传递给此函数。如果项目应显示在给定文本的建议下拉列表中,则返回 true。否则,返回 false

  • IsItemEqualToString - 此函数用于根据用户输入的文本从集合中查找精确的项目。

  • CreateObject - 仅当 SupportsNewObjectCreation 设置为 true 时才应实现此函数。调用此函数以基于提供的文本创建新对象。例如,在 AdvanceLookUpContract 实现中,我们可以通过在控件中输入以 ItemSeparator 结尾的逗号分隔值来创建复杂对象(如上面的 GIF 所示)。这只是一个示例实现。您可以定义自己的格式/解析机制。

  • SupportsNewObjectCreation - 如果此属性设置为 false,则控件不允许用户选择 ItemSource 集合之外的项目。如果此属性设置为 true,则控件允许创建新对象。当控件应允许用户添加新对象时,这非常有用。它还省去了为向现有 SelectedItems/ItemSource 添加新项而创建单独的 TextBox(es) 和按钮的麻烦。

  • DefaultLookUpContract - 如果未向控件提供新实现,则使用此 DefaultLookUpContract 实现。此合同对搜索使用 string.StartsWith,对比较使用 string.Equals。这两种比较都不考虑区域性和大小写。

演示应用程序代码详解

定义 Person

public class Person
{
    public string Name { get; set; }
    public string Company { get; internal set; }
    public string City { get; internal set; }
    public string Zip { get; internal set; }
    public string Info
    {
       get => $"{Name} - {Company}({Zip})";
    }
}

1) 简单场景(最常见)

我们将 DisplayMemberPath 设置为 'Name' 值,以在控件中显示 PersonName。我们只需要定义 ItemSourceSelectedItems 绑定。就是这样!

.XAML 代码

<controls:MultiSelectCombobox ItemSource="{Binding Source, Mode=TwoWay, 
                              UpdateSourceTrigger=PropertyChanged}"
                              SelectedItems="{Binding SelectedItems, 
                              Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                              DisplayMemberPath="Name"
                              ItemSeparator=";"/>

2) 高级场景

如果我们希望在多个属性上进行过滤,或者需要不同的搜索/过滤策略。以及/或者还希望支持直接从 MultiSelectCombobox 创建新的 Person

.XAML 代码

<controls:MultiSelectCombobox ItemSource="{Binding Source, 
                              Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                              SelectedItems="{Binding SelectedItems2, 
                              Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                              DisplayMemberPath="Info"
                              ItemSeparator=";"
                              LookUpContract="{Binding AdvanceLookUpContract}"/>

在 XAML 中,我们将 DisplayMemberPath 设置为 Info 属性。Info 设置为返回 NameCompanyZipCode

AdvanceLookUpContract.cs:在此实现中,我们修改了搜索以考虑 Person 的三个属性。如果这三个属性中的任何一个包含搜索 string,则项目将显示在建议下拉列表中。项目根据 Name 属性从 ItemSource 中选择。我们还将 SupportsNewObjectCreation 设置为 true,这意味着我们可以使用控件创建新的 Person 对象。CreateObject 编写用于解析格式为 {Name},{Company},{Zip}string。通过以这种格式输入以 ItemSeparator 结尾的 string,它将尝试从输入的 string 创建一个对象。如果创建失败,它将从 UI 中删除用户输入的字符串。如果成功创建对象,它将在从 UI 中删除用户输入的文本后,将新创建的对象添加到 UI 和 SelectedItems。

[请注意,以下实现仅用于演示 LookUpContract 功能。此实现效率不高,并且有很大的改进空间。]

public class AdvanceLookUpContract : ILookUpContract
{
    public bool SupportsNewObjectCreation => true;

    public object CreateObject(object sender, string searchString)
    {
        if (searchString?.Count(c => c == ',') != 2)
        {
            return null;
        }

        int firstIndex = searchString.IndexOf(',');
        int lastIndex = searchString.LastIndexOf(',');

        return new Person()
        {
            Name = searchString.Substring(0, firstIndex),
            Company = searchString.Substring(firstIndex + 1, lastIndex - firstIndex - 1),
            Zip = searchString.Length >= lastIndex ? 
                  searchString.Substring(lastIndex + 1) : string.Empty
        };
    }

    public bool IsItemEqualToString(object sender, object item, string seachString)
    {
        if (!(item is Person std))
        {
            return false;
        }

        return string.Compare(seachString, std.Name, 
               System.StringComparison.InvariantCultureIgnoreCase) == 0;
    }

    public bool IsItemMatchingSearchString(object sender, object item, string searchString)
    {
        if (!(item is Person person))
        {
            return false;
        }

        if (string.IsNullOrEmpty(searchString))
        {
            return true;
        }

        return person.Name?.ToLower()?.Contains(searchString?.ToLower()) == true
            || person.Company.ToString().ToLower()?.Contains(searchString?.ToLower()) == true
            || person.Zip?.ToLower()?.Contains(searchString?.ToLower()) == true;
    }
}

历史

  • 2020 年 4 月 19 日:初始版本
  • 2021 年 5 月 26 日:升级控件,进行增强和性能修复
© . All rights reserved.