ItemsControl: 'I' is for Item Container
1. 杂乱无章的项成员
考虑以下示例:
<ItemsControl HorizontalAlignment="Left">
<TextBox Name="tb" Margin="2"Text="Test" />
<sys:String>http://drwpf.com/blog/</sys:String>
<sys:String>http://forums.microsoft.com/MSDN/</sys:String>
<x:Static Member="ApplicationCommands.Copy" />
<x:Static Member="ApplicationCommands.Cut" />
<x:Static Member="ApplicationCommands.Paste" />
<x:Static Member="ApplicationCommands.SelectAll"/>
</ItemsControl>
这个ItemsControl有7个项:一个TextBox,两个字符串,四个路由命令。你可为string定义一个模板用以显示一个超链接,为RoutedUICommand定义一个按钮。那么这个ItemsControl将会显示如下:
你可以用kaxaml来查看这段xaml。
2. 需要考虑的问题
一下是在使用ItemsControl时需要考虑的问题。
问题1:自定义如何放置孩子
Panel有能力排布任何类型的UIElement。所以,当然可以处理这些杂乱的孩子,但是考虑如果你的面板是Canvas,那么你需为所有的孩子设置设置关联属性,例如Canvas.Top。这会变得很麻烦。
问题2:在Items和Visual之间建立映射
记住实际的项实际上是字符串和命令对象。如果没有数据模板,这些对象就没有可视化展示。一旦模板被展开而且添加到ItemsControl,你怎么把你的visual映射回原始项呢?
问题3:UI 虚拟化
如果你的ItemsControl有数千项时会发生什么?除非这些项都非常小,并且不会同时出现的视口中。我们绝对不能为了实例化看不见的项而损害性能。我们如何确保某一时刻只有看得见的项才能在内存中呢?
问题4:一致的项风格
你可能想要为所有项提供统一的风格,因为这些项多种多样,而且面板也不一定是StackPanel,这让ItemsControl看上去很随意。一种方法是为所有项提供统一的背景色。能不用为每个DataTemplate添加样式而做到这些吗?
问题5:可见的选择状态
最后,如果ItemsControl是一个Selector,我们如何为所有的孩子定义个一致的选择状态呢?
如果面板的所有孩子项都是同一个类型,那么我们处理起来就容易多了。
3. 什么是项容器?
项容器是ItemsControl为每个项自动产生的包装器。之所以被称作容器,是因为他包含了项集合中的一项。这个容器真正包含了数据模板产生的visual。
让我们看看前面讲到的一个关于Character的ListBox的例子:
<ListBox ItemsSource="{Binding Source={StaticResource Characters}}" />
注意到我们使用了ListBox的ItemsSource模式,这个character集合跟以前一样:
<src:CharacterCollection x:Key="Characters">
<src:Character First="Bart" Last="Simpson" Age="10"
Gender="Male" Image="images/bart.png" />
<src:Character First="Homer" Last="Simpson" Age="38"
Gender="Male" Image="images/homer.png" />
<src:Character First="Lisa" Last="Bouvier" Age="8"
Gender="Female" Image="images/lisa.png" />
<src:Character First="Maggie" Last="Simpson" Age="0"
Gender="Female" Image="images/maggie.png" />
<src:Character First="Marge" Last="Bouvier" Age="38"
Gender="Female" Image="images/marge.png" />
</src:CharacterCollection>
我们可以定义一个简单的模板来显示character:
<DataTemplate DataType=" {x:Type src:Character} ">
<StackPanel Orientation="Vertical" Margin="5">
<TextBlock FontWeight="Bold" Text="{Binding First}"
TextAlignment="Center" />
<Image Margin="0,5,0,0" Source="{Binding Image}" />
</StackPanel>
</DataTemplate>
ListBox将显示如下:
4. container在哪里?
我们应该可以看到Container,但是在图中我们没有看到。这个问题的答案取决于ItemsControl,本例中是ListBox,这个container是一个叫做ListBoxItem的控件。你虽然没有看到这个控件,但是如果你选择这个项,你会注意到选择项的背景变为蓝色,前景色变为白色。这个蓝色就是container的背景色。
我们无需修改数据模板就自动获得了这些变化。他们是ListBoxItem的默认样式。看见如果你想修改选择项的样式,container就起到了很重要的作用。
5. 理解项容器和他的样式
1) 上面谈到,ListBoxItem的选择状态是在他的样式和模板中定义的。我强烈推荐你花时间去理解控件的项容器和他的默认样式。以ListBoxItem为例:
2) ListBoxItem的背景色是Transparent,这保证了当透明部分被点击的时候,ListBoxItem仍然能够获得鼠标点击事件。
3) ListBoxItem的HorizontalContentAlignment 和 VerticalContentAlignment 属性绑定到ListBox的同名属性。这样,如果你项要所有ListBoxItem左对齐他们的内容,有可以将ListBoxItem的HorizontalContentAlignment属性设置为Left。你不用去修改Style就可以做到这点,这非常方便。
4) ListBoxItem的默认模板只是包含一个Border,其中有一个ContentPresenter。
5) ListBoxItem公开IsSelected依赖属性。在Selector的项容器中,这是很普遍的。实际上IsSelected属性定义在Selector中。ListBoxItem和其他项容器只是简单把这个属性添加为所有者。Selector.IsSelected属性同样还提供了有用的Trigger功能。
6) 还有很多Trigger在container被选择,激活或者启用的时候用来改变容器的外观。
备注:你可以使用Expression blend来获得控件的默认外观。
6. ItemContainerStyle属性
如何定义项容器的样式呢?很简单,我们只需要设置ItemsControl的ItemContainerStyle就可以了。如下:
<ListBox ItemsSource="{Binding Source={StaticResource Characters}}"
ItemContainerStyle="{StaticResource CharacterContainerStyle}" />
然后我们需要定义这个样式,如下:
<Style x:Key="CharacterContainerStyle" TargetType="{x:Type ListBoxItem}">
<Setter Property="Background" Value="#FF3B0031" />
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="Width" Value="75" />
<Setter Property="Margin" Value="5,2" />
<Setter Property="Padding" Value="3" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Grid>
<Rectangle StrokeThickness="1" Stroke="Transparent"
RadiusX="5" RadiusY="5" Fill="White" />
<Grid>
<Rectangle x:Name="BackgroundRect" Opacity="0.5" StrokeThickness="1"
Stroke="Transparent" RadiusX="5" RadiusY="5"
Fill=" {TemplateBinding Background} " />
<Rectangle StrokeThickness="1" Stroke="Black" RadiusX="3" RadiusY="3" >
<Rectangle.Fill>
<LinearGradientBrush StartPoint="-0.51,0.41" EndPoint="1.43,0.41">
<LinearGradientBrush.GradientStops>
<GradientStop Color="Transparent" Offset="0"/>
<GradientStop Color="#60FFFFFF" Offset="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="0.6*"/>
<RowDefinition Height="0.4*"/>
</Grid.RowDefinitions>
<Rectangle RadiusX="3" RadiusY="3" Margin="3"
Grid.RowSpan="1" Grid.Row="0" >
<Rectangle.Fill>
<LinearGradientBrush EndPoint="0,0" StartPoint="0,1">
<GradientStop Color="#44FFFFFF" Offset="0"/>
<GradientStop Color="#66FFFFFF" Offset="1"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</Grid>
<ContentPresenter x:Name="ContentHost" Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
<Rectangle Fill="{x:Null}" Stroke="#FFFFFFFF"
RadiusX="3" RadiusY="3" Margin="1" />
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
应用此样式后,我们的ListBox显示如下:
注意到我们将Width设置为75像素,如果不设置,项容器将会跟内容具有相同的大小。这表示每个项容器将会足够大用来显示项内的图片。通过项容器的样式而不是在数据模板内设置项的大小,让我们的数据模板能够动态调整大小。
这个模板的问题是,当我选择一个项的时候,UI没有任何变化。前面所到,ListBoxItem可以给我们显示选择状态的功能,所以我们需要在ListBoxItem中添加一个触发器,如下:
<ControlTemplate.Triggers>
<Trigger Property="Selector.IsSelected" Value="True">
<Setter TargetName="BackgroundRect" Property="Opacity" Value="1" />
<Setter TargetName="ContentHost" Property="BitmapEffect">
<Setter.Value>
<OuterGlowBitmapEffect GlowColor="White" GlowSize="9" />
</Setter.Value>
</Setter>
<Setter TargetName="BackgroundRect" Property="Opacity" Value="1" />
</Trigger>
</ControlTemplate.Triggers>
现在我们选择Homer的时候,可以看见我们设置好的选择效果了!
7. 容器的上下文是项
我们知道,数据模板的根元素的DataContext是他的当前数据项。,因为上下文是通过元素树继承的,所以每个孩子元素都有相同的DataContext。这使得我们在模板内建立绑定非常容易,如下:
<TextBlock Text="{Binding First}" />
现在我们可以解释,这是如何工作的。当项容器被产生的时候,框架会把他的DataContext设置为当前数据项。然后数据模板被展开,并设置为项容器的内容。容器内的元素自动从项容器继承。
知道了这些,我们可以为我们的项容器样式添加一个DataTrigger来为女性设置一个粉色背景色。如下:
<Style.Triggers>
<DataTrigger Binding="{Binding Gender} " Value="Female">
<Setter Property="Background" Value="#FFF339CB" />
</DataTrigger>
</Style.Triggers>
在ItemsControl内自定义项的排布
我们可以通过为view model添加额外的数据来支持定位和展示数据。为了做到这些,我们为Character类型添加一个Location属性如下:
private Point _location = new Point();
public Point Location
{
get { return _location; }
set
{
_location = value;
RaisePropertyChanged ("Location");
}
}
然后我们修改Character集合来设置Location属性,如下:
<src:CharacterCollection x:Key="Characters">
<src:Character First="Bart" Last="Simpson" Age="10"
Gender="Male" Image="images/bart.png" Location="25,150" />
<src:Character First="Homer" Last="Simpson" Age="38"
Gender="Male" Image="images/homer.png" Location="75,0" />
<src:Character First="Lisa" Last="Bouvier" Age="8"
Gender="Female" Image="images/lisa.png" Location="125,150" />
<src:Character First="Maggie" Last="Simpson" Age="0"
Gender="Female" Image="images/maggie.png" Location="225,150" />
<src:Character First="Marge" Last="Bouvier" Age="38"
Gender="Female" Image="images/marge.png" Location="175,0" />
</src:CharacterCollection>
ListBox的默认面板是VirtualizingStackPanel,这工作的很好,但是如果我们需要一个自定义布局呢?我们知道可以为项选择任何面板。因为我们提供了Location属性,那么Canvas是一个合理的选择:
<ListBox ItemsSource="{Binding Source={StaticResource Characters}}"
ItemContainerStyle="{StaticResource CharacterContainerStyle}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
现在来看结果,发现所有的5项都被定位在(0,0)位置。这不是我们想要的,我们需要设置容器项的Canvas.Left和Canvas.Top属性为Location.X和Location.Y。添加 一下样式到ListBoxItem的Style中:
<Setter Property="Canvas.Left" Value="{Binding Location.X}" />
<Setter Property="Canvas.Top" Value="{Binding Location.Y} " />
选择我们看到想要的结果了:
8. 普遍问题(回顾)
问题1,4,5已经被解决了,下面章节中,我们会继续解决其他问题。
默认的项宿主和容器
为了方便起见,下面是WPF中的ItemsControl类型以及相应的项宿主和项容器:
ItemsControl Type |
Default Items Host |
Default Item Container |
ComboBox |
StackPanel |
ComboBoxItem |
ContextMenu |
StackPanel |
MenuItem |
HeaderedItemsControl |
StackPanel |
ContentPresenter |
ItemsControl |
StackPanel |
ContentPresenter or any UIElement* |
ListBox |
VirtualizingStackPanel |
ListBoxItem |
ListView |
VirtualizingStackPanel |
ListViewItem |
Menu |
WrapPanel |
MenuItem |
MenuItem |
StackPanel |
MenuItem |
StatusBar |
DockPanel |
StatusBarItem |
TabControl |
TabPanel |
TabItem |
ToolBar** |
not used |
none |
TreeView |
StackPanel |
TreeViewItem |
TreeViewItem |
StackPanel |
TreeViewItem |
*如果一个UIElement被显式添加到ItemsControl中,它会变为项面板的直接孩子。如果一个非UIElement被加入,它会被一个ContentPresenter包装。
**注意到我把ToolBar加入到列表中,因为,技术来讲,他是ItemsControl。然而,需要注意它有很多硬编码的行为使得它不同于其他ItemsControl。他不会包装项到容器中,并且硬编码了ToolBarPanel作为项面板,如果你通过ItemsPanel改变这个行为,将会引发一个异常(Framework,Shame on You!)。