it编程 > App开发 > Windows Phone

WPF封装实现懒加载下拉列表控件(支持搜索)

21人参与 2025-04-30 Windows Phone

因为项目中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 { 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>
/// 封装获取数据的接口
/// </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="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}}" />
                <!--  箭头已在模板内,略  -->
            </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;  // 阻止事件冒泡,不触发 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下拉列表控件的资料请关注代码网其它相关文章!

(0)
打赏 微信扫一扫 微信扫一扫

您想发表意见!!点此发布评论

推荐阅读

Audition焦点指示器怎么调节亮度? Au更改焦点指示器亮度的技巧

04-27

使用WPF实现一个虚拟键盘的代码示例

04-27

wps调用Outlook 批量发送电子邮件时持续弹出警告框怎么办?

02-08

WPF实现自绘仪表盘Gauge

01-01

WPF数据绑定时出现StringFormat失效的原因和解决方法

12-11

WPF实现自定义控件的几种方法

12-07

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论