系列文章链接

数据模板

样式提供了基本的格式化能力,但是不管如何修改ListBoxItem,他都不能够展示功能更强大的元素组合,因为了每个ListBoxItem只支持单个绑定字段(通过DisplayMemberPath属性设置),不可能包含多个字段或者图像的富列表。

数据模板就是这样一个能够突破这个最大限制,允许组合使用来自绑定对象的多个属性,以特定的方式排列他们并显示简单字符串的高级样式。

数据模板是一个定义如何显示绑定的数据对象的XAML标记,有两类控件支持数据模板:

  • 内容控件:通过ContentTemplate属性支持数据模板。用于显示任何放置在Content属性中的内容(在基类ContentControl中定义了DataTemplate类型的对象ContentTemplate)
  • 列表控件(继承自ItemsControl类的控件):通过ItemTemplate属性支持数据模板。这个模板用于显示作为ItemsSource提供的集合中的每个项(或者DataTable的每一行)

基于列表的模板特性实际上时一内容控件模板为基础。列表中每一项均由内容控件封装(ListBox的ListBoxItem,ComboBox的ComboBoxItem都是内容控件),不管列表的ItemTemplate(DataTemplate类型)属性指定什么样的模板,模板都被用做列表的每项的ContentTemplate属性

<ListBox Margin="7,3,7,10" HorizontalContentAlignment="Stretch" SnapsToDevicePixels="True">
    <ListBox.ItemContainerStyle>
        <Style>
            <Setter Property="Control.Padding" Value="0" />
            <Style.Triggers>
                <Trigger Property="ListBoxItem.IsSelected" Value="True">
                    <Setter Property="ListBoxItem.Background" Value="DarkRed" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </ListBox.ItemContainerStyle>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Grid Margin="0" Background="White">
                <Border Margin="5" 
                        Background="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}, Path=Background}" 
                        BorderBrush="SteelBlue" BorderThickness="1" CornerRadius="4">
                    <Grid Margin="3">
                        <Grid.RowDefinitions>
                            <RowDefinition />
                            <RowDefinition />
                        </Grid.RowDefinitions>
                        <TextBlock Grid.Row="0" FontWeight="Bold" Text="{Binding Path=ModelNumber}" />
                        <TextBlock Grid.Row="1" Text="{Binding Path=ModelName}" />
                    </Grid>
                </Border>
            </Grid>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

分离和重用模板

与样式类似,通常也将模板声明为窗口或者应用程序的资源。在模板上增加键名,然后可以通过StaticResource引用来为列表添加数据模板。如果希望在不同类型的控件中自动重用相同的模板,可以通过设置DataTemplate.DataType属性来确定使用模板的绑定数据类型,并且删除资源键。

<DataTemplate DataType="{x:Type data:Product}">
    <Border Margin="3" BorderBrush="SteelBlue" BorderThickness="1" CornerRadius="4">
        <Grid Margin="3">
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition />
            </Grid.RowDefinitions>
            <TextBlock FontWeight="Bold" Text="{Binding Path=ModelNumber}" />
            <TextBlock Grid.Row="1" Text="{Binding Path=ModelName}" />
        </Grid>
    </Border>
</DataTemplate>

现在这个模板将用于窗口中任何绑定到Product对象的列表控件或者内容控件,二不需要指定ItemTemplate设置。

使用更高级的模板

模板可以包含非常丰富的内容,可以使用更复杂的控件、关联事件处理程序、将数据转换成不同的表达形式以及使用动画等。下面的例子使用的转换器和事件关联。

<ListBox.ItemTemplate>
          <DataTemplate>
            <Grid Margin="0" Background="White">
            <Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue"
                    Background="{Binding RelativeSource=
                          {
                             RelativeSource 
                             Mode=FindAncestor, 
                             AncestorType={x:Type ListBoxItem}
                          }, 
                          Path=Background
                         }" CornerRadius="4">
              <Grid Margin="3">
                <Grid.RowDefinitions>
                  <RowDefinition></RowDefinition>
                  <RowDefinition></RowDefinition>
                  <RowDefinition></RowDefinition>
                </Grid.RowDefinitions>
                <TextBlock FontWeight="Bold" Text="{Binding Path=ModelNumber}"></TextBlock>
                <TextBlock Grid.Row="1" Text="{Binding Path=ModelName}"></TextBlock>
                <Image Grid.Row="2" Grid.RowSpan="2" Source="{Binding Path=ProductImagePath, Converter={StaticResource ImagePathConverter}}"></Image>
              </Grid>
            </Border>
            </Grid>
          </DataTemplate>
        </ListBox.ItemTemplate>
      </ListBox>
