1. 模仿ItemsControl

顾名思义,ItemsControl是展示一组数据的控件,它是UWP UI系统中最重要的控件之一,和展示单一数据的ContentControl构成了UWP UI的绝大部分,ComboBox,ListBox,ListView,FlipView,GridView等控件都继承自ItemsControl。曾经有个说法:了解ContentControl和ItemsControl才能算是了解WPF的控件,这一点在UWP中也是一样的。

以我的经验来说,通过继承ItemsControl来自定义模板化控件十分常见,了解ItemsControl对将来要自定义模板化控件十分有用。但ItemsControl的话题十分庞大,和ContentControl不同,不太适合在这里展开讨论,所以这里就只是稍微讨论核心的思想。

虽然ItemsControl及其派生类很复杂,但核心功能很简单,所以索性自己实现一次。这次用于讨论的SimpleItemsControl直接继承自Control,简单地模仿ItemsControl实现了它基本的功能,通过这个控件可以一窥ItemsControl的原理。在XAML中使用如下,基本上和ItemsControl一样:

<StackPanel Margin="20" HorizontalAlignment="Center">
<local:SimpleItemsControl>
<ContentPresenter Content="this is ContentPresenter" />
<Rectangle Height="50"
HorizontalAlignment="Stretch"
Fill="Red" />
<local:ScoreModel />
</local:SimpleItemsControl> <local:SimpleItemsControl Margin="0,20,0,0">
<local:SimpleItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Score}" />
</DataTemplate>
</local:SimpleItemsControl.ItemTemplate>
<local:ScoreModel Score="70" />
<local:ScoreModel Score="80" />
<local:ScoreModel Score="90" />
<local:ScoreModel Score="100" />
</local:SimpleItemsControl>
</StackPanel>

SimpleItemsControl除了没有ItemsSource、ItemsPanelTemplate及虚拟化等功能等功能外,拥有ItemsControl基本的功能。

1.1 Items属性

public ICollection<object> Items
{
get;
}

实现这个控件首要的是提供Items属性,Items在构造函数中实例化成ObservableCollection类型,并且订阅它的CollectionChanged事件。注意:TemplatedControl中的集合属性通常都被可以被实例化成O巴塞尔,以便监视事件。

var items = new ObservableCollection<object>();
items.CollectionChanged += OnItemsCollectionChanged;
Items = items;

当然,为了可以在XAML的子节点直接添加元素,别忘了使用ContentPropertyAttribute。

[ContentProperty(Name = "Items")]

1.2 ItemsPanel

在ItemsControl中,ControlTemplate包含一个ItemsPresenter,它根据ItemsControl的ItemsPanelTemplate生成一个Panel,并且把Items中各个元素放入这个Panel。

SimpleItemsControl由于不是继承自ItemsControl,所以直接在ControlTemplate中放一个StackPanel代替。

_itemsPanel = GetTemplateChild(ItemsPanelPartName) as Panel;

<Style TargetType="local:SimpleItemsControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:SimpleItemsControl">
<StackPanel Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<StackPanel x:Name="ItemsPanel" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

ControlTemplate中只需要一个用于承载Items的ItemsPanel。在这个例子中使用StackPanel。

1.3 ItemTemplate属性

接下来需要提供public DataTemplate ItemTemplate { get; set; }属性,它定义了Items中每一项数据如何显示。事实上Items中每一项通常都默认使用ContentControl或ContentPresenter显示(譬如ListBoxItem和ComboxItem),所以ItemTemplate相当于它们的ContentTemplate。熟悉ContentControl的话会更容易理解这个属性。

1.4 GetContainerForItemOverride

//
// 摘要:
// 创建或标识用于显示给定项的元素。
//
// 返回结果:
// 用于显示给定项的元素。
protected virtual DependencyObject GetContainerForItemOverride()
{
return new ContentPresenter();
}

