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

因为项目中PC端前端针对基础数据选择时的下拉列表做了懒加载控件,PC端使用现成的组件,为保持两端的选择方式统一,WPF客户端上也需要使用懒加载的下拉选择。
WPF这种懒加载的控件未找到现成可用的组件,于是自己封装了一个懒加载和支持模糊过滤的下拉列表控件,控件使用了虚拟化加载,解决了大数据量时的渲染数据卡顿问题,下面是完整的代码和示例:
 
一、控件所需的关键实体类
 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 }
 
二、懒加载控件视图和数据逻辑
  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     
  7         "ZeroToVisibleConverter" />
  8         <!--  清除按钮样式:透明背景、图标  -->
  9         <Style x:Key=ClearButtonStyle" TargetType="Button">
 10             "Background" Value="Transparent" />
 11             "BorderThickness" Value="0" />
 12             "Padding" Value="0" />
 13             "Cursor" Value="Hand" />
 14             "Template">
 15                 
 16                     "Button">
 17                         "Center" VerticalAlignment="Center" />
 18                     
 19                 
 20             
 21         </Style>
 22         <!--  ToggleButton 样式  -->
 23         <Style x:Key=ComboToggleButtonStyle" TargetType="ToggleButton">
 24             "Background" Value="White" />
 25             "BorderBrush" Value="#CCC" />
 26             "BorderThickness" Value="1" />
 27             "Padding" Value="4" />
 28             "Template">
 29                 
 30                     "ToggleButton">
 31                         <Border
 32                             Padding="{TemplateBinding Padding}"
 33                             Background="{TemplateBinding Background}"
 34                             BorderBrush="{TemplateBinding BorderBrush}"
 35                             BorderThickness="{TemplateBinding BorderThickness}"
 36                             CornerRadius="4">
 37                             
 38                                 
 39                                     
 40                                     "20" />
 41                                     "20" />
 42                                 
 43                                 <!--  按钮文本  -->
 44                                 <ContentPresenter
 45                                     Grid.Column="0"
 46                                     Margin="4,0,0,0"
 47                                     VerticalAlignment="Center"
 48                                     Content="{TemplateBinding Content}" />
 49                                 <!--  箭头  -->
 50                                 <Path
 51                                     x:Name="Arrow"
 52                                     Grid.Column="2"
 53                                     VerticalAlignment="Center"
 54                                     Data="M 0 0 L 4 4 L 8 0 Z"
 55                                     Fill="Gray"
 56                                     RenderTransformOrigin="0.5,0.5">
 57                                     
 58                                         "0" />
 59                                     
 60                                 
 61                                 <!--  清除按钮  -->
 62                                 <Button
 63                                     x:Name="PART_ClearButton"
 64                                     Grid.Column="1"
 65                                     Width="16"
 66                                     Height="16"
 67                                     VerticalAlignment="Center"
 68                                     Click="OnClearClick"
 69                                     Style="{StaticResource ClearButtonStyle}"
 70                                     Visibility="Collapsed">
 71                                     <Path
 72                                         Data="M0,0 L8,8 M8,0 L0,8"
 73                                         Stroke="Gray"
 74                                         StrokeThickness="2" />
 75                                 </Button>
 76 
 77                             
 78                         
 79                         
 80                             "IsMouseOver" Value="True">
 81                                 "PART_ClearButton" Property="Visibility" Value="Visible" />
 82                             
 83                             "{Binding IsOpen, ElementName=PART_Popup}" Value="True">
 84                                 "Arrow" Property="RenderTransform">
 85                                     
 86                                         "180" />
 87                                     
 88                                 
 89                             
 90                         
 91                     
 92                 
 93             
 94         </Style>
 95         <!--  ListBoxItem 悬停/选中样式  -->
 96         <Style TargetType=ListBoxItem">
 97             "HorizontalContentAlignment" Value="Stretch" />
 98             "Template">
 99                 
100                     "ListBoxItem">
101                         <Border
102                             x:Name="Bd"
103                             Padding="4"
104                             Background="Transparent">
105                             
106                         
107                         
108                             "IsMouseOver" Value="True">
109                                 "Bd" Property="Background" Value="#EEE" />
110                             
111                             "IsSelected" Value="True">
112                                 "Bd" Property="Background" Value="#CCC" />
113                             
114                         
115                     
116                 
117             
118         </Style>
119         <!--  Popup 边框  -->
120         <Style x:Key=PopupBorder" TargetType="Border">
121             "CornerRadius" Value="5" />
122             "Background" Value="White" />
123             "BorderBrush" Value="#CCC" />
124             "BorderThickness" Value="2" />
125             "Padding" Value="10" />
126         </Style>
127         <!--  水印 TextBox  -->
128         <Style x:Key=WatermarkTextBox" TargetType="TextBox">
129             "Template">
130                 
131                     "TextBox">
132                         
133                             "PART_ContentHost" />
134                             <TextBlock
135                                 Margin="4,2,0,0"
136                                 Foreground="Gray"
137                                 IsHitTestVisible="False"
138                                 Text="搜索…"
139                                 Visibility="{Binding Text.Length, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource ZeroToVisibleConverter}}" />
140                         
141                     
142                 
143             
144         </Style>
145     
146     
147         <ToggleButton
148             x:Name="PART_Toggle"
149             Click="OnToggleClick"
150             Style="{StaticResource ComboToggleButtonStyle}">
151             
152                 <!--  显示文本  -->
153                 <TextBlock
154                     Margin="4,0,24,0"
155                     VerticalAlignment="Center"
156                     Text="{Binding DisplayText, RelativeSource={RelativeSource AncestorType=UserControl}}" />
157                 <!--  箭头已在模板内,略  -->
158             
159         
160         <Popup
161             x:Name="PART_Popup"
162             AllowsTransparency="True"
163             PlacementTarget="{Binding ElementName=PART_Toggle}"
164             PopupAnimation="Fade"
165             StaysOpen="False">
166             <!--  AllowsTransparency 启用透明,PopupAnimation 弹窗动画  -->
167             "{Binding ActualWidth, ElementName=PART_Toggle}" Style="{StaticResource PopupBorder}">
168                 
169                     <DropShadowEffect
170                         BlurRadius="15"
171                         Opacity="0.7"
172                         ShadowDepth="0"
173                         Color="#e6e6e6" />
174                 
175                 "300">
176                     
177                         "Auto" />
178                         "*" />
179                     
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                 
199             
200         
201     
202 
 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
 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 }
转换器
 
三、视图页面使用示例
xmlns:ctrl="clr-namespace:LazyComboBoxFinalDemo.Controls"
"10">
    <ctrl:LazyComboBox
        Width="200"
        Height="40"
        ItemsProvider="{Binding MyDataProvider}"
        SelectedItem="{Binding PartSelectedItem, Mode=TwoWay}" />

//对应视图的VM中绑定数据:
public ILazyDataProvider MyDataProvider { get; }
    = new ComboItemProvider();

/// 
/// 当前选择值
/// 
[ObservableProperty]
private ComboItem partSelectedItem;

 

四、效果图

 

 
 
 
 
From:https://www.cnblogs.com/adingfirstlove/p/18855065
流浪阿丁
100+评论
captcha