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:

  1. 定义itemcontainer
  2. 关联itemcontainer和itemscontrol
  3. 实现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添加了labelmemberpathlabelmembertemplate属性,并把这个属性和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设置labellabeltemplate而已。

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