<ListBox Grid.Row="1" Margin="10" Name="lstCategories" HorizontalContentAlignment="Stretch">
      <ListBox.ItemTemplate>
        <DataTemplate>
          <Grid Margin="3">
            <Grid.ColumnDefinitions>
              <ColumnDefinition></ColumnDefinition>
              <ColumnDefinition Width="Auto"></ColumnDefinition>
            </Grid.ColumnDefinitions>            
            <TextBlock VerticalAlignment="Center"  Text="{Binding Path=CategoryName}"></TextBlock>
            <Button Grid.Column="1" Padding="2"
                    Click="cmdView_Clicked" Tag="{Binding}">View ...</Button>            
            </Grid>
          </DataTemplate>
        </ListBox.ItemTemplate>
      </ListBox>

private void cmdView_Clicked(object sender, RoutedEventArgs e)
{
    Button cmd = (Button)sender;
    DataRowView row = (DataRowView)cmd.Tag;
    lstCategories.SelectedItem = row;
    
    // Alternate selection approach.
    //ListBoxItem item = (ListBoxItem)lstCategories.ItemContainerGenerator.ContainerFromItem(row);
    //item.IsSelected = true;
    MessageBox.Show("You chose category #" + row["CategoryID"].ToString() + ": " + (string)row["CategoryName"]);
}

改变模板

  • 使用数据触发器:可以绑定的数据对象中的属性值使用触发器修改模板的属性
  • 使用值转换器:实现IValueConverter接口的类,能够将值从绑定的对象转成设置模板中雨格式化相关的属性的值
  • 使用模板选择器:模板选择器检查绑定的数据对象,并从几个不同的模板之间进行选择

模板选择器

为不同的项选择不同的模板,需要创建继承自DataTemplateSelector的类。模板选择器的工作方式和前面分析的样式选择器的工作方式相同,他们检查绑定对象并使用提供的逻辑选择合适的模板。

这种方法的缺点是:可能必须创建多个类似的模板。如果模板比较复杂,这种方法会造成大量的重复内容。为了尽量提高可维护性,不应为单个列表创建多个模板,而应使用触发器和样式为模板应用不同的格式。

模板与选择

如果在列表中选择了一项,WPF会自动设置项容器(ListBoxItem对象)的Foreground和Background属性。Foreground属性使用属性继承,所以添加到模板中的任何元素都自动获得新的白色,除非明确指定新的颜色。Background属性不使用属性继承,默认是透明色。

在数据模板中,需要将数据模板的有些属性绑定到ListBoxItem对象上,所以使用Binding.RelativeSource属性从元素树上查找第一个匹配的ListBoxItem对象,一旦找到这个元素,就可以获得她的背景色,并相应的加以使用。

<ListBox.ItemTemplate>
  <DataTemplate>
    <Grid Margin="0" Background="White">
    <Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue"
            Background="{Binding RelativeSource=
                  {
                     RelativeSource 
                     Mode=FindAncestor, 
                     AncestorType={x:Type ListBoxItem}
                  }, 
                  Path=Background
                 }" CornerRadius="4">
      <Grid Margin="3">
        <Grid.RowDefinitions>
          <RowDefinition></RowDefinition>
          <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <TextBlock FontWeight="Bold" Text="{Binding Path=ModelNumber}"></TextBlock>
        <TextBlock Grid.Row="1" Text="{Binding Path=ModelName}"></TextBlock>
      </Grid>
    </Border>
    </Grid>
  </DataTemplate>
</ListBox.ItemTemplate>

改变项的布局

