WPF封裝實(shí)現(xiàn)懶加載下拉列表控件(支持搜索)
因?yàn)轫?xiàng)目中PC端前端針對(duì)基礎(chǔ)數(shù)據(jù)選擇時(shí)的下拉列表做了懶加載控件,PC端使用現(xiàn)成的組件,為保持兩端的選擇方式統(tǒng)一,WPF客戶端上也需要使用懶加載的下拉選擇。
WPF這種懶加載的控件未找到現(xiàn)成可用的組件,于是自己封裝了一個(gè)懶加載和支持模糊過(guò)濾的下拉列表控件,控件使用了虛擬化加載,解決了大數(shù)據(jù)量時(shí)的渲染數(shù)據(jù)卡頓問(wèn)題,下面是完整的代碼和示例:
一、控件所需的關(guān)鍵實(shí)體類
/// <summary>
/// 下拉項(xiàng)
/// </summary>
public class ComboItem
{
/// <summary>
/// 實(shí)際存儲(chǔ)值
/// </summary>
public string? ItemValue { get; set; }
/// <summary>
/// 顯示文本
/// </summary>
public string? ItemText { get; set; }
}
/// <summary>
/// 懶加載下拉數(shù)據(jù)源提供器
/// </summary>
public class ComboItemProvider : ILazyDataProvider<ComboItem>
{
private readonly List<ComboItem> _all;
public ComboItemProvider()
{
_all = Enumerable.Range(1, 1000000)
.Select(i => new ComboItem { ItemValue = 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>
/// 封裝獲取數(shù)據(jù)的接口
/// </summary>
/// <typeparam name="T"></typeparam>
public interface ILazyDataProvider<T>
{
Task<PageResult<T>> FetchAsync(string filter, int pageIndex, int pageSize);
}
/// <summary>
/// 懶加載下拉分頁(yè)對(duì)象
/// </summary>
/// <typeparam name="T"></typeparam>
public class PageResult<T>
{
public IReadOnlyList<T> Items { get; set; }
public bool HasMore { get; set; }
}二、懶加載控件視圖和數(shù)據(jù)邏輯
<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" />
<!-- 清除按鈕樣式:透明背景、圖標(biāo) -->
<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="ToggleButton">
<Setter Property="Background" Value="White" />
<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}}" />
<!-- 箭頭已在模板內(nèi),略 -->
</Grid>
</ToggleButton>
<Popup
x:Name="PART_Popup"
AllowsTransparency="True"
PlacementTarget="{Binding ElementName=PART_Toggle}"
PopupAnimation="Fade"
StaysOpen="False">
<!-- AllowsTransparency 啟用透明,PopupAnimation 彈窗動(dòng)畫(huà) -->
<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"
VerticalAlignment="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 ?? "請(qǐng)選擇...";
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; // 阻止事件冒泡,不觸發(fā) Toggle 打開(kāi)
SelectedItem = null; // 清空選中
Notify(nameof(DisplayText)); // 刷新按鈕文本
PART_Popup.IsOpen = false; // 確保關(guān)掉彈窗
}
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;
}
}
}
轉(zhuǎn)換器
/// <summary>
/// 下拉彈窗搜索框根據(jù)數(shù)據(jù)顯示專用轉(zhuǎn)換器
/// 用于將0轉(zhuǎn)換為可見(jiàn)
/// </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();
}
三、視圖頁(yè)面使用示例
xmlns:ctrl="clr-namespace:LazyComboBoxFinalDemo.Controls"
<Grid Margin="10">
<ctrl:LazyComboBox
Width="200"
Height="40"
ItemsProvider="{Binding MyDataProvider}"
SelectedItem="{Binding PartSelectedItem, Mode=TwoWay}" />
</Grid>對(duì)應(yīng)視圖的VM中綁定數(shù)據(jù):
public ILazyDataProvider<ComboItem> MyDataProvider { get; }
= new ComboItemProvider();
/// <summary>
/// 當(dāng)前選擇值
/// </summary>
[ObservableProperty]
private ComboItem partSelectedItem;四、效果圖


以上就是WPF封裝實(shí)現(xiàn)懶加載下拉列表控件(支持搜索)的詳細(xì)內(nèi)容,更多關(guān)于WPF下拉列表控件的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#代碼實(shí)現(xiàn)在Excel中創(chuàng)建雷達(dá)圖
本文演示了如何在?C#?中創(chuàng)建填充型雷達(dá)圖,通過(guò)設(shè)置數(shù)據(jù)、樣式與圖表屬性,實(shí)現(xiàn)多維數(shù)據(jù)的可視化展示,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下2026-05-05
C# Winfom 中ListBox的簡(jiǎn)單用法詳解
這篇文章主要介紹了C# Winfom 中ListBox的簡(jiǎn)單用法詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12
C#實(shí)現(xiàn)簡(jiǎn)易點(diǎn)餐功能
這篇文章主要為大家詳細(xì)介紹了C#實(shí)現(xiàn)簡(jiǎn)易點(diǎn)餐功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07
c#入門(mén)之枚舉和結(jié)構(gòu)體使用詳解(控制臺(tái)接收字符串以相反的方向輸出)
這篇文章主要介紹了c#入門(mén)之枚舉和結(jié)構(gòu)體使用詳解,最后提供了編寫(xiě)控制臺(tái)應(yīng)用程序接收字符串并做相應(yīng)處理的小示例,需要的朋友可以參考下2014-04-04
unity3d?對(duì)接?workerman?實(shí)現(xiàn)聯(lián)機(jī)游戲功能
workerman?是一款開(kāi)源高性能?PHP?應(yīng)用容器,他除了用于互聯(lián)網(wǎng)、即時(shí)通訊、APP?開(kāi)發(fā)、硬件通訊、智能家居、物聯(lián)網(wǎng)等領(lǐng)域的開(kāi)發(fā)外,這篇文章主要介紹了unity3d?對(duì)接?workerman?實(shí)現(xiàn)聯(lián)機(jī)游戲,需要的朋友可以參考下2022-10-10

