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。