使用数据模板可以非常灵活的控制项显示的各个方面,但是他们不允许根据项之间的关系更改项的组织方式。不管使用什么样的模板和样式,ListBox控件总是在独立的水平行中放置每个项,并堆叠每行从而创建列表。

可以通过替换列表用于布局其子元素的容器来改变这种布局。为此,使用ItemsPanelTemplate属性。

<ListBox Grid.IsSharedSizeScope="True" Grid.Row="1" Margin="7,3,7,10" 
		 Name="lstProducts" ItemTemplate="{StaticResource ItemTemplate}"
         ScrollViewer.HorizontalScrollBarVisibility="Disabled" SnapsToDevicePixels="True">
  <ListBox.ItemsPanel>
    <ItemsPanelTemplate>
      <WrapPanel></WrapPanel>
    </ItemsPanelTemplate>
  </ListBox.ItemsPanel>
</ListBox>

注意:大多数列表控件使用VirtualizingStackPanel面板而不是使用标准的StackPanel面板。前者能够高效地处理大量的绑定数据,只创建显示当前可见项所需的元素。后者则创建所有列表元素。

ComboBox控件

和ListBox类一样,ComboBox类也是Selector类的派生类。ConboBox额外增加了两部分:显示当前选择项的选择框和用于选择项的下拉列表。可以通过设置IsDropDownOpen属性打开或者关闭下拉列表。

通常ComboBox只是一个只读的组合框,只可以选择一项,不能随意输入自己的内容。但是,可以通过属性IsReadOnly=false&&IsEditable=true来改变这个行为,选择框就会变成文本框,可以输入文本。

可以通过两种方式在ComboBox控件中放置更复杂的对象。一种是手动添加,可以简单的在StackPanel面板中放置适当的元素,并在ComboBoxItem对象中华封装这个StackPanel面板。还可以通过数据模板将数据对象的内容插入到预先定义好的元素组中。

<ComboBox Margin="5" SnapsToDevicePixels="True" Name="lstProducts" HorizontalContentAlignment="Stretch"
          IsEditable="{Binding ElementName=chkIsEditable, Path=IsChecked}"
          IsReadOnly="{Binding ElementName=chkIsReadOnly, Path=IsChecked}"
          TextSearch.TextPath="{Binding ElementName=txtTextSearchPath, Path=Text}"
        >
  <ComboBox.ItemContainerStyle>
    <Style>
      <Setter Property="Control.Padding" Value="0"></Setter>
      <Style.Triggers>
        <Trigger Property="ComboBoxItem.IsSelected" Value="True">
          <Setter Property="ComboBoxItem.Background" Value="DarkRed" />
        </Trigger>
        <Trigger Property="ComboBoxItem.IsHighlighted" Value="True">
          <Setter Property="ComboBoxItem.Background" Value="LightSalmon" />
        </Trigger>
      </Style.Triggers>
    </Style>
  </ComboBox.ItemContainerStyle>
  <ComboBox.ItemTemplate>
    <DataTemplate>
      <Grid Margin="0" Background="White">
        <Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue"
                Background="{Binding RelativeSource=
                      {
                         RelativeSource 
                         Mode=FindAncestor, 
                         AncestorType={x:Type ComboBoxItem}
                      }, 
                      Path=Background
                     }" CornerRadius="4">
          <Grid Margin="3">
            <Grid.RowDefinitions>
              <RowDefinition></RowDefinition>
              <RowDefinition></RowDefinition>                  
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
              <ColumnDefinition></ColumnDefinition>
              <ColumnDefinition Width="Auto"></ColumnDefinition>
            </Grid.ColumnDefinitions>
            <TextBlock FontWeight="Bold" Text="{Binding Path=ModelNumber}"></TextBlock>
            <TextBlock Grid.Row="1" Text="{Binding Path=ModelName}"></TextBlock>
            <Image Grid.Column="1" Grid.RowSpan="2" Width="50"  
			Source="{Binding Path=ProductImagePath, Converter={StaticResource ImagePathConverter}}"></Image>
          </Grid>
        </Border>
      </Grid>
    </DataTemplate>
  </ComboBox.ItemTemplate>
</ComboBox>