天天看点

WPF:DataGrid可过滤、多语言介绍背景怎么运作自定义控件如何使用基准

目录

介绍

背景

怎么运作

自定义控件

如何使用

基准

  • 下载演示项目 - 133.8 KB
  • GitHub 存储库
  • NuGet 包
WPF:DataGrid可过滤、多语言介绍背景怎么运作自定义控件如何使用基准

介绍

本文介绍如何创建一个自定义DataGrid控件,该DataGrid控件继承自基本控件类并覆盖一些方法来为每一列实现过滤器,就像Excel一样。

您必须掌握C#编程的基础知识,并具有良好的WPF水平。

演示应用程序使用MVVM设计模式,但不需要掌握此模式即可实现此控件。

背景

在一个专业项目中,我不得不响应一个用户的请求,该用户希望能够像Excel一样过滤数据列表的列。

由于该用户在日常工作中习惯使用Excel,因此过滤器的使用使他可以快速了解要过滤的信息和要执行的操作。

我首先在Internet上搜索了建议的解决方案,这些解决方案可以让我实现这个新功能,但我没有发现它们令人满意或不完整。

因此,我从这些解决方案和代码片段中汲取灵感,开发了满足客户要求的控件datagrid自定义。

感谢所有帮助创建此控件的匿名开发人员。

怎么运作

如何过滤数据从datagrid跨多个列,而不必创建代码,看起来像一个炼油厂?

答案是允许使用多个谓词进行过滤的ICollectionView。

有关信息,请参阅MSDN的ICollectionView文档说明:

“您可以将集合视图视为绑定源集合之上的一个层,它允许您根据排序、过滤和分组查询来导航和显示集合,而无需操作底层源集合本身”

基本的单列过滤可以总结如下:

<DataGrid
x:Name="MyDataGrid"
Width="300"
Height="300"/>
           
...
// the test class
internal class DataTest
{
    public string Letter {get; set; }
}

public MainWindow()
{
   InitializeComponent();

   // the text to filter
   string searchText  = "a"

   // the data
   List<DataTest> data = new List<DataTest> {
   new DataTest{Letter = "A"},
   new DataTest{Letter = "B"},
   new DataTest{Letter = "C"}
   new DataTest{Letter = "D"}};

   // the collection view
   ICollectionView itemsView = CollectionViewSource.GetDefaultView(data);

   // set the ItemSource of the DataGrid control into xaml page
   MyDataGrid.ItemSource = itemsView;

   // the filter
   itemsView.Filter = delegate(object o)
   {
     var item = (DataTest)o;

     // if found returns false (hidden), otherwise returns true (displayed)
     return item?.Letter.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) < 0;
   };
}
           

不显示字母“A”。

WPF:DataGrid可过滤、多语言介绍背景怎么运作自定义控件如何使用基准

这个工作正常,但是如何过滤第二列?

让我们开始通过添加新的属性Number到DataTest类中。

// the test class
internal class DataTest
{
  public string Letter {get; set; }
  public int Number {get; set; }
}

// the string to be filtered
string searchText  = "a"

// adding new string to be filtered
string searchNumber = "2";

// initialize the list with new data
List <DataTest> data = new List <DataTest> {
new DataTest{Letter = "A", Number = 1},
new DataTest{Letter = "B", Number = 2},
new DataTest{Letter = "C", Number = 3},
new DataTest{Letter = "D", Number = 4}
new DataTest{Letter = "E", Number = 5}};

// the collection view and the ItemSource of the DataGrid remains unchanged

// adding a list of Predicate
var criteria = new List<Predicate<DataTest>>();

// ignore case
var ignoreCase = StringComparison.OrdinalIgnoreCase;

// add a criterion for each column
criteria.Add(e => e! = null && e.Letter.IndexOf(searchText, ignoreCase) <0);
criteria.Add(e => e! = null &&
             e.Number.ToString().IndexOf(searchNumber, ignoreCase) < 0);

// the new filter multi criteria
itemsView.Filter = delegate(object o)
{
    var item = o as DataTest;

    // return true (displayed) by all criteria
    return criteria.TrueForAll(x => x (item));
};
           

不显示字母“A”和数字2。

WPF:DataGrid可过滤、多语言介绍背景怎么运作自定义控件如何使用基准

这里也很好用,但是如何使用相同的标准过滤多个元素?

例如,过滤器[A,d]从列Letter和过滤器[2,3]从列Number。

这就是有趣的地方,用string 数组替换两个string变量“searchText”和“searchNumber” 。

string[] searchText  = {"a", "d"};
string[] searchNumber = {"2", "3"};

