20 WPF格式化绑定数据

数据绑定回顾

见601页。

数据转换

使用StringFormat属性

StringFormat属性适用于显示数字为文本。

使用Binding对象的StringFormat属性,可以将绑定对象的属性转化为指定格式的文本:

<TextBox Margin="5" Grid.Row="2" Grid.Column="1"
 Text="{Binding Path=UnitCost, StringFormat={}{0:C}}">
</TextBox>

StrngFormat值开始的成对花括号,是逃跑序列,表式StringFormat值不是标记扩展。

顺便,只有StringFormat值以花括号开头时,才需要{}逃跑序列。

WPF列表控件也支持字符串格式化列表项目。为使用它,你简单地设置列表的ItemStringFormat属性(定义在ItemsControl类)。这是一个例子,列出产品价格:

<ListBox Name="lstProducts" DisplayMemberPath="UnitCost" ItemStringFormat="{}{0:C}">
</ListBox>

介绍值转换器

值转换器负责源数据和目标的转换。恰好在源数据被显示之前转换,以及在两路绑定的情况下,恰好在目标值被应用回源数据之前转换。

创造值转换器,需要4步:

  1. 创造实现IValueConverter接口的类
  2. 添加特性ValueConversion属性到类声明,指定目标数据类型。
  3. 实现Convert()方法,此方法改变数据,从原始格式到显示格式。
  4. 实现ConvertBack()方法,此方法反向改变值,从显示格式到本地格式。

这是处理价格的完整的值转换器类:

[ValueConversion(typeof(decimal), typeof(string))]
public class PriceConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var price = (decimal) value;
        return price.ToString("C", culture);
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var price = value.ToString();
        
        decimal result;
        if (Decimal.TryParse(price, NumberStyles.Any, culture, out result))
        {
            return result;
        }
        return value;
    }
}

应用转换器,创建一个PriceConverter实例,将它赋值给绑定对象的转换器属性:

<TextBlock Margin="7" Grid.Row="2">Unit Cost:</TextBlock>
<TextBox Margin="5" Grid.Row="2" Grid.Column="1">
  <TextBox.Text>
    <Binding Path="UnitCost">
      <Binding.Converter>
        <local:PriceConverter></local:PriceConverter>
      </Binding.Converter>
    </Binding>
  </TextBox.Text>
</TextBox>

在许多情况下,相同的转换器被用于多个绑定。在这种情况下,为每个绑定创造转换器的一个实例毫无意义。而是,在Resources集合创造转换器对象,如下所示:

<Window.Resources>
  <local:PriceConverter x:Key="PriceConverter"></local:PriceConverter>
</Window.Resources>

然后引用资源:

<TextBox Margin="5" Grid.Row="2" Grid.Column="1"
 Text="{Binding Path=UnitCost, Converter={StaticResource PriceConverter}}">
</TextBox>

用值转换器创建对象

值转换器是连接数据类和窗口显示的桥梁。

这是实现文件路径到BitmapImage对象转换的类:

public class ImagePathConverter : IValueConverter
{
    private string imageDirectory = Directory.GetCurrentDirectory();

    public string ImageDirectory
    {
        get { return imageDirectory; }
        set { imageDirectory = value; }
    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var imagePath = Path.Combine(ImageDirectory, (string)value);
        return new BitmapImage(new Uri(imagePath));
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException("The method or operation is not implemented.");
    }
}

为了使用这个转换器,添加它到资源:

<Window.Resources>
  <local:ImagePathConverter x:Key="ImagePathConverter"></local:ImagePathConverter>
</Window.Resources>

创造一个使用此转换器的绑定表达式:

<Image Margin="5" Grid.Row="2" Grid.Column="1" Stretch="None"
 HorizontalAlignment="Left" Source=
 "{Binding Path=ProductImagePath, Converter={StaticResource ImagePathConverter}}">
</Image>

注意,Image.Source属性期待一个ImageSource对象,而BitmapImage类派生自ImageSource类。

应用条件格式

例如,你希望标记高价的项目,给他们一个不同的背景颜色:

public class PriceToBackgroundConverter : IValueConverter
{
    public decimal MinimumPriceToHighlight {get; set;}

    public Brush HighlightBrush{get; set;}

    public Brush DefaultBrush{get; set;}

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var price = (decimal)value;
        if (price >= MinimumPriceToHighlight)
            return HighlightBrush;
        else
            return DefaultBrush;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

定义资源对象:

<local:PriceToBackgroundConverter x:Key="PriceToBackgroundConverter"
  DefaultBrush="{x:Null}" HighlightBrush="Orange" MinimumPriceToHighlight="50">
</local:PriceToBackgroundConverter>

在元素上应用值转换器:

<Border Background=
 "{Binding Path=UnitCost, Converter={StaticResource PriceToBackgroundConverter}}"
 ... >

估值多个属性

这个过程是不可逆的,也就是只能将多个值合并为一个结果。而不能将结果分解为多个属性。

结合多个属性的第一个方法是使用MultiBinding。下面例子:

<TextBlock>
  <TextBlock.Text>
    <MultiBinding StringFormat="{1}, {0}">
      <Binding Path="FirstName"></Binding>
      <Binding Path="LastName"></Binding>
    </MultiBinding>
  </TextBlock.Text>
</TextBlock>

第二个方法是使用值转换器,实现的是IMultiValueConverter。

MultiBinding使用源对象的UnitCost和UnitsInStock属性,并且使用一个值转换器结合他们:

<TextBlock>Total Stock Value: </TextBlock>
<TextBox>
  <TextBox.Text>
    <MultiBinding Converter="{StaticResource ValueInStockConverter}">
      <Binding Path="UnitCost"></Binding>
      <Binding Path="UnitsInStock"></Binding>
    </MultiBinding>
  </TextBox.Text>
</TextBox>

注意Convert()方法的values是一个对象数组,按照他们在MultiBinding中的顺序放置这些值。在前一个示例中,首先出现UnitCost,然后是UnitsInStock。

public class ValueInStockConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var unitCost = (decimal)values[0];
        var unitsInStock = (int)values[1];

        return unitCost * unitsInStock;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

列表类控件

所有列表控件的基类是ItemsControl。

ItemsControl

ItemsControl类的格式化相关的属性

名称 描述
ItemsSource 绑定数据源(你希望显示在列表的集合或DataView)。
DisplayMemberPath 你希望为每个数据项目显示的属性。对于一个更世故的表示法或为使用属性的一个联合,使用ItemTemplate代替。
ItemStringFormat .NET格式字符串,如果设置,将被用于格式化每个项目的文本。通常,这技术被用来转换数字或日期值到一个合适的显示表示法,确切地为Binding.StringFormat属性。
ItemContainerStyle 一个样式允许你设置包裹每个项目容器的属性。容器取决于列表的类型(例如,对于ListBox类它是ListBoxItem,对于ComboBox类ComboBoxItem)。当列表被填充时,这些包裹器对象被自动地创造。
ItemContainerStyleSelector StyleSelector。使用代码为每个列表项目的包裹器选择一个样式。这允许你为不同的列表项目给出不同的样式。你必须自己创造一个自定义StyleSelector。
AlternationCount 在你的数据中交替集合的数目。例如,2交替2行样式,3交替3行样式,等等。
ItemTemplate 一个模板,从你的绑定对象提取合适的数据,并且排列它到合适的控件联合里。
ItemTemplateSelector DataTemplateSelector,使用代码为每个列表项目选择一个模板。这允许你为不同的项目给出不同的模板。你必须自己创造一个自定义DataTemplateSelector类。
ItemsPanel 定义被创造持有列表项目面板。所有的项目包装被添加到这容器。通常,VirtualizingStackPanel带有一个垂直的(自顶到底)方向被使用。
GroupStyle 如果你使用分组,这是一个样式那定义每个组应该如何被格式化。当使用分组时,项目包装(ListBoxItem,ComboBoxItem,等等)被添加在GroupItem包装那表示每个组,和这些组是然后添加到列表。
GroupStyleSelector StyleSelector。使用代码为每个组选择一个样式。这允许你为不同的组给不同的样式。你必须自己创造一个自定义StyleSelector。

ItemsControl类的下一级是Selector类,它添加了一组属性描述被选择的项目。

Selector类添加的属性包括:SelectedItem,SelectedIndex,SelectedValue,SelectedValuePath。

注意,Selector类不支持多选。ListBox通过SelectionMode和SelectedItems属性支持多选。

列表样式

ItemContainerStyle

如果ItemContainerStyle被设置,当项目被创造时,样式将向下传递到列表控件的每个项目。在列表框控件的情况,每个项目是一个ListBoxItem对象。(在组合框,它是ComboBoxItem,等等。)因而,你用ListBox.ItemContainerStyle属性应用任何样式被用来设置每个ListBoxItem对象的属性。

<ListBox Name="lstProducts" Margin="5" DisplayMemberPath="ModelName">
  <ListBox.ItemContainerStyle>
    <Style>
      <Setter Property="ListBoxItem.Background" Value="LightSteelBlue" />
      <Setter Property="ListBoxItem.Margin" Value="5" />
      <Setter Property="ListBoxItem.Padding" Value="5" />
    </Style>
  </ListBox.ItemContainerStyle>
</ListBox>

带触发器的样式,样式中的触发器是在满足某个前提条件的情况下,设置控件的属性:

<ListBox Name="lstProducts" Margin="5" DisplayMemberPath="ModelName">
  <ListBox.ItemContainerStyle>
    <Style TargetType="{x:Type ListBoxItem}">
      <Setter Property="Background" Value="LightSteelBlue" />
      <Setter Property="Margin" Value="5" />
      <Setter Property="Padding" Value="5" />

