[WPF自定义控件库]了解如何自定义ItemsControl
1. 前言
对wpf来说contentcontrol和itemscontrol是最重要的两个控件。
顾名思义,itemscontrol表示可用于呈现一组item的控件。大部分时候我们并不需要自定义itemscontrol,因为wpf提供了一大堆itemscontrol的派生类:headereditemscontrol、treeview、menu、statusbar、listbox、listview、combobox;而且配合style或datatemplate足以完成大部分的定制化工作,可以说itemscontrol是xaml系统灵活性的最佳代表。不过,既然它是最常用的控件,那么掌握一些它的原理对所有wpf开发者都有好处。
我以前写过一篇文章介绍如何模仿itemscontrol,并且博客园也已经很多文章深入介绍itemscontrol的原理,所以这篇文章只介绍简单的自定义itemscontrol知识,通过重写getcontainerforitemoverride和isitemitsowncontaineroverride、preparecontainerforitemoverride函数并使用itemcontainergenerator等自定义一个简单的iitemscontrol控件。
2. 介绍作为例子的repeater
作为教学我创建了一个继承自itemscontrol的控件repeater(虽然简单,用来展示资料的话好像还真的有点用)。它的基本用法如下:
<local:repeater> <local:repeateritem content="1234999" label="product id" /> <local:repeateritem content="power projector 4713" label="ignore" /> <local:repeateritem content="projector (pr)" label="category" /> <local:repeateritem content="a very powerful projector with special features for internet usability, usb" label="description" /> </local:repeater>
也可以不直接使用items,而是绑定itemssource并指定displaymemberpath和labelmemberpath。
public class product { public string key { get; set; } public string value { get; set; } public static ienumerable<product> products { get { return new list<product> { new product{key="product id",value="1234999" }, new product{key="ignore",value="power projector 4713" }, new product{key="category",value="projector (pr)" }, new product{key="description",value="a very powerful projector with special features for internet usability, usb" }, new product{key="price",value="856.49 eur" }, }; } } }
<local:repeater itemssource="{x:static local:product.products}" displaymemberpath="value" labelmemberpath="key"/>
运行结果如下图:
3. 实现
确定好需要实现的itemscontrol后,通常我大致会使用三步完成这个itemscontrol:
- 定义itemcontainer
- 关联itemcontainer和itemscontrol
- 实现itemscontrol的逻辑
3.1 定义itemcontainer
派生自itemscontrol的控件通常都会有匹配的子元素控件,如listbox对应listboxitem,combobox对应comboboxitem。如果itemscontrol的items内容不是对应的子元素控件,itemscontrol会创建对应的子元素控件作为容器再把item放进去。
<listbox> <system:string>item1</system:string> <system:string>item2</system:string> </listbox>
例如这段xaml中,item1和item2是listbox的logicalchildren,而它们会被listbox封装到listboxitem,listboxitem才是listbox的visualchildren。在这个例子中,listboxitem可以称作itemcontainer
。
itemscontrol派生类的itemcontainer
控件要使用父元素名称做前缀、-item做后缀,例如combobox的子元素comboboxitem,这是wpf约定俗成的做法(不过也有tabcontrol和tabitem这种例外)。repeater也派生自itemscontrol,repeatertem即为repeater的itemcontainer
控件。
public repeateritem() { defaultstylekey = typeof(repeateritem); } public object label { get => getvalue(labelproperty); set => setvalue(labelproperty, value); } public datatemplate labeltemplate { get => (datatemplate)getvalue(labeltemplateproperty); set => setvalue(labeltemplateproperty, value); }
<style targettype="local:repeateritem"> <setter property="padding" value="8" /> <setter property="template"> <setter.value> <controltemplate targettype="local:repeateritem"> <border borderbrush="{templatebinding borderbrush}" borderthickness="{templatebinding borderthickness}" background="{templatebinding background}"> <stackpanel margin="{templatebinding padding}"> <contentpresenter content="{templatebinding label}" contenttemplate="{templatebinding labeltemplate}" verticalalignment="center" textblock.foreground="#ff777777" /> <contentpresenter x:name="contentpresenter" /> </stackpanel> </border> </controltemplate> </setter.value> </setter> </style>
上面是repeateritem的代码和defaultstyle。repeateritem继承contentcontrol并提供label、labeltemplate。defaultstyle的做法参考contentcontrol。
3.2 关联itemcontainer和itemscontrol
<style targettype="{x:type local:repeater}"> <setter property="scrollviewer.verticalscrollbarvisibility" value="auto" /> <setter property="template"> <setter.value> <controltemplate targettype="{x:type local:repeater}"> <border borderbrush="{templatebinding borderbrush}" borderthickness="{templatebinding borderthickness}" background="{templatebinding background}"> <scrollviewer padding="{templatebinding padding}"> <itemspresenter /> </scrollviewer> </border> </controltemplate> </setter.value> </setter> </style>
如上面xaml所示,repeater的controltemplate中需要提供一个itemspresenter,用于指定itemscontrol中的各item摆放的位置。
[styletypedproperty(property = "itemcontainerstyle", styletargettype = typeof(repeateritem))] public class repeater : itemscontrol { public repeater() { defaultstylekey = typeof(repeater); } protected override bool isitemitsowncontaineroverride(object item) { return item is repeateritem; } protected override dependencyobject getcontainerforitemoverride() { var item = new repeateritem(); return item; } }
repeater的基本代码如上所示。要将repeater和repeateritem关联起来,除了使用约定俗成的命名方式告诉用户,还需要使用下面两步:
重写 getcontainerforitemoverride
protected virtual dependencyobject getcontainerforitemoverride () 用于返回item的container。repeater返回的是repeateritem。
重写 isitemitsowncontainer
protected virtual bool isitemitsowncontaineroverride (object item),确定item是否是(或者是否可以作为)其自己的container。在repeater中,只有repeateritem返回true,即如果item的类型不是repeateritem,就将它作使用repeateritem包装起来。
完成上面几步后,为repeater设置itemssource的话repeater将会创建对应的repeateritem并添加到自己的visualtree下面。
使用 styletypedpropertyattribute
最后可以在repeater上添加styletypedpropertyattribute,指定itemcontainerstyle
的类型为repeateritem
。添加这个attribute后在blend中选择“编辑生成项目的容器(itemcontainerstyle)”就会默认使用repeateritem的样式。
3.3 实现itemscontrol的逻辑
public string labelmemberpath { get => (string)getvalue(labelmemberpathproperty); set => setvalue(labelmemberpathproperty, value); } /*labelmemberpathproperty code...*/ protected virtual void onlabelmemberpathchanged(string oldvalue, string newvalue) { // refresh the label member template. _labelmembertemplate = null; var newtemplate = labelmemberpath; int count = items.count; for (int i = 0; i < count; i++) { if (itemcontainergenerator.containerfromindex(i) is repeateritem repeateritem) preparerepeateritem(repeateritem, items[i]); } } private datatemplate _labelmembertemplate; private datatemplate labelmembertemplate { get { if (_labelmembertemplate == null) { _labelmembertemplate = (datatemplate)xamlreader.parse(@" <datatemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""> <textblock text=""{binding " + labelmemberpath + @"}"" verticalalignment=""center""/> </datatemplate>"); } return _labelmembertemplate; } } protected override void preparecontainerforitemoverride(dependencyobject element, object item) { base.preparecontainerforitemoverride(element, item); if (element is repeateritem repeateritem ) { preparerepeateritem(repeateritem,item); } } private void preparerepeateritem(repeateritem repeateritem, object item) { if (repeateritem == item) return; repeateritem.labeltemplate = labelmembertemplate; repeateritem.label = item; }
repeater本身没什么复杂的逻辑,只是模仿displaymemberpath
添加了labelmemberpath
和labelmembertemplate
属性,并把这个属性和repeateritem的label
和'labeltemplate'属性关联起来,上面的代码即用于实现这个功能。
labelmemberpath和labelmembertemplate
repeater动态地创建一个内容为textblock的datatemplate,这个textblock的text绑定到labelmemberpath
。
xamlreader相关的技术我在如何使用代码创建datatemplate这篇文章里讲解了。
itemcontainergenerator.containerfromindex
itemcontainergenerator.containerfromindex(int32)返回itemscontrol中指定索引处的item,当repeater的labelmemberpath
改变时,repeater首先强制更新了labelmembertemplate,然后用itemcontainergenerator.containerfromindex
找到所有的repeateritem并更新它们的label和labeltemplate。
preparecontainerforitemoverride
protected virtual void preparecontainerforitemoverride (dependencyobject element, object item) 用于在repeateritem添加到ui前为其做些准备工作,其实也就是为repeateritem设置label
和labeltemplate
而已。
4. 结语
实际上wpf的itemscontrol很强大也很复杂,源码很长,对初学者来说我推荐参考moonlight中的实现(moonlight, an open source implementation of silverlight for unix systems),上面labelmembertemplate的实现就是抄moonlight的。silverlight是wpf的简化版,moonlight则是很久没维护的silverlight的简陋版,这使得moonlight反而成了很优秀的wpf教学材料。
当然,也可以参考silverlight的实现,使用justdecompile可以轻松获取silverlight的源码,这也是很好的学习材料。不过itemscontrol的实现比moonlight多了将近一倍的代码。
5. 参考
itemscontrol class (system.windows.controls) microsoft docs
moon_itemscontrol.cs at master
itemcontainer control pattern - windows applications _ microsoft docs
上一篇: 去睡沙发吧!这里没有你的位置了!
下一篇: 常态化订单 比补贴更能驱动B2B的成长
推荐阅读
-
[WPF自定义控件库] 模仿UWP的ProgressRing
-
如何在双向绑定的Image控件上绘制自定义标记(wpf)
-
[WPF自定义控件库] 给WPF一个HyperlinkButton
-
[WPF自定义控件库]使用TextBlockHighlightSource强化高亮的功能,以及使用TypeConverter简化调用
-
[WPF自定义控件库] 关于ScrollViewr和滚动轮劫持(scroll-wheel-hijack)
-
[WPF自定义控件库]排序、筛选以及高亮
-
[WPF自定义控件库]好用的VisualTreeExtensions
-
[WPF自定义控件库]了解WPF的布局过程,并利用Measure为Expander添加动画
-
[WPF自定义控件] 开始一个自定义控件库项目
-
[WPF自定义控件库] 自定义控件的代码如何与ControlTemplate交互