// modify the two criteria, the search is now carried out on arrays,
// and no longer on the value
// of each property.

criteria.Add(e => e != null && !searchText.Contains(e.Letter.ToLower()));
criteria.Add(e => e != null && !searchNumber.Contains(e.Number.ToString()));

// the filter code remains unchanged
           

不显示字母[A, D]和数字[2, 3]。

WPF:DataGrid可过滤、多语言介绍背景怎么运作自定义控件如何使用基准

该演示解释了自定义DataGrid控件的基本操作原理。

自定义控件

注意:由于多位开发者的贡献、修复和优化,本文中的代码与源代码之间可能存在一些差异,但原理是相同的。

在上面的例子中,我使用了类DataTest并初始化了一个列表来提供DataGrid。

为了让过滤器起作用,我必须知道类的名称和每个字段的名称,为了解决这个问题,反射来拯救我们,但首先,让我们看看标头自定义的实现。

注意:所有源文件都在FilterDataGrid项目的文件夹中。

为了简化本文,我不会详细介绍自定义列标题,您将在文件FilterDataGrid.xaml中找到DataGridColumn.HeaderTemplate属性的DataTemplate。

在这一点上,重要的是要知道header custom包含一个Button,一个弹出窗口,它本身包含一个TextBox用于搜索,一个 ListBox,一个TreeView,以及两个OK和Cancel Button。

WPF:DataGrid可过滤、多语言介绍背景怎么运作自定义控件如何使用基准

当DataGrid被初始化,一些方法被按照特定的顺序调用,所有的这些方法已经被替换,以提供一个具体的实现。

我们目前感兴趣的是OnInitialized,OnAutoGeneratingColumn和OnItemsSourceChanged。

FilterDataGrid 类(简化):

WPF:DataGrid可过滤、多语言介绍背景怎么运作自定义控件如何使用基准

该方法OnInitialized仅处理XAML页面(AutoGenerateColumns="False")代码中手动定义的列。

  • DataGridTemplateColumn
  • DataGridTextColumn
<control: FilterDataGrid.Columns>
  <control: DataGridTextColumn IsColumnFiltered="True" ... />
  <control: DataGridTemplateColumn IsColumnFiltered="True" FieldName="LastName" ... />
           

这两种类型的列已经扩展了其基类,并且实现了两个DependencyProperty,IsColumnFiltered和FieldName,查看文件DataGridColumn.cs。

DataGridTemplateColumn和DataGridTextColumn类:

WPF:DataGrid可过滤、多语言介绍背景怎么运作自定义控件如何使用基准

循环遍历DataGrid的可用列,并用HeaderTemplate自定义模板替换原始模板。

对于DataGridTemplateColumn类型的列,在实现自定义列时必须填写该FieldName属性,因为绑定可以在模板中的任何类型的控件上完成,例如TextBox或Label。

// FilterLanguage : default : 0 (english)
Translate = new Loc {Language = (int) FilterLanguage};

// DataGridTextColumn
var column = (DataGridTextColumn)col;
column.HeaderTemplate = (DataTemplate)FindResource("DataGridHeaderTemplate");
column.FieldName = ((Binding)column.Binding).Path.Path;
           

方法OnAutoGeneratingColumn仅处理类型为自动生成的列,System.Windows.Controls.DataGridTextColumn对该方法的调用与列数一样多,当前列包含在DataGridAutoGeneratingColumnEventArgs事件处理程序中。

e.Column = new DataGridTextColumn
{
            FieldName = e.PropertyName,
            IsColumnFiltered = true,
            HeaderTemplate = (DataTemplate)FindResource("DataGridHeaderTemplate")
            ...
};
           

方法OnItemsSourceChanged负责初始化ICollectionView和定义过滤器(稍后将详细介绍)以及重新初始化之前加载的源集合。

// initialize the collection view, the data source is ItemsSource of the DataGrid
CollectionViewSource =
          System.Windows.Data.CollectionViewSource.GetDefaultView(ItemsSource);

// set Filter
CollectionViewSource.Filter = Filter;

// get type of data source
collectionType = ItemsSource?.Cast<object>().First().GetType();
           

应用程序运行后的自定义列标题。

WPF:DataGrid可过滤、多语言介绍背景怎么运作自定义控件如何使用基准

单击按钮(向下箭头)时,会打开一个弹出窗口,此弹出窗口的所有内容均由方法ShowFilterCommand生成,ExecutedRoutedEventArgs命令事件包含属性OriginalSource(按钮)。

它不是弹出窗口的父级按钮,而是标题(按钮是其子级)。

