首页/文章列表/文章详情

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

编程知识762025-04-30评论
因为项目中PC端前端针对基础数据选择时的下拉列表做了懒加载控件,PC端使用现成的组件,为保持两端的选择方式统一,WPF客户端上也需要使用懒加载的下拉选择。
WPF这种懒加载的控件未找到现成可用的组件,于是自己封装了一个懒加载和支持模糊过滤的下拉列表控件,控件使用了虚拟化加载,解决了大数据量时的渲染数据卡顿问题,下面是完整的代码和示例:
 
一、控件所需的关键实体类
1///2///下拉项3///4publicclassComboItem5{6///7///实际存储值8///9publicstring? ItemValue { get;set; }10///11///显示文本12///13publicstring? ItemText { get;set; }14}1516///17///懒加载下拉数据源提供器18///19publicclass ComboItemProvider : ILazyDataProvider20{21privatereadonlyList_all;22publicComboItemProvider()23{24 _all = Enumerable.Range(1,1000000)25 .Select(i =>new ComboItem { ItemValue = i.ToString(), ItemText = $"Item {i}"})26.ToList();27}28publicasyncTask>FetchAsync(stringfilter,intpageIndex,intpageSize)29{30awaitTask.Delay(100);31var q =_all.AsQueryable();32if(!string.IsNullOrEmpty(filter))33 q = q.Where(x => x.ItemText.Contains(filter, StringComparison.OrdinalIgnoreCase));34var page = q.Skip(pageIndex *pageSize).Take(pageSize).ToList();35bool has = q.Count() > (pageIndex + 1) *pageSize;36returnnewPageResult { Items = page, HasMore = has };37}38}3940///41///封装获取数据的接口42///43///44publicinterfaceILazyDataProvider45{46Task>FetchAsync(stringfilter,intpageIndex,intpageSize);47}4849///50///懒加载下拉分页对象51///52///53publicclassPageResult54{55publicIReadOnlyList Items { get;set; }56publicbool HasMore { get;set; }57}
 
