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"/>
運行結果如下圖:
3. 實現
確定好需要實現的ItemsControl後,通常我大致會使用三步完成這個ItemsControl:
- 定義ItemContainer
- 關聯ItemContainer和ItemsControl
- 實現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
添加了LabelMemberPath
和LabelMemberTemplate
屬性,並把這個屬性和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設置Label
和LabelTemplate
而已。
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