ItemsControl使用GetContainerForItemOverride函数为Items中每一个item创建它的容器用于在UI上显示,默认是ContentPresenter。对于不是派生自UIElement的Item,它们无法直接在UI上显示,所以Container是必须的。

1.5 IsItemItsOwnContainerOverride

//
// 摘要:
// 确定指定项是否是为自身的容器,或是否可以作为其自身的容器。
//
// 参数:
// item:
// 要检查的项。
//
// 返回结果:
// 如果项是其自己的容器(或可以作为自己的容器),则为 true;否则为 false。
protected virtual System.Boolean IsItemItsOwnContainerOverride(System.Object item)
{
return item is ContentPresenter;
}

对于Items中的每一个item,ItemsControl在为它创建容器前都用这个方法检查它是不是就是容器本身。譬如这段XAML:

<local:SimpleItemsControl>
<ContentPresenter Content="this is ContentPresenter" />
<Rectangle Height="50"
Width="200"
Fill="Red" />
<local:ScoreModel />
</local:SimpleItemsControl>

在这段XAML中,ContentPresenter本身就是容器,所以它将直接被放到ItemsPanel中;Rectangle 不是容器,需要创建一个ContentPresenter,将Rectangle 设置为这个ContentPresenter的Content再放到ItemsPanel中。

1.6 PrepareContainerForItemOverride

//
// 摘要:
// 准备指定元素以显示指定项。
//
// 参数:
// element:
// 用于显示指定项的元素。
//
// item:
// 要显示的项。
protected virtual void PrepareContainerForItemOverride(DependencyObject element, System.Object item)
{
ContentControl contentControl;
ContentPresenter contentPresenter; if ((contentControl = element as ContentControl) != null)
{
contentControl.Content = item;
contentControl.ContentTemplate = ItemTemplate;
}
else if ((contentPresenter = element as ContentPresenter) != null)
{
contentPresenter.Content = item;
contentPresenter.ContentTemplate = ItemTemplate;
}
}

这个方法在Item被呈现到UI前调用,目标是设定ContainerForItem中的某些值,譬如Content及ContentTemplate。其中参数element即之前创建的ContainerForItem(也有可能是Item自己)。在调用这个函数后ContainerForItem将被放到ItemsPanel中。

1.7 UpdateView

private void UpdateView()
{
if (_itemsPanel == null)
return; _itemsPanel.Children.Clear();
foreach (var item in Items)
{
DependencyObject container;
if (IsItemItsOwnContainerOverride(item))
{
container = item as DependencyObject;
}
else
{
container = GetContainerForItemOverride();
PrepareContainerForItemOverride(container, item);
} if (container is UIElement)
_itemsPanel.Children.Add(container as UIElement);
}
}

这个函数在OnItemsCollectionChanged或OnApplyTemplate后调用,简单地将ItemsPanel.Children清空,然后将所有Item创建容器(或者不创建)然后放进ItemsPanel。实际上ItemsControl的逻辑要复杂很多,这里只是个极端简化的版本。

到这一步一个简单的ItemsControl就完成了,总共只有100多行代码。

看到这里可能会有个疑惑,GetContainerForItemOverride、IsItemItsOwnContainerOverride、PrepareContainerForItemOverride三个函数明明做的是同一件事(为Item创建Container),为什么要将它们分开?这是因为ItemsControl支持使用UI虚拟化技术。

假设Items中包含一万个项,为这一万个项创建容器并放到ItemsPanel上,将会造成巨大的内存消耗。而且拖动ItemsControl的滚动条时由于要将所有一万个容器同时移动,对CPU造成很大的负担。UI虚拟化就是为了解决这两个问题。通常一个ItemsControl能同时显示的Item最多几十个,ItemsControl就只是创建几十个容器,在拖动滚动条时回收移出可视范围的容器,更改容器的内容(因为容器通常是ContentControl,所以就是更改ContentControl.Content),再重新放到可视范围里面。为了实现这个技术,Item和它的Container就不能是一一对应的,所以才会把上述的三个函数分离。