使用VisualTreeHelper类的方法可以浏览可视化树并“发现”我们感兴趣的元素。

注意:您将在FilterHelpers.cs中找到所有这些类及其方法。

一旦检索到标题,我们就可以在树中向下检索其他元素。

WPF:DataGrid可过滤、多语言介绍背景怎么运作自定义控件如何使用基准
button = (Button) e.OriginalSource;
var header = VisualTreeHelpers.FindAncestor<DataGridColumnHeader>(button);
popup = VisualTreeHelpers.FindChild<Popup>(header, "FilterPopup");
var columnType = header.Column.GetType();

// get field name from binding Path
if (columnType == typeof(DataGridTextColumn))
{
 var column = (DataGridTextColumn) header.Column;
 fieldName = column.FieldName;
}

// get field name by custom DependencyProperty "FieldName"
if (columnType == typeof(DataGridTemplateColumn))
{
 var column = (DataGridTemplateColumn)header.Column;
 fieldName = column.FieldName;
}

// get type of field
Type fieldType = null;
var fieldProperty = collectionType.GetProperty(fieldName);

// get type or get underlying type if nullable
if (fieldProperty! = null)
 fieldType = Nullable.GetUnderlyingType
             (fieldProperty.PropertyType)??fieldProperty.PropertyType;
           

在本章的开头,我们看到对于每个字段,我们需要一个值列表和一个谓词,因为我们预先忽略了字段的数量,我们必须使用一个特定的类来包含这些信息以及一些方法和字段用于管理过滤器和树结构(在DateTime类型控件的情况下)。

FilterCommon 类(简化):

WPF:DataGrid可过滤、多语言介绍背景怎么运作自定义控件如何使用基准

这个类的AddFilter方法,负责将谓词添加到Dictionary全局作用域中,在FilterDataGrid类中声明。

// Dictionary declaration, the string key is the name of the field
private readonly Dictionary<string, Predicate<object>> criteria =
                                    new Dictionary<string, Predicate<object>>();
           
public void AddFilter(Dictionary<string, Predicate<object>> criteria)
{
  if (IsFiltered) return;

  // predicat
  bool Predicate(object o)
  {
    var value = o.GetType().GetProperty(FieldName)?.GetValue(o, null);

    // find the value in the list of values (PreviouslyFilteredItems)
    return! PreviouslyFilteredItems.Contains(value);
  }

  criteria.Add(FieldName, Predicate);
  IsFiltered = true;
}
           

让我们继续检查当前字段的过滤器是否已经存在于过滤器列表中,如果是,我们恢复它,否则我们创建一个新的。

// if no filter, add new filter for the current fieldName
CurrentFilter = GlobalFilterList.FirstOrDefault(f => f.FieldName == fieldName) ??
                     new FilterCommon
                     {
                       FieldName = fieldName,
                       FieldType = fieldType
                     };
           

至此,我们已经实现了之前演示中看到的过滤器操作所需的所有元素。

  • IcollectionView
  • 要过滤的值列表
  • 谓词

是时候根据要过滤的字段类型填写ListBox或TreeView了。

该包含在视图中的DataGrid属性Items显示项目的集合,而不是与包含数据源的ItemsSource混淆。

反射用于检索字段的值。

sourceObjectList = new List<object>();

// retrieves the values of the "FieldName" field and removes the duplicates.
sourceObjectList = Items.Cast<object>()
.Select (x => x.GetType().GetProperty(fieldName)?.GetValue(x, null))
.Distinct() // clear duplicate values before select
.Select(item = > item)
.ToList();
           

我们将这些原始值保存在一个列表中,稍后将使用该列表来比较要过滤的元素和未过滤的元素。

// only the raw values of the items of the datagrid view
rawValuesDataGridItems = new List<object>(sourceObjectList);
           

如果该字段的名称等于最后一个过滤字段的名称,则该字段已过滤的值将添加到此列表中。

if (lastName == CurrentFilter.FieldName)
sourceObjectList.AddRange(PreviouslyFilteredItems);
           

表示为checkbox取决于字段类型,DateTime对于TreeView和所有其他类型的ListBox。

因此,有必要创建另一个对象集合来管理搜索和由复选框触发的事件,以获得“已检查”或“未检查”状态,正是这种状态将决定要过滤的值。

负责这个的类是FilterItem.

FilterItem 类(简化):

WPF:DataGrid可过滤、多语言介绍背景怎么运作自定义控件如何使用基准

该字段Label是所显示的值,该字段Content是原始值,IsChecked是checkbox的状态,并且IsDateChecked被用于日期。

