G是产生器

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>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值