注意: UWP中ItemsControl默认没有启用UI虚拟化,但它的派生类有。

1.8 完整的代码

[TemplatePart(Name = ItemsPanelPartName, Type = typeof(Panel))]
[ContentProperty(Name = "Items")]
public class SimpleItemsControl : Control
{
private const string ItemsPanelPartName = "ItemsPanel";
public SimpleItemsControl()
{
this.DefaultStyleKey = typeof(SimpleItemsControl);
var items = new ObservableCollection<object>();
items.CollectionChanged += OnItemsCollectionChanged;
Items = items;
} /// <summary>
/// 获取或设置ItemTemplate的值
/// </summary>
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
} /// <summary>
/// 标识 ItemTemplate 依赖属性。
/// </summary>
public static readonly DependencyProperty ItemTemplateProperty =
DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(SimpleItemsControl), new PropertyMetadata(null, OnItemTemplateChanged)); private static void OnItemTemplateChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
SimpleItemsControl target = obj as SimpleItemsControl;
DataTemplate oldValue = (DataTemplate)args.OldValue;
DataTemplate newValue = (DataTemplate)args.NewValue;
if (oldValue != newValue)
target.OnItemTemplateChanged(oldValue, newValue);
} protected virtual void OnItemTemplateChanged(DataTemplate oldValue, DataTemplate newValue)
{
UpdateView();
} public ICollection<object> Items
{
get;
} private Panel _itemsPanel; protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_itemsPanel = GetTemplateChild(ItemsPanelPartName) as Panel;
UpdateView();
} private void OnItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
UpdateView();
} //
// 摘要:
// 创建或标识用于显示给定项的元素。
//
// 返回结果:
// 用于显示给定项的元素。
protected virtual DependencyObject GetContainerForItemOverride()
{
return new ContentPresenter();
} //
// 摘要:
// 确定指定项是否是为自身的容器,或是否可以作为其自身的容器。
//
// 参数:
// item:
// 要检查的项。
//
// 返回结果:
// 如果项是其自己的容器(或可以作为自己的容器),则为 true;否则为 false。
protected virtual System.Boolean IsItemItsOwnContainerOverride(System.Object item)
{
return item is ContentPresenter;
} //
// 摘要:
// 准备指定元素以显示指定项。
//
// 参数:
// element:
// 用于显示指定项的元素。
//
// item:
// 要显示的项。
protected virtual void PrepareContainerForItemOverride(DependencyObject element, System.Object item)
{
ContentControl contentControl;
ContentPresenter contentPresenter; if ((contentControl = element as ContentControl) != null)
{
contentControl.Content = item;
contentControl.ContentTemplate = ItemTemplate;
}
else if ((contentPresenter = element as ContentPresenter) != null)
{
contentPresenter.Content = item;
contentPresenter.ContentTemplate = ItemTemplate;
}
} private void UpdateView()
{
if (_itemsPanel == null)
return; _itemsPanel.Children.Clear();
foreach (var item in Items)
{
DependencyObject container;
if (IsItemItsOwnContainerOverride(item))
{
container = item as DependencyObject;
}
else
{
container = GetContainerForItemOverride();
PrepareContainerForItemOverride(container, item);
} if (container is UIElement)
_itemsPanel.Children.Add(container as UIElement);
}
}
}

2. 扩展ItemsControl

了解过ItemsControl的原理,或通过继承ItemsControl自定义控件就很简单了。譬如要实现这个功能:一个事件列表,自动为事件添加上触发的时间。效果如下:

通过重载GetContainerForItemOverride、IsItemItsOwnContainerOverride、PrepareContainerForItemOverride这三个函数,很简单就能实现这个需求:

public class EventListView : ListView
{
public EventListView()
{
_items = new Dictionary<object, DateTime>();
} private Dictionary<object, DateTime> _items; protected override DependencyObject GetContainerForItemOverride()
{
return new HeaderedContentControl();
} protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is HeaderedContentControl;
} protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
var control = element as HeaderedContentControl;
control.Content = item;
if (_items.ContainsKey(item))
{
var time = _items[item];
control.Header = time.ToString("HH:mm:ss")+": ";
}
} protected override void OnItemsChanged(object e)
{
base.OnItemsChanged(e);
foreach (var item in Items)
{
if (_items.ContainsKey(item) == false)
_items.Add(item, DateTime.Now);
}
}
}
public sealed class EventListViewItem : ListViewItem
{
public EventListViewItem()
{
this.DefaultStyleKey = typeof(EventListViewItem);
} public object Header
{
get { return (object)GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
} // Using a DependencyProperty as the backing store for Header. This enables animation, styling, binding, etc...
public static readonly DependencyProperty HeaderProperty =
DependencyProperty.Register("Header", typeof(object), typeof(EventListViewItem), new PropertyMetadata(null)); }

3. 集合类型属性

在XAML中使用集合类型属性,通常不会这样:

<ItemsControl>
<ItemsControl.Items>
<ItemCollection>
<local:ScoreModel Score="70" />
<local:ScoreModel Score="80" />
<local:ScoreModel Score="90" />
<local:ScoreModel Score="100" />
</ItemCollection>
</ItemsControl.Items>
</ItemsControl>

而是这样:

<ItemsControl>
<ItemsControl.Items>
<local:ScoreModel Score="70" />
<local:ScoreModel Score="80" />
<local:ScoreModel Score="90" />
<local:ScoreModel Score="100" />
</ItemsControl.Items>
</ItemsControl>

因为集合类型属性通常定义为只读的,不必也不可以对它赋值,只可以向它添加内容。

控件中的集合属性一般遵循以下做法:

3.1 只读属性

public IList<HubSection> Sections { get; }

这是Hub的Section属性,模板化控件中的集合类型属性基本都定义成这样的CLR属性。

3.2 监视更改通知

如果需要监视集合项更改,可以将属性定义为继承INotifyCollectionChanged 自的集合类型,譬如 ObservableCollection。

3.3 不使用依赖属性

因为集合属性通常不会使用动画,或者通过Style中的Setter赋值,而且依赖属性标识符是静态的,集合属性的初始值有可能引起单例的问题。集合属性通常在构造函数中初始化。

3.4 绑定到集合属性

通常不会绑定到集合属性,更常见的做法是如ItemsControl那样,绑定到ItemsSource。

[UWP 自定义控件]了解模板化控件(8):ItemsControl的更多相关文章

  1. UWP 自定义控件:了解模板化控件 系列文章