// add the first element (select all) at List
var filterItemList = new List<FilterItem>{new FilterItem
                             {Id = 0, Label = Loc.All, IsChecked = true}};

// fill the list
for (var i = 0; i < sourceObjectList.Count; i ++)
{
  var item = sourceObjectList[i];
  var filterItem = new FilterItem
      {
        Id        = filterItemList.Count,
        FieldType = fieldType,
        Content   = item,
        Label     = item? .ToString (), // Content displayed

        // check or uncheck if the item (value)
        // exists in the previously filtered elements
        IsChecked =! CurrentFilter?.PreviouslyFilteredItems.Contains(item) ?? false
      };

  filterItemList.Add(filterItem);
}
           

剩下的就是将这个集合传递给ListBox的ItemsSource属性,或者在DateTime类型字段的情况下为TreeView生成层次树(参见FilterCommon类的BuildTree 方法)。

if (fieldType == typeof(DateTime))
{
  // TreeView
  treeview = VisualTreeHelpers.FindChild<TreeView>(popup.Child, "PopupTreeview");
  treeview.ItemsSource = CurrentFilter?.BuildTree(sourceObjectList, lastFilterName);
  ...
}
else {
  // ListBox
  listBox = VisualTreeHelpers.FindChild<ListBox>(popup.Child, "PopupListBox");
  listBox.ItemsSource = filterItemList;
  ...
}
           

每个PopUp都包含一个搜索TextBox来过滤项目,这个功能需要一个特别专用的过滤器,再次使用ICollectionView。

// set CollectionView
ItemCollectionView =
    System.Windows.Data.CollectionViewSource.GetDefaultView(filterItemList);

// set filter in popup
ItemCollectionView.Filter = SearchFilter;
           

最后,PopUp是开放的。

popup.IsOpen = true;
           

例如PopUp,我在DataTest测试类中添加了一个DateTime字段进行演示。

ListBox和TreeView:

WPF:DataGrid可过滤、多语言介绍背景怎么运作自定义控件如何使用基准
WPF:DataGrid可过滤、多语言介绍背景怎么运作自定义控件如何使用基准

在这两种情况下,搜索都是通过ListBox或TreeView进行的,并且只显示包含搜索值的元素,在验证时,这些元素仍然显示在DataGrid中。

选中的项在DataGrid中保持可见,其他项目的原始值存储在每个过滤器的PreviouslyFilteredItems列表中,此操作在单击确定按钮时的ApplyFilterCommand方法中进行。

方法ApplyFilterCommand的任务是使要过滤的元素列表保持最新。

Except和Intersect是Linq方法,这里有一个图表解释了这两个操作是如何工作的。

WPF:DataGrid可过滤、多语言介绍背景怎么运作自定义控件如何使用基准
// unchecked items : list of the content of the items to filter
var uncheckedItems = new List<object>();

// checked items : list of the content of items not to be
filtered var checkedItems = new List<object>();

// to test if unchecked items are checked again
var contain = false;

// items already filtered
var previousFilteredItems = new List<object>(CurrentFilter.PreviouslyFilteredItems);

// get all items listbox/treeview from popup
var viewItems = ItemCollectionView?.Cast<FilterItem>().Skip(1).ToList()??
                new List<FilterItem>();

// items to be not filtered (checked)
checkedItems = viewItems.Where(f => f.IsChecked).Select(f => f.Content).ToList();

// unchecked:
// the search variable (bool) indicates if this is the search result
// rawValuesDataGridItems is only items displayed in datagrid

if (search) {
  uncheckedItems = rawValuesDataGridItems.Except(checkedItems).ToList();
}
else {
uncheckedItems = viewItems.Where(f =>! f.IsChecked).Select (f => f.Content).ToList();
}
           

日期代码的工作原理相同,只是项目列表是通过FilterCommon类的GetAllItemsTree方法检索的。

// get the list of dates from the TreeView (any state: checked / not checked)
var dateList = CurrentFilter.GetAllItemsTree();

// items to be not filtered (checked)
checkedItems = dateList.Where(f => f.IsChecked).Select(f => f.Content).ToList();
           

一旦检索到这些列表(checkedItems和uncheckedItems),我们必须测试是否有任何过滤项再次被checkedItems和previousFilteredItems之间的交集检查过。

// check if unchecked(filtered) items have been checked
 contain = checkedItems.Intersect(previousFilteredItems).Any();

 // if that is the case !
 // remove filtered items that should no longer be filtered
 if (contain)
   previousFilteredItems = previousFilteredItems.Except(checkedItems).ToList();