      <Style.Triggers>
        <Trigger Property="IsSelected" Value="True">
          <Setter Property="Background" Value="DarkRed" />
          <Setter Property="Foreground" Value="White" />
          <Setter Property="BorderBrush" Value="Black" />
          <Setter Property="BorderThickness" Value="1" />
        </Trigger>
      </Style.Triggers>
    </Style>
  </ListBox.ItemContainerStyle>
</ListBox>

带复选框或单选按钮的列表框

基本技术是修改代表每个列表项目容器的控件模板。不要修改ListBox.Template属性,因为它是列表框的模板。你需要修改ListBoxItem.Template属性。这里是带有单选按钮的模板:

<Window.Resources>
  <Style x:Key="RadioButtonListStyle" TargetType="{x:Type ListBox}">
    <Setter Property="ItemContainerStyle">
      <Setter.Value>
        <Style TargetType="{x:Type ListBoxItem}" >
          <Setter Property="Margin" Value="2" />
          <Setter Property="Template">
            <Setter.Value>
              <ControlTemplate TargetType="{x:Type ListBoxItem}">
                <RadioButton Focusable="False"
                 IsChecked="{Binding Path=IsSelected, Mode=TwoWay,
                             RelativeSource={RelativeSource TemplatedParent} }">
                  <ContentPresenter></ContentPresenter>
                </RadioButton>
              </ControlTemplate>
            </Setter.Value>
          </Setter>
        </Style>
      </Setter.Value>
    </Setter>
  </Style>
</Window.Resources>

直接设置列表框的样式:

<ListBox Style="{StaticResource RadioButtonListStyle}" Name="lstProducts"
 DisplayMemberPath="ModelName">

复选框样式的列表框建立方法与单选列表框基本相同。只是允许列表框多选:

<Style x:Key="CheckBoxListStyle" TargetType="{x:Type ListBox}">
  <Setter Property="SelectionMode" Value="Multiple"></Setter>
  <Setter Property="ItemContainerStyle">
    <Setter.Value>
      <Style TargetType="{x:Type ListBoxItem}" >
        <Setter Property="Margin" Value="2" />
        <Setter Property="Template">
          <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
              <CheckBox Focusable="False"
               IsChecked="{Binding Path=IsSelected, Mode=TwoWay,
                          RelativeSource={RelativeSource TemplatedParent} }">
                <ContentPresenter></ContentPresenter>
              </CheckBox>
            </ControlTemplate>
          </Setter.Value>
        </Setter>
      </Style>
    </Setter.Value>
  </Setter>
