WPF 中自定义懒加载与模糊过滤下拉列表控件的实现

笔记哥 / 04-30 / 13点赞 / 0评论 / 289阅读
因为项目中PC端前端针对基础数据选择时的下拉列表做了懒加载控件,PC端使用现成的组件,为保持两端的选择方式统一,WPF客户端上也需要使用懒加载的下拉选择。 WPF这种懒加载的控件未找到现成可用的组件,于是自己封装了一个懒加载和支持模糊过滤的下拉列表控件,控件使用了虚拟化加载,解决了大数据量时的渲染数据卡顿问题,下面是完整的代码和示例: 一、控件所需的关键实体类 ```csharp 1 /// 2 /// 下拉项 3 /// 4 public class ComboItem 5 { 6 /// 7 /// 实际存储值 8 /// 9 public string? ItemValue { get; set; } 10 /// 11 /// 显示文本 12 /// 13 public string? ItemText { get; set; } 14 } 15 16 /// 17 /// 懒加载下拉数据源提供器 18 /// 19 public class ComboItemProvider : ILazyDataProvider 20 { 21 private readonly List _all; 22 public ComboItemProvider() 23 { 24 _all = Enumerable.Range(1, 1000000) 25 .Select(i => new ComboItem { ItemValue = i.ToString(), ItemText = $"Item {i}" }) 26 .ToList(); 27 } 28 public async Task> FetchAsync(string filter, int pageIndex, int pageSize) 29 { 30 await Task.Delay(100); 31 var q = _all.AsQueryable(); 32 if (!string.IsNullOrEmpty(filter)) 33 q = q.Where(x => x.ItemText.Contains(filter, StringComparison.OrdinalIgnoreCase)); 34 var page = q.Skip(pageIndex * pageSize).Take(pageSize).ToList(); 35 bool has = q.Count() > (pageIndex + 1) * pageSize; 36 return new PageResult { Items = page, HasMore = has }; 37 } 38 } 39 40 /// 41 /// 封装获取数据的接口 42 /// 43 /// 44 public interface ILazyDataProvider 45 { 46 Task> FetchAsync(string filter, int pageIndex, int pageSize); 47 } 48 49 /// 50 /// 懒加载下拉分页对象 51 /// 52 /// 53 public class PageResult 54 { 55 public IReadOnlyList Items { get; set; } 56 public bool HasMore { get; set; } 57 } ``` 二、懒加载控件视图和数据逻辑 ```csharp 1 6 7 8 9 22 23 95 96 119 120 127 128 145 146 147 151 152 153 157 158 159 160 166 167 168 169 174 175 176 177 178 179 180 181 187 188 198 199 200 201 202 ``` ![](https://cdn.res.knowhub.vip/c/2505/07/472632c7.gif?G0EAAMTWZmzajZpMhqy4MW4c7OTXtuBzkETyRFc%2bwBclewRJZEudY4c%2fxg8VE%2bOeHr9%2bIjc3nEm4dlWnN7Hc)![](https://cdn.res.knowhub.vip/c/2505/07/f9fcb557.gif?G0QAAMRyW2zQiWwEEsGQEPT9%2bftn%2fLnZtuBzkEbyRFc%2bwPIUPYKEoacY%2bQ5%2ftA%2fCyko1PT5seyz%2f6jqvmcXGe6BvBOIb) ```csharp 1 public partial class LazyComboBox : UserControl, INotifyPropertyChanged 2 { 3 public static readonly DependencyProperty ItemsProviderProperty = 4 DependencyProperty.Register(nameof(ItemsProvider), typeof(ILazyDataProvider), 5 typeof(LazyComboBox), new PropertyMetadata(null)); 6 7 public ILazyDataProvider ItemsProvider 8 { 9 get => (ILazyDataProvider)GetValue(ItemsProviderProperty); 10 set => SetValue(ItemsProviderProperty, value); 11 } 12 13 public static readonly DependencyProperty SelectedItemProperty = 14 DependencyProperty.Register(nameof(SelectedItem), typeof(ComboItem), 15 typeof(LazyComboBox), 16 new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged)); 17 18 public ComboItem SelectedItem 19 { 20 get => (ComboItem)GetValue(SelectedItemProperty); 21 set => SetValue(SelectedItemProperty, value); 22 } 23 24 private static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 25 { 26 if (d is LazyComboBox ctrl) 27 { 28 ctrl.Notify(nameof(DisplayText)); 29 } 30 } 31 32 public ObservableCollection Items { get; } = new ObservableCollection(); 33 private string _currentFilter = ""; 34 private int _currentPage = 0; 35 private const int PageSize = 30; 36 public bool HasMore { get; private set; } 37 public string DisplayText => SelectedItem?.ItemText ?? "请选择..."; 38 39 public LazyComboBox() 40 { 41 InitializeComponent(); 42 } 43 44 public event PropertyChangedEventHandler PropertyChanged; 45 private void Notify(string prop) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop)); 46 47 private async void LoadPage(int pageIndex) 48 { 49 if (ItemsProvider == null) return; 50 var result = await ItemsProvider.FetchAsync(_currentFilter, pageIndex, PageSize); 51 if (pageIndex == 0) Items.Clear(); 52 foreach (var it in result.Items) Items.Add(it); 53 HasMore = result.HasMore; 54 PART_Popup.IsOpen = true; 55 } 56 57 private void OnClearClick(object sender, RoutedEventArgs e) 58 { 59 e.Handled = true; // 阻止事件冒泡,不触发 Toggle 打开 60 SelectedItem = null; // 清空选中 61 Notify(nameof(DisplayText)); // 刷新按钮文本 62 PART_Popup.IsOpen = false; // 确保关掉弹窗 63 } 64 65 private void OnToggleClick(object sender, RoutedEventArgs e) 66 { 67 _currentPage = 0; 68 LoadPage(0); 69 PART_Popup.IsOpen = true; 70 } 71 72 private void OnSearchChanged(object sender, TextChangedEventArgs e) 73 { 74 _currentFilter = PART_SearchBox.Text; 75 _currentPage = 0; 76 LoadPage(0); 77 } 78 79 private void OnScroll(object sender, ScrollChangedEventArgs e) 80 { 81 if (!HasMore) return; 82 if (e.VerticalOffset >= e.ExtentHeight - e.ViewportHeight - 2) 83 LoadPage(++_currentPage); 84 } 85 86 private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) 87 { 88 if (PART_List.SelectedItem is ComboItem item) 89 { 90 SelectedItem = item; 91 Notify(nameof(DisplayText)); 92 PART_Popup.IsOpen = false; 93 } 94 } 95 } ``` LazyComboBox.cs ![](https://cdn.res.knowhub.vip/c/2505/07/ae4945b1.gif?G0EAAMTWZmzajZpMhqy4MW4c7OTXtuBzkETyRFc%2bwBclewRJZEudY4c%2fxg8VE%2bOeHr9%2bIjc3nEm4dlWnN7Hc)![](https://cdn.res.knowhub.vip/c/2505/07/c373af5a.gif?G0QAAMRyW2zQiWwEEsGQEPT9%2bftn%2fLnZtuBzkEbyRFc%2bwPIUPYKEoacY%2bQ5%2ftA%2fCyko1PT5seyz%2f6jqvmcXGe6BvBOIb) ```csharp 1 /// 2 /// 下拉弹窗搜索框根据数据显示专用转换器 3 /// 用于将0转换为可见 4 /// 5 public class ZeroToVisibleConverter : IValueConverter 6 { 7 public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 8 { 9 if (value is int i && i == 0) 10 return Visibility.Visible; 11 return Visibility.Collapsed; 12 } 13 14 public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 15 => throw new NotImplementedException(); 16 } ``` 转换器 三、视图页面使用示例 ```csharp xmlns:ctrl="clr-namespace:LazyComboBoxFinalDemo.Controls" ``` ```csharp ``` //对应视图的VM中绑定数据: ```csharp public ILazyDataProvider MyDataProvider { get; } = new ComboItemProvider(); /// /// 当前选择值 /// [ObservableProperty] private ComboItem partSelectedItem; ``` 四、效果图 ![](https://cdn.res.knowhub.vip/c/2505/07/8ad4e915.png?G1YAAETn9LyUAmjYvtMdbIlTE20GJLIIKiWs13vO2jfR94dALD%2bj9Rn7w19an0EM9YJKAikwJK9gmJg6OLko41L1vEYA) ![](https://cdn.res.knowhub.vip/c/2505/07/2b581ed5.png?G1YAAMTsdJzIJyKl26hD2jvFHc2ARBZBpYT1es9Z%2byb6fheIxme0Pn1%2f%2bEvr04mRSoaRQDIUwScwVLRUTeEqlU3NclzDAQ%3d%3d)