ItemsControl: 'G' is for Generator
本节中我们要学习项容器是如何出现的。
1. 这些项容器来自哪里?
在上一节的例子里,项容器被神奇的创建 ,而我们只是绑定了控件的ItemsSource属性。考虑以下ListBox示例:
<ListBox ItemsSource="{Binding Source={StaticResource Characters}}"
ItemContainerStyle="{StaticResource CharacterContainerStyle}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
我们知道ListBox的每个项都包含在ListBoxItem中,但是我们不知道谁创建了它。假定ItemsControl创建了它是合理的,但事实不是这样的。
2. 关于ItemContainerGenerator
事实是每个ItemsControl都有自己的ItemContainerGenerator实例。该对象可从ItemsControl的ItemContainerGenerator属性访问。
从类名可知,ItemContainerGenerator为ItemsControl提供了产生项容器的方法。特别的,ItemContainerGenerator知道如何创建项容器,并将其连接到它要包含的项。它还知道什么时候能够移除一个项容器。最后,ItemContainerGenerator提供了一些重要的方法,用来将现有项映射到项容器,反之亦然。
3. 谁在控制ItemContainerGenerator?
你可能认为ItemContainerGenerator控制着自己,知道何时开始工作。但是实际上,它跟这些事情无关。
ItemContainerGenerator确实在监听项集合的视图(通过使用弱事件来监听INotifyCollectionChanged )。它使用变化通知来决定何时解除与被移除项的连接,但是它从来不会自己为一个添加项产生一个项容器。它只是静静的维护这集合中的项并在需要的时候创建一个项容器。
如果不是ItemContainerGenerator,那么一定是ItemsControl,是吗?仍然不是,ItemsControl也不会产生项容器。但是他确实在这个过程中起着关键作用。它创建并拥有ItemContainerGenerator。并且决定这和中类型的项容器将被创建。
好吧,到底谁负责决定何时产生项容器呢?如果你已经看过以前的文章,那么很明显既不是ItemsControl也不是ItemContainerGenerator来决定何时产生项容器。因为ItemsControl支持一种叫做UI虚拟化的功能。
在P是Panel中,我们知道ItemsControl的项宿主是动态的,所以ItemsControl和ItemContainerGenerator都不会知道项何时是可见的。很明确,项面板必须直接负责项容器的产生。毕竟项容器是项面板的直接可视孩子。因为面板能够准确的知道怎么排放孩子,所以由面板来控制ItemContainerGenerator来产生容器是很合理的。
4. ItemContainerGenerator如何知道产生什么样的container呢?
事实上,它根本不知道产生什么样的项容器。每个ItemsControl需要自己指定项容器。ItemContainerGenerator只是通过调用ItemsControl的GetContainerForItem延迟项容器的创建。这个方法负责创建和返回新的容器项。
如果项的类型和项容器的类型相同会发生什么呢?
我们前面已经看过下面的例子:
<ListBox SelectedIndex="0"Width="100">
<ListBoxItem>Item1</ListBoxItem>
<ListBoxItem>Item2</ListBoxItem>
<ListBoxItem>Item3</ListBoxItem>
<ListBoxItem>Item4</ListBoxItem>
<ListBoxItem>Item5</ListBoxItem>
</ListBox>
你可能会项,这种情况下ItemContainerGenerator会如何处理呢?它会创建一个新的ListBoxItem包含指定的ListBoxItem吗?答案是否定的。在ItemContainerGenerator创建新的项容器之前,它会首先检查项本身是不是已经是一个项容器。这是通过调用ItemsControl.IsItemItsOwnContainer方法做到的。
5. 我们如何为自定义的ItemsControl指定自己的项容器呢?
假如你希望创建一个ForceDirectedItemsControl类,并且希望项容器是一个ForceDirectedItem对象。我们如何确保我们的ForceDirectedItemsControl类为我们创建了ForceDirectedItem对象呢?
事实是,非常简单。我们只需要重写ItemsControl的两个方法:
IsItemItsOwnContainerOverride() 和 GetContainerForItemOverride().
如下所示:
public class ForceDirectedItemsControl : ItemsControl
{
protected override bool IsItemItsOwnContainerOverride(object item)
{
return (item is ForceDirectedItem);
}
protected override DependencyObject GetContainerForItemOverride()
{
return new ForceDirectedItem();
}
}
通过上述代码,ItemContainerGenerator需要产生项容器的时候,将会首先检查当前项是不是一个ForceDirectedItem。如果不是,将会通过调用ItemsControl. GetContainerForItemOverride来获取项容器。
警告:永远不要认为项和项容器之间的联系是不变的。GetContainerForItemOverride方法只是指明了面板中所支持的项。而项和项容器之间的联系是由ItemContainerGenerator来决定的。在虚拟面板的场景中,连接一个项到一个项容器,之后将项连接到另一个容器的做法是普遍的。这提供了很好的性能优化。
6. ItemsControl中UI虚拟化是如何工作的呢?
在框架中只有两个值得注意的面板Panel 和 VirtualizingStackPanel,这两个面板控制着ItemContainerGenerator指导其产生项容器。
抽象的Panel基类包含着产生默认容器的代码。这个默认逻辑就是为集合中的每个项都产生一个项容器,所以,默认情况下,UI没有虚拟化。
框架包含另一个抽象类VirtualizingPanel。该类从Panel继承并且重写了默认项容器产生的逻辑。所以如果你从VirtualizingPanel派生,那么它不会为你产生任何项容器。所以你需要负责实现利用ItemContainerGenerator来产生项容器的逻辑。
在早期的框架中,只有一个虚拟化面板叫做VirtualizingStackPanel。而这个面板是ListBox和ListView的默认面板。所以如果我们使用这些控件,我们就已经获得虚拟化的能力。
VirtualizingStackPanel跟其他面板一样,负责定位和调整孩子的尺寸。所以该面板准确的知道哪个在ItemsControl视口中的孩子是可见的。所以它利用这种能力来只创建可见的孩子(加上一些额外的在视口之外的孩子用来支持键盘操作)。VirtualizingStackPanel还会将移除的项容器虚拟化,所以这些资源可以被回收。
如果你在自己写一个虚拟面板,你同样需要实现现实化可见项容器和虚拟化非可见项容器的逻辑。现实化包括产生项容器(generator.GenerateNext()) 和准备它以便寄宿到面板中 (generator.PrepareItemContainer())。 虚拟化包含移除项容器 (generator.Remove())。
7. 把所有绑定到一起
我们已经谈到了ItemsControl的许多方面,他们都在项容器的产生过程中起着作用。把ItemsControl,项面板,项集合和ItemContainerGenerator结合起来是一件很复杂的事情。结合的结果就是创建项容器,用来包含项或者作为可视元素的寄宿。
8. 通过把项映射到容器来查找模板里的元素
WPF论坛里最常问的一个问题是:我如何通过指定的项来获取对应的模板元素?
备注:在大多数情况下,你不需要这么做。这么做的理由是通过编程来改变模板里某些元素的属性。如果你只关心视图模型,可以将模板属性绑定到数据项属性来解决这个问题。
如果我们真的需要遍历visual tree去查找某个元素,我们可以使用ItemContainerGenerator的方法来做到这些:ContainerFromIndex() 和 ContainerFromItem()。一旦你有了项容器,你可以使用递归路径来查找元素,如下:
public static Visual GetDescendantByName(Visual element, string name)
{
if (element == null) return null;
if (element is FrameworkElement
&& (element as FrameworkElement).Name == name) return element;
Visual result = null;
if (element is FrameworkElement)
(element as FrameworkElement).ApplyTemplate();
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
{
Visual visual = VisualTreeHelper.GetChild(element, i) as Visual;
result = GetDescendantByName(visual, name);
if (result != null)
break;
}
return result;
}
public static Visual GetDescendantByType(Visual element, Type type)
{
if (element.GetType() == type) return element;
Visual foundElement = null;
if (element is FrameworkElement)
(element as FrameworkElement).ApplyTemplate();
for (int i = 0;
i < VisualTreeHelper.GetChildrenCount(element); i++)
{
Visual visual = VisualTreeHelper.GetChild(element, i) as Visual;
foundElement = GetDescendantByType(visual, type);
if (foundElement != null)
break;
}
return foundElement;
}
9. 查找与模板元素相关的容器
如果你想找到某个模板元素所在的项容器,可以使用如下方法:
public static DependencyObject GetAncestorByType(
DependencyObject element, Type type)
{
if (element == null) return null;
if (element.GetType() == type) return element;
return GetAncestorByType(VisualTreeHelper.GetParent(element), type);
}
所有这些把戏可以在简单的ItemsControl上工作的良好,但是对于HeaderedItemsControl类型就变得复杂了,例如TreeView或者MenuItem。这些情况下,我强烈推荐使用视图模型,命令,绑定来处理属性更新。
10.处理异步的容器产生器
有些情况下,我们需要考虑容器产生器的时机问题,假定你有一个ListBox为CharacterListBox,它绑定了一个Character集合,你可能会写下如下代码:
private void AddScooby()
{
Character scooby = new Character("Scooby Doo");
Characters.Add(scooby);
ListBoxItem lbi = CharacterListBox.ItemContainerGenerator
.ContainerFromItem(scooby) as ListBoxItem;
lbi.IsSelected = true;
}
这段代码将会引发一个异常。因为lbi将会是空值。这是因为容器产生是在一个单独的分派器操作中。结果是更改了ItemsSource并不会立即引发容器项的创建。
关键是我们需要时刻记住这是一个异步操作。那么我们如何定位知道容器何时产生呢?为了这个目的,ItemContainerGenerator提供了一个Status属性以及一个属性变化通知的事件。如果需要,我们可以订阅StatusChanged事件,如下:
private void AddScooby()
{
_scooby = new Character("Scooby Doo");
Characters.Add(_scooby);
CharacterListBox.ItemContainerGenerator.StatusChanged
+= OnStatusChanged;
}
private void OnStatusChanged(object sender, EventArgs e)
{
if (CharacterListBox.ItemContainerGenerator.Status
== GeneratorStatus.ContainersGenerated)
{
CharacterListBox.ItemContainerGenerator.StatusChanged
-= OnStatusChanged;
ListBoxItem lbi = CharacterListBox.ItemContainerGenerator
.ContainerFromItem(_scooby) as ListBoxItem;
if (lbi != null)
{
lbi.IsSelected = true;
}
}
}
因为GeneratorStatus有多种状态,所以我们需要检查它是不是我们想要的状态,全部的状态有NotStarted, GeneratingContainers,ContainersGenerated, or Error。我们永远都应该检查这个状态。其次,一旦项容器产生了,处理器方法将被从事件中移除。之后将不会再被调用。
11.奖励阅读:通过FindAncestor绑定在模板中查找项容器
一个简单的示例如下:
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type ListBoxItem}}, Path=IsSelected}" Value="True">
<Setter Property="Foreground" Value="#A1927E" TargetName="tb" />
</DataTrigger>