// add the previous filtered items to the list of new items to filter
uncheckedItems.AddRange(previousFilteredItems);
           

填写HashSet PreviouslyFilteredItems要过滤的元素,HashSet自动删除重复项。

HashSet使用它是因为它是最快的搜索。

// fill the PreviouslyFilteredItems HashSet with unchecked items
  CurrentFilter.PreviouslyFilteredItems =
         new HashSet<object>(uncheckedItems, EqualityComparer<object>.Default);

// add a filter to criteria dictionary if it is not already added previously
if (!CurrentFilter.IsFiltered)
  CurrentFilter.AddFilter(criteria);

// add current filter to GlobalFilterList
if (GlobalFilterList.All(f => f.FieldName! = CurrentFilter.FieldName))
                        GlobalFilterList.Add(CurrentFilter);

// set the current field name as the last filter name
lastFilter = CurrentFilter.FieldName;
           

以下语句触发过滤器并刷新DataGrid视图。

// apply filter
CollectionViewSource.Refresh();

// remove the current filter if there is no items to filter
if (!CurrentFilter.PreviouslyFilteredItems.Any())
      RemoveCurrentFilter();
           

最后,这里有两种过滤方法,第一种是通过聚合所有谓词来应用过滤器的方法,第二种是所有弹出窗口的搜索方法。

考虑到前面的操作,对列表的每个元素执行Aggregate 操作。

// Filter by aggregation of dictionary predicates.
private bool Filter (object o)
{
  return criteria.Values.Aggregate(true,
         (prevValue, predicate) => prevValue && predicate (o));
}

// Common search filter for all popups
private bool SearchFilter(object obj)
{
  var item = (FilterItem) obj;

  // item.Id == 0, is the item (select all)
  if (string.IsNullOrEmpty(searchText) || item == null || item.Id == 0) return true;

  // DateTime type
  if (item.FieldType == typeof(DateTime))
  return ((DateTime?) item.Content)?.ToString ("d") // date format
                      .IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0;
  // other types
  return item.Content?.ToString().IndexOf
         (searchText, StringComparison.OrdinalIgnoreCase) > = 0;
}
           

如何使用

  1. 它们有两种安装方式 
    • Nuget命令: Install Package FilterDataGrid.
    • 或者添加FilDataGrid项目作为对主项目的引用。
  2. 将FilterDataGrid控件添加到XAML页面中:
    • Namespace:
xmlns:control="http://filterdatagrid.control.com/2021"
           
  • Control:
<control:FilterDataGrid
            FilterLanguge="Italian"
            ShowStatusBar="True"
            ShowElapsedTime="True"
            DateFormatString="d" ...
           

* 如果添加自定义列,则必须设置AutoGenerateColumns="False".

  • 属性:
    • 翻译来自谷歌翻译。如果您发现任何错误或想添加其他语言,请告诉我。
    • 可用语言:英语、法语、俄语、德语、意大利语、中文、荷兰语
    • ShowStatusBar:显示状态栏,默认: false
    • ShowElapsedTime:在状态栏显示过滤经过的时间,默认: false
    • DateFormatString:日期显示格式,默认:“d”
    • FilterLanguage:翻译成可用语言,默认:英语
    • 自定义文本列:
<control:FilterDataGrid.Columns>
    <Control:DataGridTextColumn IsColumnFiltered="true" ...
           
  • 自定义模板列

*该DataGridTemplateColumn的属性FieldName就是必需的。

<control:FilterDataGrid.Columns>
     <control:DataGridTemplateColumn IsColumnFiltered="True"
                                     FieldName="LastName" ...
           

基准

英特尔酷睿 i7,2.93 GHz,16 GB,Windows 10,64位。使用长度在5到8个字母之间的随机不同名称生成器在演示应用程序的“LastName”列上进行测试。

已用时间根据列数和过滤元素数减少。

行数 弹出窗口的打开 应用过滤器 总计(弹出窗口 + 过滤器)
1000 < 1 秒 < 1 秒 < 1 秒
100,000 < 1 秒 < 1 秒 < 1 秒
500,000 ± 1.5 秒 < 1 秒 ± 2.5 秒
1 000 000 ± 3 秒 ± 1.5 秒 ± 4.5 秒

您可以通过激活状态栏ShowStatusBar="True"和ShowElapsedTime="True"来显示经过的时间。

WPF:DataGrid可过滤、多语言介绍背景怎么运作自定义控件如何使用基准

https://www.codeproject.com/Articles/5292782/WPF-DataGrid-Filterable-Multi-Language

继续阅读