</Style>

交替项目样式

AlternationCount是形成一个序列的项目数,在此数目以后样式交替。默认情况下,AlternationCount被设置为0,并且不使用交替格式化。如果你设置AlternationCount为1,列表将在每个项目以后交替,这允许你应用偶奇格式化模式。

给每个ListBoxItem一个AlternationIndex属性,允许你决定它在交替项目的序列如何编号。假设你设置AlternationCount为2,第一ListBoxItem获得一个AlternationIndex的0,第二获得一个AlternationIndex的1,第三获得一个AlternationIndex的0,第四获得一个AlternationIndex的1,等等。

<ListBox Name="lstProducts" Margin="5" DisplayMemberPath="ModelName"
 AlternationCount=”2”>
  <ListBox.ItemContainerStyle>
    <Style TargetType="{x:Type ListBoxItem}">
      <Setter Property="Background" Value="LightSteelBlue" />
      <Setter Property="Margin" Value="5" />
      <Setter Property="Padding" Value="5" />
        <Style.Triggers>
          <Trigger Property="ItemsControl.AlternationIndex" Value="1">
            <Setter Property="Background" Value="LightBlue" />
          </Trigger>
          <Trigger Property="IsSelected" Value="True">
            <Setter Property="Background" Value="DarkRed" />
            <Setter Property="Foreground" Value="White" />
            <Setter Property="BorderBrush" Value="Black" />
            <Setter Property="BorderThickness" Value="1" />
          </Trigger>
        </Style.Triggers>
      </Style>
    </ListBox.ItemContainerStyle>
</ListBox>

样式选择器

必须用代码方式实现,从StyleSelector派生一个专用的类,覆盖SelectStyle()方法,选择合适的样式。

这里是一个基本的选择器,二样式选择一个:

public class ProductByCategoryStyleSelector : StyleSelector
{
    public override Style SelectStyle(object item, DependencyObject container)
    {
        var product = (Product)item;
        var window = Application.Current.MainWindow;
        if (product.CategoryName == "Travel")
        {
            return (Style)window.FindResource("TravelProductStyle");
        }
        else
        {
            return (Style)window.FindResource("DefaultProductStyle");
        }
    }
}

下面代码使用了反射,更为通用的一个样式选择器:

public class SingleCriteriaHighlightStyleSelector : StyleSelector
{
    public Style DefaultStyle
    {
        get; set;
    }
    public Style HighlightStyle
    {
        get; set;
    }
    public string PropertyToEvaluate
    {
        get; set;
    }
    public string PropertyValueToHighlight
    {
        get; set;
    }
    public override Style SelectStyle(object item, DependencyObject container)
    {
        Product product = (Product)item;
        
        // 使用反射获得要检测的属性
        Type type = product.GetType();
        PropertyInfo property = type.GetProperty(PropertyToEvaluate);
        
        // 根据属性值决定是否这个产品应该被高亮
        if (property.GetValue(product, null).ToString() == PropertyValueToHighlight)
        {
            return HighlightStyle;
        }
        else
        {
            return DefaultStyle;
        }
    }
}

定义代码中用到的两个样式:

<Window.Resources>
  <Style x:Key="DefaultStyle" TargetType="{x:Type ListBoxItem}">
    <Setter Property="Background" Value="LightYellow" />
    <Setter Property="Padding" Value="2" />
  </Style>

  <Style x:Key="HighlightStyle" TargetType="{x:Type ListBoxItem}">
    <Setter Property="Background" Value="LightSteelBlue" />
    <Setter Property="FontWeight" Value="Bold" />
    <Setter Property="Padding" Value="2" />
  </Style>
</Window.Resources>

内联使用样式选择器:

<ListBox Name="lstProducts" HorizontalContentAlignment="Stretch">
  <ListBox.ItemContainerStyleSelector>
    <local:SingleCriteriaHighlightStyleSelector
      DefaultStyle="{StaticResource DefaultStyle}"
      HighlightStyle="{StaticResource HighlightStyle}"
      PropertyToEvaluate="CategoryName"
      PropertyValueToHighlight="Travel"
    >
    </local:SingleCriteriaHighlightStyleSelector>
  </ListBox.ItemContainerStyleSelector>
