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下拉列表控件的资料请关注代码网其它相关文章!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论