qidao123.com技术社区-IT企服评测·应用市场
标题:
WPF封装一个懒加载下拉列表控件(支持搜索)
[打印本页]
作者:
反转基因福娃
时间:
2025-4-30 16:06
标题:
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
22
23
95
96
119
120
127
128
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
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>
复制代码
[/code]//对应视图的VM中绑定命据:[code]public ILazyDataProvider<ComboItem> MyDataProvider { get; }
= new ComboItemProvider();
/// <summary>
/// 当前选择值
/// </summary>
[ObservableProperty]
private ComboItem partSelectedItem;
复制代码
四、结果图
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 qidao123.com技术社区-IT企服评测·应用市场 (https://dis.qidao123.com/)
Powered by Discuz! X3.4