    UWP自定义控件的入门文章 [UWP 自定义控件]了解模板化控件(1):基础知识 [UWP 自定义控件]了解模板化控件(2):模仿ContentControl [UWP 自定义控件]了解模板化控件(2 ...

  2. [UWP 自定义控件]了解模板化控件(10):原则与技巧

    1. 原则 推荐以符合以下原则的方式编写模板化控件: 选择合适的父类:选择合适的父类可以节省大量的工作,从UWP自带的控件中选择父类是最安全的做法,通常的选择是Control.ContentContr ...

  3. [UWP 自定义控件]了解模板化控件(1):基础知识

    1.概述 UWP允许开发者通过两种方式创建自定义的控件:UserControl和TemplatedControl(模板化控件).这个主题主要讲述如何创建和理解模板化控件,目标是能理解模板化控件常见的知 ...

  4. [UWP 自定义控件]了解模板化控件(2):模仿ContentControl

    ContentControl是最简单的TemplatedControl,而且它在UWP出场频率很高.ContentControl和Panel是VisualTree的基础,可以说几乎所有VisualTr ...

  5. [UWP 自定义控件]了解模板化控件(3):实现HeaderedContentControl

    1. 概述 来看看这段XMAL: <StackPanel Width="300"> <TextBox Header="TextBox" /&g ...

  6. [UWP 自定义控件]了解模板化控件(4):TemplatePart

    1. TemplatePart TemplatePart(部件)是指ControlTemplate中的命名元素.控件逻辑预期这些部分存在于ControlTemplate中,并且使用protected ...

  7. [UWP 自定义控件]了解模板化控件(5.2):UserControl vs. TemplatedControl

    1. UserControl vs. TemplatedControl 在UWP中自定义控件常常会遇到这个问题:使用UserControl还是TemplatedControl来自定义控件. 1.1 使 ...

  8. [UWP 自定义控件]了解模板化控件(9):UI指南

    1. 使用TemplateSettings统一外观 TemplateSettings提供一组只读属性,用于在新建ControlTemplate时使用这些约定的属性. 譬如,修改HeaderedCont ...

  9. [UWP 自定义控件]了解模板化控件(2.1):理解ContentControl

    UWP的UI主要由布局容器和内容控件(ContentControl)组成.布局容器是指Grid.StackPanel等继承自Panel,可以拥有多个子元素的类.与此相对,ContentControl则 ...

随机推荐

  1. 通用triggerEvent方法

    假设有一个id为testA的a元素,然后有以下代码(jquery已存在): $(document).ready(function(){ $('#testA').on('testEvent', func ...

  2. 如何用vmware workstation来做虚拟化实验

    前言 以前做用vmare只是简单的实验,但是随着现在虚拟化的兴起,我们的开始要开始虚拟化的实验了. 我们看到有些windows 2012的书上面说用hyper-v来实验,但是hyper-v只能做一些列 ...

  3. AD域自定义属性《完整》

    1.安装Active Directory 架构,下载:adminpak.msi安装. 2.以管理员运行cmd,执行:regsvr32 schmmgmt.dll(该命令将在计算机上注册“schmmgmt ...

  4. 第五章 绘图基础(DEVCAPS1)

    获取设备环境的信息 //DEVCAPS1.C--Device Capabilities Display Program No.1 (c) Charles Petzold, 1998 #include ...

  5. Python3编写网络爬虫11-数据存储方式四-关系型数据库存储

    关系型数据库存储 关系型数据库是基于关系模型的数据库,而关系模型是通过二维表保存的,所以它的存储方式就是行列组成的表.每一列是一个字段,每一行是一条记录.表可以看作某个实体的集合,而实体之间存在联系, ...

  6. 【转】struct和typedef struct在C/C++中的区别

    分三块来讲述: 1 首先://注意在C和C++里不同 在C中定义一个结构体类型要用typedef: typedef struct Student { int a; }Stu; 于是在声明变量的时候就可 ...

  7. MySQL InnoDB Update和Crash Recovery流程

    MySQL InnoDB Update和Crash Recovery流程 概要信息 首先介绍了Redo,Undo,Log Sequence Number (LSN),Checkpoint,Rollba ...

  8. cglib之Enhancer

    1. 背景 cglib库的Enhancer在Spring AOP中作为一种生成代理的方式被广泛使用.本文针对Enhancer的用法以实际代码为例作一些介绍. 2. Enhancer是啥 Enhance ...

  9. 解决MySQL Workbench导出乱码问题

    1.导出数据 2.默认CSV格式 3.乱码 4.解决 文件->另存为,会发现编码为UTF-8,正是MySQL表的编码方式.我们选择编码方式为ANSI,保存类型为所有,覆盖源文件

  10. sed命令替换字符包含斜杠\,引号的处理方法

    在字符替换中,可能会遇见引号,“/”等的替换,这时应该注意,sed的命令原型是: sed -i  "s/oldstring/goalstring/g" file 如果一个路径是da ...