因为项目中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下拉列表控件的资料请关注代码网其它相关文章!
发表评论