目錄
介紹
背景
怎麼運作
自定義控件
如何使用
基準
- 下載下傳示範項目 - 133.8 KB
- GitHub 存儲庫
- NuGet 包
介紹
本文介紹如何建立一個自定義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”。
這個工作正常,但是如何過濾第二列?
讓我們開始通過添加新的屬性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。
這裡也很好用,但是如何使用相同的标準過濾多個元素?
例如,過濾器[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]。
該示範解釋了自定義DataGrid控件的基本操作原理。
自定義控件
注意:由于多位開發者的貢獻、修複和優化,本文中的代碼與源代碼之間可能存在一些差異,但原理是相同的。
在上面的例子中,我使用了類DataTest并初始化了一個清單來提供DataGrid。
為了讓過濾器起作用,我必須知道類的名稱和每個字段的名稱,為了解決這個問題,反射來拯救我們,但首先,讓我們看看标頭自定義的實作。
注意:所有源檔案都在FilterDataGrid項目的檔案夾中。
為了簡化本文,我不會詳細介紹自定義列标題,您将在檔案FilterDataGrid.xaml中找到DataGridColumn.HeaderTemplate屬性的DataTemplate。
在這一點上,重要的是要知道header custom包含一個Button,一個彈出視窗,它本身包含一個TextBox用于搜尋,一個 ListBox,一個TreeView,以及兩個OK和Cancel Button。
當DataGrid被初始化,一些方法被按照特定的順序調用,所有的這些方法已經被替換,以提供一個具體的實作。
我們目前感興趣的是OnInitialized,OnAutoGeneratingColumn和OnItemsSourceChanged。
FilterDataGrid 類(簡化):
該方法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類:
循環周遊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();
應用程式運作後的自定義列标題。
單擊按鈕(向下箭頭)時,會打開一個彈出視窗,此彈出視窗的所有内容均由方法ShowFilterCommand生成,ExecutedRoutedEventArgs指令事件包含屬性OriginalSource(按鈕)。
它不是彈出視窗的父級按鈕,而是标題(按鈕是其子級)。
使用VisualTreeHelper類的方法可以浏覽可視化樹并“發現”我們感興趣的元素。
注意:您将在FilterHelpers.cs中找到所有這些類及其方法。
一旦檢索到标題,我們就可以在樹中向下檢索其他元素。
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 類(簡化):
這個類的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 類(簡化):
該字段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:
在這兩種情況下,搜尋都是通過ListBox或TreeView進行的,并且隻顯示包含搜尋值的元素,在驗證時,這些元素仍然顯示在DataGrid中。
選中的項在DataGrid中保持可見,其他項目的原始值存儲在每個過濾器的PreviouslyFilteredItems清單中,此操作在單擊确定按鈕時的ApplyFilterCommand方法中進行。
方法ApplyFilterCommand的任務是使要過濾的元素清單保持最新。
Except和Intersect是Linq方法,這裡有一個圖表解釋了這兩個操作是如何工作的。
// 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;
}
如何使用
- 它們有兩種安裝方式
- Nuget指令: Install Package FilterDataGrid.
- 或者添加FilDataGrid項目作為對主項目的引用。
- 将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"來顯示經過的時間。
https://www.codeproject.com/Articles/5292782/WPF-DataGrid-Filterable-Multi-Language