</ListBox>

样式选择过程,当第一次绑定列表时,被执行一次。如果,由于修改了决定样式的数据,样式不会自动改变。只能用暴力的方法强制样式更新:

var selector = lstProducts.ItemContainerStyleSelector;
lstProducts.ItemContainerStyleSelector = null;
lstProducts.ItemContainerStyleSelector = selector;

数据模板

每个ListBoxItem只能绑定一个字段,没有办法结合多个字段。如果要显示多个字段,这就需要用到数据模板。数据模板是一块XAML标记。它定义了应该如何显示一个绑定数据对象。二类型的控件支持数据模板:

  • 内容控件通过ContentTemplate属性支持数据模板。内容模板被用来显示Content属性。
  • 列表控件通过ItemTemplate属性支持数据模板。这模板被用来显示集合每个项目。集合通过ItemsSource提供。

列表项目是一个内容控件。

数据模板应该包括数据绑定表达式。

这是一个例子,用一个导圆角的边界包裹每个项目,显示二片信息,并且使用粗体的格式化高亮模型数:

<ListBox Name="lstProducts" HorizontalContentAlignment="Stretch">
  <ListBox.ItemTemplate>
    <DataTemplate>
      <Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue" 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>
    </DataTemplate>
  </ListBox.ItemTemplate>
</ListBox>

分离和重用模板

提取前一个例子的模板:

<Window.Resources>
  <DataTemplate x:Key="ProductDataTemplate">
    <Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue"
     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>
  </DataTemplate>
</Window.Resources>

现在,你能添加你的数据模板到列表,使用一个静态资源:

<ListBox Name="lstProducts" HorizontalContentAlignment="Stretch"
 ItemTemplate="{StaticResource ProductDataTemplate}"></ListBox>

自动地在不同的类型的控件重用同一个数据模板。设置DataTemplate.DataType属性为绑定数据的类型。例如,前一个例子,移除Key并指定绑定Product对象:

<Window.Resources>
  <DataTemplate DataType="{x:Type local:Product}">
  </DataTemplate>
</Window.Resources>

使用更高级的模板

第一个例子,

<Window.Resources>
  <local:ImagePathConverter x:Key="ImagePathConverter"></local:ImagePathConverter>
  <DataTemplate x:Key="ProductTemplate">
    <Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue"
     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>
  </DataTemplate>
</Window.Resources>

直接一个模板内部放置控件。例如,一列种类。挨着每个种类是一个View按钮。你能使用按钮发射另一个窗口恰好匹配在那种类产品。

处理按钮点击。明显地,所有的按钮将被链接到同样的事件处理器,你在模板内部定义它。但是,你需要决定哪一个列表项目被点击。一个解决方案是存储一些额外的识别信息在按钮的Tag属性,如下所示:

<DataTemplate>
  <Grid Margin="3">
    <Grid.ColumnDefinitions>
      <ColumnDefinition></ColumnDefinition>
      <ColumnDefinition Width="Auto"></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <TextBlock Text="{Binding Path=CategoryName}"></TextBlock>
    <Button Grid.Column="2" HorizontalAlignment="Right" Padding="2"
      Click="cmdView_Clicked" Tag="{Binding Path=CategoryID}">View ...</Button>
  </Grid>
</DataTemplate>

你能取回Tag属性,在事件处理器中:

private void cmdView_Clicked(object sender, RoutedEventArgs e)
{
    var cmd = (Button)sender;
    var categoryID = (int)cmd.Tag;
    ...
}

当你定义绑定时,遗漏Path属性,你能抓取整个数据对象:

<Button HorizontalAlignment="Right" Padding="1"
  Click="cmdView_Clicked" Tag="{Binding}">View ...</Button>

传递整个的对象使更新列表选择更容易。当点击View按钮之前,移动选择到按钮被点击的列表项目。如下所示:

var cmd = (Button)sender;
var product = (Product)cmd.Tag;
lstCategories.SelectedItem = product;

不同的模板

