因为项目中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 : ILazyDataProvider 20{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/// 44publicinterfaceILazyDataProvider 45{46Task >FetchAsync(stringfilter,intpageIndex,intpageSize);47}4849/// 50///懒加载下拉分页对象51///52/// 53publicclassPageResult 54{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 146"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 21</Style>22 <!-- ToggleButton 样式 -->23 <Style x:Key=ComboToggleButtonStyle"TargetType="ToggleButton">2416 20"Button">17 19"Center"VerticalAlignment="Center"/>18 " Background"Value="White"/>25"BorderBrush"Value="#CCC"/>26 "BorderThickness"Value="1"/>27 "Padding"Value="4"/>28 "Template">29 94</Style>95 <!-- ListBoxItem 悬停/选中样式 -->96 <Style TargetType=ListBoxItem">9730 93"ToggleButton">31<Border32Padding="{TemplateBinding Padding}"33Background="{TemplateBinding Background}"34BorderBrush="{TemplateBinding BorderBrush}"35BorderThickness="{TemplateBinding BorderThickness}"36CornerRadius="4">37 9238 787939 43 <!-- 按钮文本 -->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">5740 "20"/>41 "20"/>42 58 6061 <!-- 清除按钮 -->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>7677"0"/>59 80 91"IsMouseOver"Value="True">81 83"PART_ClearButton"Property="Visibility"Value="Visible"/>82 "{Binding IsOpen, ElementName=PART_Popup}"Value="True">84 90"Arrow"Property="RenderTransform">85 8986 88"180"/>87 " HorizontalContentAlignment"Value="Stretch"/>98"Template">99 118</Style>119 <!-- Popup 边框 -->120 <Style x:Key=PopupBorder"TargetType="Border">121100 117"ListBoxItem">101<Border102x:Name="Bd"103Padding="4"104Background="Transparent">105 116106107 108 115"IsMouseOver"Value="True">109 111"Bd"Property="Background"Value="#EEE"/>110 "IsSelected"Value="True">112 114"Bd"Property="Background"Value="#CCC"/>113 " 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 144</Style>145" Template">130131 143"TextBox">132 142133 141"PART_ContentHost"/>134<TextBlock135Margin="4,2,0,0"136Foreground="Gray"137IsHitTestVisible="False"138Text="搜索…"139Visibility="{Binding Text.Length, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource ZeroToVisibleConverter}}"/>140 147<ToggleButton148x:Name="PART_Toggle"149Click="OnToggleClick"150Style="{StaticResource ComboToggleButtonStyle}">151 202152 <!-- 显示文本 -->153<TextBlock154Margin="4,0,24,0"155VerticalAlignment="Center"156Text="{Binding DisplayText, RelativeSource={RelativeSource AncestorType=UserControl}}"/>157 <!-- 箭头已在模板内,略 -->158 159160<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}">168 200201169<DropShadowEffect170BlurRadius="15"171Opacity="0.7"172ShadowDepth="0"173Color="#e6e6e6"/>174 175"300">176 199177 180 <!-- 搜索框 -->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"/>198"Auto"/>178 "*"/>179


1publicpartialclass LazyComboBox : UserControl, INotifyPropertyChanged2{3publicstaticreadonly DependencyProperty ItemsProviderProperty =4DependencyProperty.Register(nameof(ItemsProvider),typeof(ILazyDataProvider),5typeof(LazyComboBox),newPropertyMetadata(null));67publicILazyDataProvider ItemsProvider8{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}


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中绑定数据:
publicILazyDataProviderMyDataProvider { get; } =newComboItemProvider();/// ///当前选择值///[ObservableProperty]private ComboItem partSelectedItem;
四、效果图