因为项目中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>
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
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;
四、效果图
来源链接:https://www.cnblogs.com/adingfirstlove/p/18855065
© 版权声明
本站所有资源来自于网络,仅供学习与参考,请勿用于商业用途,否则产生的一切后果将由您(转载者)自己承担!
如有侵犯您的版权,请及时联系3500663466#qq.com(#换@),我们将第一时间删除本站数据。
如有侵犯您的版权,请及时联系3500663466#qq.com(#换@),我们将第一时间删除本站数据。
THE END
暂无评论内容