以不同的方式呈现列表项目:

  • 数据触发器
  • 值转换器
  • 模板选择器

数据触发器。基于数据项目的一个属性,设置模板元素的一个属性。例如,基于相应产品对象的CategoryName属性,你能改变包裹每个列表项目的自定义边界的背景。这是一个例子,用红字高亮在工具目录中的产品:

<DataTemplate x:Key="DefaultTemplate">
  <DataTemplate.Triggers>
    <DataTrigger Binding="{Binding Path=CategoryName}" Value="Tools">
     <Setter Property="ListBoxItem.Foreground" Value="Red"></Setter>
    </DataTrigger>
  </DataTemplate.Triggers>
  <Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue"
   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>
</DataTemplate>

值转换器方法:

<Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue" CornerRadius="4"
 Background=
 "{Binding Path=CategoryName, Converter={StaticResource CategoryToColorConverter}">

模板选择器

你需要创造一个派生自DataTemplateSelector类。检查绑定对象并且使用你提供逻辑选择一个合适的模板。

public class SingleCriteriaHighlightTemplateSelector : DataTemplateSelector
{
    public DataTemplate DefaultTemplate
    {
        get; set;
    }
    public DataTemplate HighlightTemplate
    {
        get; set;
    }
    public string PropertyToEvaluate
    {
        get; set;
    }
    public string PropertyValueToHighlight
    {
        get; set;
    }
    public override DataTemplate SelectTemplate(object item,
      DependencyObject container)
    {
        Product product = (Product)item;
        // Use reflection to get the property to check.
        Type type = product.GetType();
        PropertyInfo property = type.GetProperty(PropertyToEvaluate);
        // Decide if this product should be highlighted
        // based on the property value.
        if (property.GetValue(product, null).ToString() == PropertyValueToHighlight)
        {
            return HighlightTemplate;
        }
        else
        {
            return DefaultTemplate;
        }
    }
}

这里是创建两个数据模板的标记:

<Window.Resources>
  <DataTemplate x:Key="DefaultTemplate">
    <Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue"
      CornerRadius="4">
      <Grid Margin="3">
        <Grid.RowDefinitions>
          <RowDefinition></RowDefinition>
          <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <TextBlock
         Text="{Binding Path=ModelNumber}"></TextBlock>
        <TextBlock Grid.Row="1"
         Text="{Binding Path=ModelName}"></TextBlock>
      </Grid>
    </Border>
  </DataTemplate>
  
  <DataTemplate x:Key="HighlightTemplate">
    <Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue"
     Background="LightYellow" 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" FontWeight="Bold"
         Text="{Binding Path=ModelName}"></TextBlock>
        <TextBlock Grid.Row="2" FontStyle="Italic" HorizontalAlignment="Right">
         *** Great for vacations ***</TextBlock>
      </Grid>
    </Border>
  </DataTemplate>
</Window.Resources>    

这里是应用模板选择器的标记:

<ListBox Name="lstProducts" HorizontalContentAlignment="Stretch">
  <ListBox.ItemTemplateSelector>
    <local:SingleCriteriaHighlightTemplateSelector
      DefaultTemplate="{StaticResource DefaultTemplate}"
      HighlightTemplate="{StaticResource HighlightTemplate}"
      PropertyToEvaluate="CategoryName"
      PropertyValueToHighlight="Travel"
    >
    </local:SingleCriteriaHighlightTemplateSelector>
  </ListBox.ItemTemplateSelector>
</ListBox>

模板与选择

例1,修改选中项目的背景色:

Foreground属性使用属性继承,所以你添加到模板任何元素自动地获得白颜色。Background属性不使用属性继承,但是默认背景颜色是透明。

首先,设置项目容器的样式:

<ListBox Name="lstProducts" HorizontalContentAlignment="Stretch">
  <ListBox.ItemContainerStyle>
    <Style>
      <Setter Property="Control.Padding" Value="0"></Setter>
      <Style.Triggers>
        <Trigger Property="ListBoxItem.IsSelected" Value="True">
          <Setter Property="ListBoxItem.Background" Value="DarkRed" />
        </Trigger>
      </Style.Triggers>
    </Style>
  </ListBox.ItemContainerStyle>
