WPF封裝實(shí)現(xiàn)懶加載下拉列表控件(支持搜索)
因?yàn)轫?xiàng)目中PC端前端針對(duì)基礎(chǔ)數(shù)據(jù)選擇時(shí)的下拉列表做了懶加載控件,PC端使用現(xiàn)成的組件,為保持兩端的選擇方式統(tǒng)一,WPF客戶(hù)端上也需要使用懶加載的下拉選擇。
WPF這種懶加載的控件未找到現(xiàn)成可用的組件,于是自己封裝了一個(gè)懶加載和支持模糊過(guò)濾的下拉列表控件,控件使用了虛擬化加載,解決了大數(shù)據(jù)量時(shí)的渲染數(shù)據(jù)卡頓問(wèn)題,下面是完整的代碼和示例:
一、控件所需的關(guān)鍵實(shí)體類(lèi)
/// <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)用轉(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)格式化SQL語(yǔ)句的示例代碼
這篇文章主要為大家詳細(xì)介紹了C#如何實(shí)現(xiàn)格式化SQL語(yǔ)句的功能,文中的示例代碼簡(jiǎn)潔易懂,具有一定的借鑒價(jià)值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-08-08C#實(shí)現(xiàn)NPOI的Excel導(dǎo)出詳解
這篇文章主要介紹了C#實(shí)現(xiàn)NPOI的Excel導(dǎo)出的示例代碼,文中的實(shí)現(xiàn)過(guò)程講解詳細(xì),對(duì)我們的學(xué)習(xí)或工作有一定的幫助,感興趣的可以跟隨小編一起學(xué)習(xí)一下2022-01-01Unity3D實(shí)現(xiàn)人物移動(dòng)示例
這篇文章主要為大家詳細(xì)介紹了Unity3D實(shí)現(xiàn)人物移動(dòng)示例,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-01-01C# 中的IComparable和IComparer的使用及區(qū)別
這篇文章主要介紹了C# 中的IComparable和IComparer的使用及區(qū)別,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01c#數(shù)據(jù)綁定之將datatabel的data添加listView
這篇文章主要介紹了c#將DataTabel的data添加ListView的示例,實(shí)現(xiàn)功能是通過(guò)響應(yīng)UI Textbox 的值向ListView 綁定新添加的紀(jì)錄。 ,需要的朋友可以參考下2014-04-04淺析JAVA中過(guò)濾器、監(jiān)聽(tīng)器、攔截器的區(qū)別
本文通過(guò)代碼分析和文字說(shuō)明的方式給大家淺析JAVA中過(guò)濾器、監(jiān)聽(tīng)器、攔截器的區(qū)別,感興趣的朋友一起看下吧2015-09-09C#模擬瀏覽器實(shí)現(xiàn)自動(dòng)操作
這篇文章主要為大家詳細(xì)介紹了如何使用C#實(shí)現(xiàn)模擬瀏覽器實(shí)現(xiàn)自動(dòng)操作,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-11-11