能杜孱 发表于 2025-5-29 10:54:18

WPF封装一个懒加载下拉列表控件(支持搜索)

因为项目中PC端前端针对基础数据选择时的下拉列表做了懒加载控件,PC端使用现成的组件,为保持两端的选择方式统一,WPF客户端上也需要使用懒加载的下拉选择。WPF这种懒加载的控件未找到现成可用的组件,于是自己封装了一个懒加载和支持模糊过滤的下拉列表控件,控件使用了虚拟化加载,解决了大数据量时的渲染数据卡顿问题,下面是完整的代码和示例: 一、控件所需的关键实体类 1 /// <summary>
2 /// 下拉项
3 /// </summary>
4 public class ComboItem
5 {
6   /// <summary>
7   /// 实际存储值
8   /// </summary>
9   public string? ItemValue { get; set; }
10   /// <summary>
11   /// 显示文本
12   /// </summary>
13   public string? ItemText { get; set; }
14 }
15
16 /// <summary>
17 /// 懒加载下拉数据源提供器
18 /// </summary>
19 public class ComboItemProvider : ILazyDataProvider<ComboItem>
20 {
21   private readonly List<ComboItem> _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<PageResult<ComboItem>> 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<ComboItem> { Items = page, HasMore = has };
37   }
38 }
39
40 /// <summary>
41 /// 封装获取数据的接口
42 /// </summary>
43 /// <typeparam name="T"></typeparam>
44 public interface ILazyDataProvider<T>
45 {
46   Task<PageResult<T>> FetchAsync(string filter, int pageIndex, int pageSize);
47 }
48
49 /// <summary>
50 /// 懒加载下拉分页对象
51 /// </summary>
52 /// <typeparam name="T"></typeparam>
53 public class PageResult<T>
54 {
55   public IReadOnlyList<T> Items { get; set; }
56   public bool HasMore { get; set; }
57 } 二、懒加载控件视图和数据逻辑1 <UserControl
2   x:Class="LazyComboBoxFinalDemo.Controls.LazyComboBox"
3   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
4   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5   xmlns:local="clr-namespace:LazyComboBoxFinalDemo.Controls">
6   <UserControl.Resources>
7         <local:ZeroToVisibleConverter x:Key="ZeroToVisibleConverter" />
8         
9         
22         
23         
95         
96         
119         
120         
127         
128         
145   </UserControl.Resources>
146   <Grid>
147         <ToggleButton
148             x:Name="PART_Toggle"
149             Click="OnToggleClick"
150             Style="{StaticResource ComboToggleButtonStyle}">
151             <Grid>
152               
153               <TextBlock
154                     Margin="4,0,24,0"
155                     VerticalAlignment="Center"
156                     Text="{Binding DisplayText, RelativeSource={RelativeSource AncestorType=UserControl}}" />
157               
158             </Grid>
159         </ToggleButton>
160         <Popup
161             x:Name="PART_Popup"
162             AllowsTransparency="True"
163             PlacementTarget="{Binding ElementName=PART_Toggle}"
164             PopupAnimation="Fade"
165             StaysOpen="False">
166            
167             <Border Width="{Binding ActualWidth, ElementName=PART_Toggle}" Style="{StaticResource PopupBorder}">
168               <Border.Effect>
169                     <DropShadowEffect
170                         BlurRadius="15"
171                         Opacity="0.7"
172                         ShadowDepth="0"
173                         Color="#e6e6e6" />
174               </Border.Effect>
175               <Grid Height="300">
176                     <Grid.RowDefinitions>
177                         <RowDefinition Height="Auto" />
178                         <RowDefinition Height="*" />
179                     </Grid.RowDefinitions>
180                     
181                     <TextBox
182                         x:Name="PART_SearchBox"
183                         Margin="0,0,0,8"
184                         VerticalAlignment="Center"
185                         Style="{StaticResource WatermarkTextBox}"
186                         TextChanged="OnSearchChanged" />
187                     
188                     <ListBox
189                         x:Name="PART_List"
190                         Grid.Row="1"
191                         DisplayMemberPath="ItemText"
192                         ItemsSource="{Binding Items, RelativeSource={RelativeSource AncestorType=UserControl}}"
193                         ScrollViewer.CanContentScroll="True"
194                         ScrollViewer.ScrollChanged="OnScroll"
195                         SelectionChanged="OnSelectionChanged"
196                         VirtualizingStackPanel.IsVirtualizing="True"
197                         VirtualizingStackPanel.VirtualizationMode="Recycling" />
198               </Grid>
199             </Border>
200         </Popup>
201   </Grid>
202 </UserControl> 1public partial class LazyComboBox : UserControl, INotifyPropertyChanged
2{
3      public static readonly DependencyProperty ItemsProviderProperty =
4          DependencyProperty.Register(nameof(ItemsProvider), typeof(ILazyDataProvider<ComboItem>),
5            typeof(LazyComboBox), new PropertyMetadata(null));
6
7      public ILazyDataProvider<ComboItem> ItemsProvider
8      {
9          get => (ILazyDataProvider<ComboItem>)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<ComboItem> Items { get; } = new ObservableCollection<ComboItem>();
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 1 /// <summary>
2 /// 下拉弹窗搜索框根据数据显示专用转换器
3 /// 用于将0转换为可见
4 /// </summary>
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 }转换器 三、视图页面使用示例xmlns:ctrl="clr-namespace:LazyComboBoxFinalDemo.Controls"
<Grid Margin="10">
    <ctrl:LazyComboBox
      Width="200"
      Height="40"
      ItemsProvider="{Binding MyDataProvider}"
      SelectedItem="{Binding PartSelectedItem, Mode=TwoWay}" />
</Grid>//对应视图的VM中绑定数据:public ILazyDataProvider<ComboItem> MyDataProvider { get; }
    = new ComboItemProvider();

/// <summary>
/// 当前选择值
/// </summary>

private ComboItem partSelectedItem; 
四、效果图

 
    
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: WPF封装一个懒加载下拉列表控件(支持搜索)