</ListBox>

然后,修改数据模板:

<DataTemplate>
  <Grid Margin="0" Background="White">
    <Border Margin="5" BorderThickness="1"
     BorderBrush="SteelBlue" CornerRadius="4"
     Background="{Binding Path=Background, RelativeSource={
                             RelativeSource
                             Mode=FindAncestor,
                             AncestorType={x:Type ListBoxItem}
                          }}" >
      <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>

例2,选中项目后,显示此项目的细节:

在数据模板中,使用Binding的RelativeSource属性搜索目前的ListBoxItem。如果它没有被选择,设置额外信息的Visibility属性,你能隐藏它。

使用一个数据触发器,当ListBoxItem的IsSelected属性被改变,修改容器的Visibility属性。

数据触发器只需要放在要隐藏的容器内。

这是还没有自动扩展功能的简化版:

<DataTemplate>
  <Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue"
   CornerRadius="4">
    <StackPanel Margin="3">
      <TextBlock Text="{Binding Path=ModelName}"></TextBlock>
      <StackPanel>
        <TextBlock Margin="3" Text="{Binding Path=Description}"
         TextWrapping="Wrap" MaxWidth="250" HorizontalAlignment="Left"></TextBlock>
        <Image Source="{Binding Path=ProductImagePath, Converter={StaticResource ImagePathConverter}}">
        </Image>
        <Button FontWeight="Regular" HorizontalAlignment="Right" Padding="1"
         Tag="{Binding}">View Details...</Button>
      </StackPanel>
    </StackPanel>
  </Border>
</DataTemplate>

最内层的StackPanel包含着只对选中项目显示的内容,所以设置这个容器的样式:

<StackPanel>
  <StackPanel.Style>
    <Style>
      <Style.Triggers>
        <DataTrigger
          Binding="{Binding Path=IsSelected, RelativeSource={
                             RelativeSource
                             Mode=FindAncestor,
                             AncestorType={x:Type ListBoxItem}
                          }}"
          Value="False">
          <Setter Property="StackPanel.Visibility" Value="Collapsed" />
        </DataTrigger>
      </Style.Triggers>
    </Style>
  </StackPanel.Style>
</StackPanel>

在这个例子中,你需要使用DataTrigger而不是一个普通的触发器,因为你需要的属性在祖先元素中(ListBoxItem),并且唯一访问它的办法是使用数据绑定表达式。

改变项目布局

使用任何从System.Windows.Controls.Panel派生的类,设置ListBox的ItemsPanelTemplate属性,可以改变项目布局。

下面例子,使用WrapPanel改变项目布局:

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

你必须也设置ScrollViewer.HorizontalScrollBarVisibility附加属性为Disabled。这确保ScrollViewer从不使用水平的scrollbar。

VirtualizingStackPanel对于大量数据项目有更好的性能。

组合框

默认,ComboBox是只读的。当设置IsReadOnly属性为假并且IsEditable属性为真,选择框变成文本框,你能键入任何文本。

组合框有自动完成的功能。要关闭它,设置ComboBox.IsTextSearchEnabled属性为假。此属性位于ItemsControl类。

如果IsEditable属性是假(这是默认值),选择框将显示一个项目的精确视觉副本。

重要的细节是组合框显示的内容是什么,而不是它的数据源是什么。例如,用Product对象填充一个组合框控件,并且设置DisplayMemberPath属性为ModelName。如此,组合框显示每个项目的ModelName属性。即组合框从一组Product对象获取信息,你的标记创造的是一个普通的文本列表。它将显示当前产品的ModelName,并且如果IsEditable是真并且IsReadOnly是假,它将允许你编辑那值。

如果IsEditable属性是真,选择框显示一个它的一个逐字的表示法。就是简单地调用ToString()到项目。一般情况下,显示一个类名的全称。

纠正这个问题最简单的方法是,设置TextSearch.TextPath附着属性指明组合框应该使用的内容。

<ComboBox IsEditable="True" IsReadOnly="True" TextSearch.TextPath="ModelName" ...>