欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

[WPF自定义控件库]了解如何自定义ItemsControl

程序员文章站 2022-06-28 20:11:51
1. 前言 对WPF来说ContentControl和 "ItemsControl" 是最重要的两个控件。 顾名思义,ItemsControl表示可用于呈现一组Item的控件。大部分时候我们并不需要自定义ItemsControl,因为WPF提供了一大堆ItemsControl的派生类:Headere ......

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"/>

运行结果如下图:

[WPF自定义控件库]了解如何自定义ItemsControl

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>

[WPF自定义控件库]了解如何自定义ItemsControl

例如这段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的样式。

[WPF自定义控件库]了解如何自定义ItemsControl

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多了将近一倍的代码。

[WPF自定义控件库]了解如何自定义ItemsControl

5. 参考

itemscontrol class (system.windows.controls) microsoft docs
moon_itemscontrol.cs at master
itemcontainer control pattern - windows applications _ microsoft docs