1. 程式人生 > >[WPF自定義控制元件庫]排序、篩選以及高亮

[WPF自定義控制元件庫]排序、篩選以及高亮

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