WPF封装实现懒加载下拉列表控件(支持搜索)
目录
- 一、控件所需的关键实体类
- 二、懒加载控件视图和数据逻辑
- 三、视图页面使用示例
- 四、效果图
因为项目中PC端前端针对基础数据选择时的下拉列表做了懒加载控件,PC端使用现成的组件,为保持两端的选择方式统一,wpF客户端上也需要使用懒加载的下拉选择。
WPF这种懒加载的控件未找到现成可用的组件,于是自己封装了一个懒加载和支持模糊过滤的下拉列表控件,控件使用了虚拟化加载,解决了大数据量时的渲染数据卡顿问题,下面是完整的代码和示例:
一、控件所需的关键实体类
/// <summary> /// 下拉项 /// </summary> public class ComboItem { /// <summary> /// 实际存储值 /// </summary> public string? ItemValue { get; set; } /// <summary> /// 显示文本 /// </summary> public string? ItemText { get; set; } } /// <summary> /// 懒加载下拉数据源提供器 /// </summary> public class ComboItemProvider : ILazyDataProvider<ComboItem> { private readonly List<ComboItem> _all; public ComboItemProvider() { _all = Enumerable.Range(1, 1000000) .Select(i => new ComboItem { ItemValFuZIUlWBqTue = i.ToString(), ItemText = $"Item {i}" }) .ToList(); } public async Task<PageResult<ComboItem>> FetchAsync(string filter, int pageIndex, int pageSize) { await Task.Delay(100); var q = _all.AsQueryable(); if (!string.IsNullOrEmpty(filter)) q = q.Where(x => x.ItemText.Contains(filter, StringComparison.OrdinalIgnoreCase)); var page = q.Skip(pageIndex * pageSize).Take(pageSize).ToList(); bool has = q.Count() > (pageIndex + 1) * pageSize; return new PageResult<ComboItem> { Items = page, HasMore = has }; } } /// <summary> /// 封装获取数据的接口 /// </summary> /// <typeparam name="T"></typeparam> public interface ILazyDataProvider<T> { Task<PageResult<T>> FetchAsync(string filter, int pageIndex, int pageSize); } /// <summary> /// 懒加载下拉分页对象 /// </summary> /// <typeparam name="T"></typeparam> public class PageResult<T> { public IReadOnlyList<T> Items { get; set; } public bool HasMore { get; set; } }
二、懒加载控件视图和数据逻辑
<UserControl x:Class="LazyComboBoxFinalDemo.Controls.LazyComboBox" XMLns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:LazyComboBoxFinalDemo.Controls"> <UserControl.Resources> <local:ZeroToVisibleConverter x:Key="ZeroToVisibleConverter" /> <!-- 清除按钮样式:透明背景、图标 --> <Style x:Key="ClearButtonStyle" TargetType="Button"> <Setter Property="Background" Value="Transparent" /> <Setter Property="BorderThickness" Value="0" /> <Setter Property="Padding" Value="0" /> <Setter Property="Cursor" Value="Hand" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="Button"> <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" /> </ControlTemplate> </Setter.Value> </Setter> </Style> <!-- ToggleButton 样式 --> <Style x:Key="ComboToggleButtonStyle" TargetType=javascript"ToggleButton"> <Setter Property="Background" Value="White" />js; <Setter Property="BorderBrush" Value="#CCC" /> <Setter Property="BorderThickness" Value="1" /> <Setter Property="Padding" Value="4" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="ToggleButton"> <Border Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="20" /> <ColumnDefinition Width="20" /> </Grid.ColumnDefinitions> <!-- 按钮文本 --> <ContentPresenter Grid.Column="0" Margin="4,0,0,0" VerticalAlignment="Center" Content="{TemplateBinding Content}" /> <!-- 箭头 --> <Path x:Name="Arrow" Grid.Column="2" VerticalAlignment="Center" Data="M 0 0 L 4 4 L 8 0 Z" Fill="Gray" RenderTransformOrigin="0.5,0.5"> <Path.RenderTransform> <RotateTransform Angle="0" /> </Path.RenderTransform> 编程 </Path> <!-- 清除按钮 --> <Button x:Name="PART_ClearButton" Grid.Column="1" Width="16" Height="16" VerticalAlignment="Center" Click="OnClearClick" Style="{StaticResource ClearButtonStyle}" Visibility="Collapsed"> <Path Data="M0,0 L8,8 M8,0 L0,8" Stroke="Gray" StrokeThickness="2" /> </Button> </Grid> </Border> <ControlTemplate.Triggers> <Trigger Property="IsMouseover" Value="True"> <Setter TargetName="PART_ClearButton" Property="Visibility" Value="Visible" /> </Trigger> <DataTrigger Binding="{Binding IsOpen, ElementName=PART_Popup}" Value="True"> <Setter TargetName="Arrow" Property="RenderTransform"> <Setter.Value> <RotateTransform Angle="180" /> </Setter.Value> </Setter> </DataTrigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> <!-- ListBoxItem 悬停/选中样式 --> <Style TargetType="ListBoxItem"> <Setter Property="HorizontalContentAlignment" Value="Stretch" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="ListBoxItem"> <Border x:Name="Bd" Padding="4" Background="Transparent"> <ContentPresenter /> </Border> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="Bd" Property="Background" Value="#EEE" /> </Trigger> <Trigger Property="IsSelected" Value="True"> <Setter TargetName="Bd" Property="Background" Value="#CCC" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> <!-- Popup 边框 --> <Style x:Key="PopupBorder" TargetType="Border"> <Setter Property="CornerRadius" Value="5" /> <Setter Property="Background" Value="White" /> <Setter Property="BorderBrush" Value="#CCC" /> <Setter Property="BorderThickness" Value="2" /> <Setter Property="Padding" Value="10" /> </Style> <!-- 水印 TextBox --> <Style x:Key="WatermarkTextBox" TargetType="TextBox"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="TextBox"> <Grid> <ScrollViewer x:Name="PART_ContentHost" /> <Textblock Margin="4,2,0,0" Foreground="Gray" IsHitTestVisible="False" Text="搜索…" Visibility="{Binding Text.Length, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource ZeroToVisibleConverter}}" /> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </UserControl.Resources> <Grid> <ToggleButton x:Name="PART_Toggle" Click="OnToggleClick" Style="{StaticResource ComboToggleButtonStyle}"> <Grid> <!-- 显示文本 --> <TextBlock Margin="4,0,24,0" VerticalAlignment="Center" Text="{Binding DisplayText, RelativeSource={RelativeSource AncestorType=UserControl}}" /> <!-- 箭头已在模板内,略 --> </Grid> </ToggleButton> <Popup x:Name="PART_Popup" AllowsTransparency="True" PlacementTarget="{Binding ElementName=PART_Toggle}" PopupAnimation="Fade" StaysOpen="False"> <!-- AllowsTransparency 启用透明,PopupAnimation 弹窗动画 --> <Border Width="{Binding ActualWidth, ElementName=PART_Toggle}" Style="{StaticResource PopupBorder}"> <Border.Effect> <DropShadowEffect BlurRadius="15" Opacity="0.7" ShadowDepth="0" Color="#e6e6e6" /> </Border.Effect> <Grid Height="300"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <!-- 搜索框 --> <TextBox x:Name="PART_SearchBox" Margin="0,0,0,8" VerticalAl编程客栈ignment="Center" Style="{StaticResource WatermarkTextBox}" TextChanged="OnSearchChanged" /> <!-- 列表 --> <ListBox x:Name="PART_List" Grid.Row="1" DisplayMemberPath="ItemText" ItemsSource="{Binding Items, RelativeSource={RelativeSource AncestorType=UserControl}}" ScrollViewer.CanContentScroll="True" ScrollViewer.ScrollChanged="OnScroll" SelectionChanged="OnSelectionChanged" VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling" /> </Grid> </Border> </Popup> </Grid> </UserControl>
LazyComboBox.cs
public partial class LazyComboBox : UserControl, INotifyPropertyChanged { public static readonly DependencyProperty ItemsProviderProperty = DependencyProperty.Register(nameof(ItemsProvider), typeof(ILazyDataProvider<ComboItem>), typeof(LazyComboBox), new PropertyMetadata(null)); public ILazyDataProvider<ComboItem> ItemsProvider { get => (ILazyDataProvider<ComboItem>)GetValue(ItemsProviderProperty); set => SetValue(ItemsProviderProperty, value); } public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register(nameof(SelectedItem), typeof(ComboItem), typeof(LazyComboBox), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged)); public ComboItem SelectedItem { get => (ComboItem)GetValue(SelectedItemProperty); set => SetValue(SelectedItemProperty, value); } private static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is LazyComboBox ctrl) { ctrl.Notify(nameof(DisplayText)); } } public ObservableCollection<ComboItem> Items { get; } = new ObservableCollection<ComboItem>(); private string _currentFilter = ""; private int _currentPage = 0; private const int PageSize = 30; public bool HasMore { get; private set; } public string DisplayText => SelectedItem?.ItemText ?? "请选择..."; public LazyComboBox() { InitializeComponent(); } public event PropertyChangedEventHandler PropertyChanged; private void Notify(string prop) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop)); private async void LoadPage(int pageIndex) { if (ItemsProvider == null) return; var result = await ItemsProvider.FetchAsync(_currentFilter, pageIndex, PageSize); if (pageIndex == 0) Items.Clear(); foreach (var it in result.Items) Items.Add(it); HasMore = result.HasMore; PART_Popup.IsOpen = true; } private void OnClearClick(object sender, RoutedEventArgs e) { e.Handled = true; // 阻止事件冒泡,不触发 Toggle 打开 SelectedItem = null; // 清空选中 Notify(nameof(DisplayText)); // 刷新按钮文本 PART_Popup.IsOpen = false; // 确保关掉弹窗 } private void OnToggleClick(object sender, RoutedEventArgs e) { _currentPage = 0; LoadPage(0); PART_Popup.IsOpen = true; } private void OnSearchChanged(object sender, TextChangedEventArgs e) { _currentFilter = PART_SearchBox.Text; _currentPage = 0; LoadPage(0); } private void OnScroll(object sender, ScrollChangedEventArgs e) { if (!HasMore) return; if (e.VerticalOffset >= e.ExtentHeight - e.ViewportHeight - 2) LoadPage(++_currentPage); } private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) { if (PART_List.SelectedItem is ComboItem item) { SelectedItem = item; Notify(nameof(DisplayText)); PART_Popup.IsOpen = false; } } }
转换器
/// <summary> /// 下拉弹窗搜索框根据数据显示专用转换器 /// 用于将0转换为可见 /// </summary> public class ZeroToVisibleConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is int i && i == 0) return Visibility.Visible; return Visibility.Collapsed; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException(); }
三、视图页面使用示例
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下拉列表控件的资料请关注编程客栈(www.devze.com)其它相关文章!
精彩评论