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]了解模板化控件(10):原则与技巧

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

  2. [UWP]了解模板化控件(1):基础知识

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

  3. [UWP]了解模板化控件(2):模仿ContentControl

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

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

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

  5. [UWP]了解模板化控件(9):UI指南

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

  6. [UWP]了解模板化控件(2.1):理解ContentControl

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

  7. [UWP]了解模板化控件(3):实现HeaderedContentControl

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

  8. [UWP]了解模板化控件(5):VisualState

    1. 功能需求 使用TemplatePart实现上篇文章的两个需求(Header为空时隐藏HeaderContentPresenter,鼠标没有放在控件上时HeaderContentPresent半透 ...

  9. [UWP]了解模板化控件(5.2):UserControl vs. TemplatedControl

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

随机推荐

  1. 免费搭建wordpress博客有感

    之前一直有搭建个wordpress博客的想法,但一直没有实施.最近离职之后,空闲时间多了起来,就开始折腾wordpress博客起来. wordpress博客可玩性很高,但刚开始只想练练手,就没有想买域 ...

  2. Android使用Aspectj

    使用AspectJ 集成步骤: 1.AS配置Aspectj环境 2.配置使用ajc编译 4.定义注解 5.配置规则 6.使用 7.注意事项 AS配置Aspectj环境.Aspect目前最新版本为 1. ...

  3. java学习笔记 --- 集合

    1.定义:集合是一种容器,专门用来存储对象 数组和集合的区别?   A:长度区别  数组的长度固定 集合长度可变 B:内容不同  数组存储的是同一种类型的元素  而集合可以存储不同类型的元素  C:元 ...

  4. c#实现list,dataset,DataTable转换成josn等各种转换方法总和

    using System;using System.Collections.Generic;using System.Text;using System.Data;using System.Refle ...

  5. iOS开发之pch文件

    项目的Supporting files文件夹下面有个“工程名-Prefix.pch”文件,也是一个头文件 pch头文件的内容能被项目中的其他所有源文件共享和访问 一般在pch文件中定义一些全局的宏 在 ...

  6. 初识Object-C

    Object-C是苹果推出用来开发苹果软件的一门编程语言.大学学了3年的JAVA,到了大四毅然决然的放弃JAVA,是因为第一次接触Object-C就被它的简单语法吸引了.其实不仅仅是语法简单,相对于A ...

  7. 20155237 2016-2017-2 《Java程序设计》第5周学习总结

    20155237 2016-2017-2 <Java程序设计>第5周学习总结 教材学习内容总结 语法与继承架构 使用try...catch 与C语言中程序流程和错误处理混在一起不同,Jav ...

  8. 手机自动化测试:appium源码分析之bootstrap十三

    手机自动化测试:appium源码分析之bootstrap十三   poptest(www.poptest.cn)是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开 ...

  9. 五分钟秒懂Java日志组件

    Java中有许多种日志记录方式,有些API有占位符,有些API没占位符,初学的人可能会搞不清楚这些日志组件的由来.我一开始的时候也是很懵逼的,后来一点点弄懂了于是就又了这篇文章. 在Java中进行日志 ...

  10. DUBBO高级配置:多注册中心配置

    有时候我们需要的服务不在同一个 zookeeper 注册中心上,此时我们需要在 DUBBO 配置文件中配置多个注册中心. 下面我们在之前创建项目的基础上在 provider 模块中增加一个 IBook ...