因为项目中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;
四、效果图

 
 
 
 

WPF封装一个懒加载下拉列表控件(支持搜索)的更多相关文章

  1. vb6加载时提示出错,窗体log文件中错误信息为:控件 XX 的类 MSComctlLib.ListView 不是一个已加载的控件类。

    解决办法:单击[工程] -- [部件] 添加此Microsoft Windows Common Controls-6.0 (SP6)部件,如果列表中没有,浏览到~\project\包\Support中 ...

  2. VB 错误日志:MSForms.CommandButton 不是一个已加载的控件类等解决方法

    是由于缺少了fm20.dll这个必要组件 网上找到 然后在工程中引用 找到路径 完美解决

  3. 如何去掉IE控件的垂直滚动条(使用QAxWidget加载IE控件)

    如果使用MFC的CHtmlView或Qt的QAxWidget加载IE控件,载入html文件后都会自动带一个垂直滚动条,我们不想要这个滚动条,改怎么办呢?搜索了一下“隐藏IE控件滚动条”,发现在 htt ...

  4. asp.net读取用户控件,自定义加载用户控件

    1.自定义加载用户控件 ceshi.aspx页面 <html> <body> <div id="divControls" runat="se ...

  5. EasyUI加载树控件自动展开所有目录

    在这里如何加载树控件就不在熬述,在加载树控件后,树的节点全部展开,要在OnLoadSuccess事件中写代码:

  6. QT自定义控件系列(二) --- Loading加载动画控件

    本系列主要使用Qt painter来实现一些基础控件.主要是对平时自行编写的一些自定义控件的总结. 为了简洁.低耦合,我们尽量不使用图片,qrc,ui等文件,而只使用c++的.h和.cpp文件. 由于 ...

  7. C#窗体加载和控件加载不同步导致控件闪烁

    窗体加载和控件加载不同步导致的控件闪烁现象:// 代码块加在父窗体中的任意位置,解决窗体加载和控件加载不同步导致的控件闪烁问题        protected override CreatePara ...

  8. SWF加载器控件 SWFLoaderControl

    SWF加载器控件 书:165 <?xml version="1.0" encoding="utf-8"?> <s:Application xm ...

  9. WPF自定义控件与样式(11)-等待/忙/正在加载状态-控件实现

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要有三种实现方式 ...

  10. 【转】WPF自定义控件与样式(11)-等待/忙/正在加载状态-控件实现

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等. 本文主要有三种实现方式: 简单忙碌状态控件BusyBox: Win8/win10效果忙 ...

随机推荐

  1. 更换Linux系统镜像源

    更换Linux系统镜像源 切换镜像源通常是为了提高软件包下载的速度和稳定性.以下是CentOS 7切换镜像源的一般步骤: 一.安装wget(如果尚未安装) 首先,需要确保系统中安装了wget工具,因为 ...

  2. 在flink消费一段时间kafka后,kafka-group的offset被重置了是怎么回事?

    一.背景 腾讯Flink使用 KafkaSource API创建source端,源码中默认开启了checkpoint的时候提交offset 到kafka-broker.读取kafka数据写入到iceb ...

  3. mac 安装ActiveMQ

    1.http://activemq.apache.org/activemq-5154-release.html 选gz 2.cd apache-activemq-5.15.4/bin/macosx 3 ...

  4. Luogu P3899 湖南集训 更为厉害 题解 [ 紫 ] [ 可持久化线段树 ] [ dfs 序 ] [ 线段树合并 ]

    更为厉害:可持久化做法有点意思,但线段树合并做法就很无脑了. 线段树合并做法 显然有三种 \(b\) 的位置的分类讨论. 当 \(b\) 为 \(a\) 的祖先时 从祖先里选 \(b\),从儿子里选 ...

  5. Python构建包、上传包详细步骤

    1.从git上拉取最新的代码 2.在当前项目目录中创建setup.py文件 setup.py 1 # coding: utf-8 2 ​ 3 """打包 4 " ...

  6. vue+elementui怎样点击table中的单元格触发事件--弹框

    可以先看一下官网中table的自定义列模板代码   <template>   <el-table   :data="tableData"   border   s ...

  7. SQL SERVER日常运维巡检系列之-性能

    前言 做好日常巡检是数据库管理和维护的重要步骤,而且需要对每次巡检日期.结果进行登记,同时可能需要出一份巡检报告. 本系列旨在解决一些常见的困扰: 不知道巡检哪些东西 不知道怎么样便捷体检 机器太多体 ...

  8. Python装饰器:套层壳我变得更强了!

    Python装饰器:套层壳我变得更强了 Python装饰器:套层壳我变得更强了 关于作用域和闭包可以聊点什么? 什么是作用域 什么是闭包 装饰器:套层壳我变得更强了 参考资料 昨天阅读了<Pyt ...

  9. 互联网寒冬下,如何写好一份.NET求职简历?附带简洁免费的简历模板!!!

    前言 在当今互联网行业的寒冬时期,每一位求职者都面临着更为激烈的竞争环境,如何在众多.NET候选人中脱颖而出,成为企业心仪的对象,用心准备一份简历显得尤为重要.简历不仅是个人职业经历的简要概述,更是向 ...

  10. C语言中标准输出的缓冲机制

    什么是缓冲区 缓存区是内存空间的一部分,再内存中,内存空间会预留一定的存储空间,这些存储空间是用来缓冲输入和输出的数据,预留的这部分空间就叫做缓冲区. 其中缓冲区还会根据对应的是输入设备还是输出设备分 ...