二、懒加载控件视图和数据逻辑
1<UserControl2x:Class="LazyComboBoxFinalDemo.Controls.LazyComboBox"3xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"4xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"5xmlns:local="clr-namespace:LazyComboBoxFinalDemo.Controls">67"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">1516"Button">17"Center"VerticalAlignment="Center"/>18192021</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">2930"ToggleButton">31<Border32Padding="{TemplateBinding Padding}"33Background="{TemplateBinding Background}"34BorderBrush="{TemplateBinding BorderBrush}"35BorderThickness="{TemplateBinding BorderThickness}"36CornerRadius="4">37383940"20"/>41"20"/>4243 <!-- 按钮文本 -->44<ContentPresenter45Grid.Column="0"46Margin="4,0,0,0"47VerticalAlignment="Center"48Content="{TemplateBinding Content}"/>49 <!-- 箭头 -->50<Path51x:Name="Arrow"52Grid.Column="2"53VerticalAlignment="Center"54Data="M 0 0 L 4 4 L 8 0 Z"55Fill="Gray"56RenderTransformOrigin="0.5,0.5">5758"0"/>596061 <!-- 清除按钮 -->62<Button63x:Name="PART_ClearButton"64Grid.Column="1"65Width="16"66Height="16"67VerticalAlignment="Center"68Click="OnClearClick"69Style="{StaticResource ClearButtonStyle}"70Visibility="Collapsed">71<Path72Data="M0,0 L8,8 M8,0 L0,8"73Stroke="Gray"74StrokeThickness="2"/>75</Button>7677787980"IsMouseOver"Value="True">81"PART_ClearButton"Property="Visibility"Value="Visible"/>8283"{Binding IsOpen, ElementName=PART_Popup}"Value="True">84"Arrow"Property="RenderTransform">8586"180"/>8788899091929394</Style>95 <!-- ListBoxItem 悬停/选中样式 -->96 <Style TargetType=ListBoxItem">97"HorizontalContentAlignment"Value="Stretch"/>98"Template">99100"ListBoxItem">101<Border102x:Name="Bd"103Padding="4"104Background="Transparent">105106107108"IsMouseOver"Value="True">109"Bd"Property="Background"Value="#EEE"/>110111"IsSelected"Value="True">112"Bd"Property="Background"Value="#CCC"/>113114115116117118</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">130131"TextBox">132133"PART_ContentHost"/>134<TextBlock135Margin="4,2,0,0"136Foreground="Gray"137IsHitTestVisible="False"138Text="搜索…"139Visibility="{Binding Text.Length, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource ZeroToVisibleConverter}}"/>140141142143144</Style>145146147<ToggleButton148x:Name="PART_Toggle"149Click="OnToggleClick"150Style="{StaticResource ComboToggleButtonStyle}">151152 <!-- 显示文本 -->153<TextBlock154Margin="4,0,24,0"155VerticalAlignment="Center"156Text="{Binding DisplayText, RelativeSource={RelativeSource AncestorType=UserControl}}"/>157 <!-- 箭头已在模板内,略 -->158159160<Popup161x:Name="PART_Popup"162AllowsTransparency="True"163PlacementTarget="{Binding ElementName=PART_Toggle}"164PopupAnimation="Fade"165StaysOpen="False">166 <!-- AllowsTransparency 启用透明,PopupAnimation 弹窗动画 -->167"{Binding ActualWidth, ElementName=PART_Toggle}"Style="{StaticResource PopupBorder}">168169<DropShadowEffect170BlurRadius="15"171Opacity="0.7"172ShadowDepth="0"173Color="#e6e6e6"/>174175"300">176177"Auto"/>178"*"/>179180 <!-- 搜索框 -->181<TextBox182x:Name="PART_SearchBox"183Margin="0,0,0,8"184VerticalAlignment="Center"185Style="{StaticResource WatermarkTextBox}"186TextChanged="OnSearchChanged"/>187 <!-- 列表 -->188<ListBox189x:Name="PART_List"190Grid.Row="1"191DisplayMemberPath="ItemText"192ItemsSource="{Binding Items, RelativeSource={RelativeSource AncestorType=UserControl}}"193ScrollViewer.CanContentScroll="True"194ScrollViewer.ScrollChanged="OnScroll"195SelectionChanged="OnSelectionChanged"196VirtualizingStackPanel.IsVirtualizing="True"197VirtualizingStackPanel.VirtualizationMode="Recycling"/>198199200201202
1publicpartialclass LazyComboBox : UserControl, INotifyPropertyChanged2{3publicstaticreadonly DependencyProperty ItemsProviderProperty =4DependencyProperty.Register(nameof(ItemsProvider),typeof(ILazyDataProvider),5typeof(LazyComboBox),newPropertyMetadata(null));67publicILazyDataProviderItemsProvider8{9get =>(ILazyDataProvider)GetValue(ItemsProviderProperty);10set=> SetValue(ItemsProviderProperty, value);11}1213publicstaticreadonly DependencyProperty SelectedItemProperty =14DependencyProperty.Register(nameof(SelectedItem),typeof(ComboItem),15typeof(LazyComboBox),16newFrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));1718public ComboItem SelectedItem19{20get=>(ComboItem)GetValue(SelectedItemProperty);21set=> SetValue(SelectedItemProperty, value);22}2324privatestaticvoid OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)25{26if(dis LazyComboBox ctrl)27{28ctrl.Notify(nameof(DisplayText));29}30}3132publicObservableCollection Items { get; } = newObservableCollection();33privatestring _currentFilter = "";34privateint _currentPage = 0;35privateconstint PageSize = 30;36publicbool HasMore { get;privateset; }37publicstring DisplayText => SelectedItem?.ItemText ?? "请选择...";3839publicLazyComboBox()40{41InitializeComponent();42}4344publicevent PropertyChangedEventHandler PropertyChanged;45privatevoidNotify(string prop) =>PropertyChanged?.Invoke(this,newPropertyChangedEventArgs(prop));4647privateasyncvoidLoadPage(intpageIndex)48{49if (ItemsProvider == null)return;50var result = await ItemsProvider.FetchAsync(_currentFilter, pageIndex, PageSize);51if (pageIndex == 0) Items.Clear();52foreach(varitin result.Items) Items.Add(it);53 HasMore =result.HasMore;54 PART_Popup.IsOpen = true;55}5657privatevoidOnClearClick(object sender, RoutedEventArgs e)58{59 e.Handled = true;// 阻止事件冒泡,不触发 Toggle 打开60 SelectedItem = null;//清空选中61Notify(nameof(DisplayText));//刷新按钮文本62 PART_Popup.IsOpen = false;//确保关掉弹窗63}6465privatevoidOnToggleClick(object sender, RoutedEventArgs e)66{67 _currentPage = 0;68LoadPage(0);69 PART_Popup.IsOpen = true;70}7172privatevoidOnSearchChanged(object sender, TextChangedEventArgs e)73{74 _currentFilter =PART_SearchBox.Text;75 _currentPage = 0;76LoadPage(0);77}7879privatevoidOnScroll(object sender, ScrollChangedEventArgs e)80{81if(!HasMore)return;82if (e.VerticalOffset >= e.ExtentHeight - e.ViewportHeight - 2)83LoadPage(++_currentPage);84}8586privatevoidOnSelectionChanged(object sender, SelectionChangedEventArgs e)87{88if(PART_List.SelectedItemis ComboItem item)89{90 SelectedItem =item;91Notify(nameof(DisplayText));92 PART_Popup.IsOpen = false;93}94}95}
LazyComboBox.cs
1///2///下拉弹窗搜索框根据数据显示专用转换器3///用于将0转换为可见4///5publicclass ZeroToVisibleConverter : IValueConverter6{7publicobjectConvert(object value, Type targetType, object parameter, CultureInfo culture)8{9if(valueisint i && i == 0)10returnVisibility.Visible;11returnVisibility.Collapsed;12}1314publicobjectConvertBack(object value, Type targetType, object parameter, CultureInfo culture)15=>thrownewNotImplementedException();16}
转换器
 
三、视图页面使用示例
xmlns:ctrl="clr-namespace:LazyComboBoxFinalDemo.Controls""10"><ctrl:LazyComboBox Width="200"Height="40"ItemsProvider="{Binding MyDataProvider}"SelectedItem="{Binding PartSelectedItem, Mode=TwoWay}"/>
//对应视图的VM中绑定数据:
publicILazyDataProvider MyDataProvider { get; } =newComboItemProvider();//////当前选择值///[ObservableProperty]private ComboItem partSelectedItem;

 

四、效果图

 

 
 
 
 

流浪阿丁

这个人很懒...

用户评论 (0)

发表评论

captcha