开发者

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封装实现懒加载下拉列表控件(支持搜索)

以上就是WPF封装实现懒加载下拉列表控件(支持搜索)的详细内容,更多关于WPF下拉列表控件的资料请关注编程客栈(www.devze.com)其它相关文章!

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新开发

开发排行榜