WPF封裝實現(xiàn)懶加載下拉列表控件(支持搜索)
因為項目中PC端前端針對基礎(chǔ)數(shù)據(jù)選擇時的下拉列表做了懶加載控件,PC端使用現(xiàn)成的組件,為保持兩端的選擇方式統(tǒng)一,WPF客戶端上也需要使用懶加載的下拉選擇。
WPF這種懶加載的控件未找到現(xiàn)成可用的組件,于是自己封裝了一個懶加載和支持模糊過濾的下拉列表控件,控件使用了虛擬化加載,解決了大數(shù)據(jù)量時的渲染數(shù)據(jù)卡頓問題,下面是完整的代碼和示例:
一、控件所需的關(guān)鍵實體類
/// <summary> /// 下拉項 /// </summary> public class ComboItem { /// <summary> /// 實際存儲值 /// </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> /// 懶加載下拉分頁對象 /// </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 彈窗動畫 --> <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 ?? "請選擇..."; 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 打開 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)換為可見 /// </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>
對應(yīng)視圖的VM中綁定數(shù)據(jù):
public ILazyDataProvider<ComboItem> MyDataProvider { get; } = new ComboItemProvider(); /// <summary> /// 當(dāng)前選擇值 /// </summary> [ObservableProperty] private ComboItem partSelectedItem;
四、效果圖
以上就是WPF封裝實現(xiàn)懶加載下拉列表控件(支持搜索)的詳細(xì)內(nèi)容,更多關(guān)于WPF下拉列表控件的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C# 中的IComparable和IComparer的使用及區(qū)別
這篇文章主要介紹了C# 中的IComparable和IComparer的使用及區(qū)別,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01c#數(shù)據(jù)綁定之將datatabel的data添加listView
這篇文章主要介紹了c#將DataTabel的data添加ListView的示例,實現(xiàn)功能是通過響應(yīng)UI Textbox 的值向ListView 綁定新添加的紀(jì)錄。 ,需要的朋友可以參考下2014-04-04淺析JAVA中過濾器、監(jiān)聽器、攔截器的區(qū)別
本文通過代碼分析和文字說明的方式給大家淺析JAVA中過濾器、監(jiān)聽器、攔截器的區(qū)別,感興趣的朋友一起看下吧2015-09-09