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         <Style x:Key="ClearButtonStyle" TargetType="Button">  10             <Setter Property="Background" Value="Transparent" />  11             <Setter Property="BorderThickness" Value="0" />  12             <Setter Property="Padding" Value="0" />  13             <Setter Property="Cursor" Value="Hand" />  14             <Setter Property="Template">  15                 <Setter.Value>  16                     <ControlTemplate TargetType="Button">  17                         <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />  18                     </ControlTemplate>  19                 </Setter.Value>  20             </Setter>  21         </Style>  22         <!--  ToggleButton 样式  -->  23         <Style x:Key="ComboToggleButtonStyle" TargetType="ToggleButton">  24             <Setter Property="Background" Value="White" />  25             <Setter Property="BorderBrush" Value="#CCC" />  26             <Setter Property="BorderThickness" Value="1" />  27             <Setter Property="Padding" Value="4" />  28             <Setter Property="Template">  29                 <Setter.Value>  30                     <ControlTemplate TargetType="ToggleButton">  31                         <Border  32                             Padding="{TemplateBinding Padding}"  33                             Background="{TemplateBinding Background}"  34                             BorderBrush="{TemplateBinding BorderBrush}"  35                             BorderThickness="{TemplateBinding BorderThickness}"  36                             CornerRadius="4">  37                             <Grid>  38                                 <Grid.ColumnDefinitions>  39                                     <ColumnDefinition />  40                                     <ColumnDefinition Width="20" />  41                                     <ColumnDefinition Width="20" />  42                                 </Grid.ColumnDefinitions>  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                                     <Path.RenderTransform>  58                                         <RotateTransform Angle="0" />  59                                     </Path.RenderTransform>  60                                 </Path>  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                             </Grid>  78                         </Border>  79                         <ControlTemplate.Triggers>  80                             <Trigger Property="IsMouseOver" Value="True">  81                                 <Setter TargetName="PART_ClearButton" Property="Visibility" Value="Visible" />  82                             </Trigger>  83                             <DataTrigger Binding="{Binding IsOpen, ElementName=PART_Popup}" Value="True">  84                                 <Setter TargetName="Arrow" Property="RenderTransform">  85                                     <Setter.Value>  86                                         <RotateTransform Angle="180" />  87                                     </Setter.Value>  88                                 </Setter>  89                             </DataTrigger>  90                         </ControlTemplate.Triggers>  91                     </ControlTemplate>  92                 </Setter.Value>  93             </Setter>  94         </Style>  95         <!--  ListBoxItem 悬停/选中样式  -->  96         <Style TargetType="ListBoxItem">  97             <Setter Property="HorizontalContentAlignment" Value="Stretch" />  98             <Setter Property="Template">  99                 <Setter.Value> 100                     <ControlTemplate TargetType="ListBoxItem"> 101                         <Border 102                             x:Name="Bd" 103                             Padding="4" 104                             Background="Transparent"> 105                             <ContentPresenter /> 106                         </Border> 107                         <ControlTemplate.Triggers> 108                             <Trigger Property="IsMouseOver" Value="True"> 109                                 <Setter TargetName="Bd" Property="Background" Value="#EEE" /> 110                             </Trigger> 111                             <Trigger Property="IsSelected" Value="True"> 112                                 <Setter TargetName="Bd" Property="Background" Value="#CCC" /> 113                             </Trigger> 114                         </ControlTemplate.Triggers> 115                     </ControlTemplate> 116                 </Setter.Value> 117             </Setter> 118         </Style> 119         <!--  Popup 边框  --> 120         <Style x:Key="PopupBorder" TargetType="Border"> 121             <Setter Property="CornerRadius" Value="5" /> 122             <Setter Property="Background" Value="White" /> 123             <Setter Property="BorderBrush" Value="#CCC" /> 124             <Setter Property="BorderThickness" Value="2" /> 125             <Setter Property="Padding" Value="10" /> 126         </Style> 127         <!--  水印 TextBox  --> 128         <Style x:Key="WatermarkTextBox" TargetType="TextBox"> 129             <Setter Property="Template"> 130                 <Setter.Value> 131                     <ControlTemplate TargetType="TextBox"> 132                         <Grid> 133                             <ScrollViewer x:Name="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                         </Grid> 141                     </ControlTemplate> 142                 </Setter.Value> 143             </Setter> 144         </Style> 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             <!--  AllowsTransparency 启用透明,PopupAnimation 弹窗动画  --> 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>

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

 1  public 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

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

 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> [ObservableProperty] private ComboItem partSelectedItem;

 

四、效果图

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

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

 

 

 
 
 

发表评论

评论已关闭。

相关文章