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)其它相关文章!
加载中,请稍侯......
精彩评论