1. 如何让列表的内容更容易查找

假设有这么一个列表(数据源在本地),由于内容太多,要查找到其中某个想要的数据会比较困难。要优化这个列表,无非就是排序、筛选和高亮。

改造过的结果如上。

2. 排序

在wpf中要实现数据排序的功能有很多种,例如用linq,但这种场景的标准做法是使用collectionviewsource。

collectionviewsource是一种数据集合的代理类。它有两个很重要的属性:

  • source 是数据源的集合;

  • view 是经过处理后的数据视图。

看上去感觉是不是很像数据库里的table和view的关系?

在这个例子里使用collectionviewsource排序的代码如下:

private readonly collectionviewsource _viewsource;

public highlightsample()
{
    initializecomponent();
    _viewsource = new collectionviewsource
    {
        source = employee.allexecutives
    };

    _viewsource.view.culture = new system.globalization.cultureinfo("zh-cn");
    _viewsource.view.sortdescriptions.add(new sortdescription(nameof(employee.firstname), listsortdirection.ascending));
    employeeelement.itemssource = _viewsource.view;
}

这段代码为collectionviewsource的source赋值后,把collectionviewsource的view作为listbox的数据源。其中sortdescriptions用于描述view的排序方式。如果包含中文,别忘记将culture设置为zh-cn

至此排序的功能就实现了。文档中还提到collectionviewsource的其它信息:

您可以将集合视图作为绑定源集合,可用于导航和显示集合中基于排序、 筛选和分组查询,而无需操作基础源集合本身的所有顶层。 如果source实现inotifycollectionchanged接口,所做的更改引起collectionchanged事件传播到view。

由于view不会更改source,因此每个source都可以有多个关联的view。 使用view,可以通过不同方式显示相同数据。 例如,可能希望在页面左侧显示按优先级排序的任务,而在页面右侧显示按区域分组的任务。

3. 筛选

collectionviewsource的view属性类型为icollectionview接口,它提供了filter属性用于实现数据的过滤。在这个例子里实现如下:

_viewsource.view.filter = (obj) => (obj as employee).displayname.tolower().contains(filterelement.text);

private void onfiltertextchanged(object sender, textchangedeventargs e)
{
    if (_viewsource != null)
        _viewsource.view.refresh();
}

这段代码实现了当输入框的文字改变时刷新view的功能。其中refresh方法用于重新创建view,也就是刷新视图。

icollectionview还提供了一个deferrefresh函数,这个函数用于进入延迟循环,该循环可用于将更改合并到视图并延迟自动刷新,在需要多次操作并刷新数据量大的集合时可以用这个函数。

4. 高亮

<textbox x:name="filterelement"
         textchanged="onfiltertextchanged"/>
<listbox name="employeeelement"
         grid.row="1"
         height="200"
         margin="0,8,0,0">
    <listbox.itemtemplate>
        <datatemplate>
            <stackpanel orientation="horizontal">
                <textblock text="{binding displayname}"
                           kino:textblockservice.highlighttext="{binding elementname=filterelement,path=text}" />
            </stackpanel>
        </datatemplate>
    </listbox.itemtemplate>
</listbox>

uwp的高亮可以使用texthighlighter这个类,实现起来很简单。wpf中的高亮则是使用自定义的textblockservice.highlighttext附加属性声明要高亮的文字,然后将textblock的text替换为处理过的inlines,使用方式如上。

private static void markhighlight(textblock target, string highlighttext)
{
    var text = target.text;
    target.inlines.clear();
    if (string.isnullorwhitespace(text))
        return;

    if (string.isnullorwhitespace(highlighttext))
    {
        target.inlines.add(new run { text = text });
        return;
    }

    while (text.length > 0)
    {
        var runtext = string.empty;
        var index = text.indexof(highlighttext, stringcomparison.invariantcultureignorecase);
        if (index > 0)
        {
            runtext = text.substring(0, index);
            target.inlines.add(new run { text = runtext, foreground = _nohighlightbrush });
        }
        else if (index == 0)
        {
            runtext = text.substring(0, highlighttext.length);
            target.inlines.add(new run { text = runtext });
        }
        else if (index == -1)
        {
            runtext = text;
            target.inlines.add(new run { text = runtext, foreground = _nohighlightbrush });
        }

        text = text.substring(runtext.length);
    }
}

这是实现代码。其实用regex.split代码会好看很多,但懒得改了。
本来应该是高亮匹配的文字,但实际使用中发觉把未匹配的文字置灰更好看,就这样实现了。

5. 结语

这篇文章介绍了使用collectionviewsource实现的排序、筛选功能,以及使用附加属性和inlines实现高亮功能。

不过这样实现的高亮功能有个问题:不能定义高亮(或者低亮)的颜色,不管在代码中还是在xaml中。一种可行的方法是参考tooltipservice定义一大堆附加属性,例如这样:

<textbox x:name="filterelement" 
         tooltipservice.tooltip="filter text"
         tooltipservice.horizontaloffset="10"
         tooltipservice.verticaloffset="10"
         textchanged="onfiltertextchanged"/>

这种方式的缺点是这一大堆附加属性会导致代码变得很复杂,难以维护。tooltipservice还可以创建一个tooltip类,把这个类设置为附加属性的值:

<textbox x:name="filterelement" 
         textchanged="onfiltertextchanged">
    <tooltipservice.tooltip>
        <tooltip content="filter text"
                 horizontaloffset="10" 
                 verticaloffset="10"/>
    </tooltipservice.tooltip>
</textbox>

这种方式比较容易维护,但有人可能不明白tooltipservice.tooltip属性的值为什么既可以是文本(或图片等其它内容),又可以是tooltip类型,xaml如何识别。关于这一点我在下一篇文章会讲解,并且重新实现高亮的功能以支持style等功能。

也可以参考searchabletextblock写一个高亮的文本框,一了百了,但我希望通过这个有趣的功能多介绍几种知识。

6. 参考

collectionviewsource class (system.windows.data) microsoft docs

textblock.inlines property (system.windows.controls) microsoft docs

a wpf searchable textblock control with highlighting wpf

7. 源码

textblockservice.cs at master