I是项容器

来源:互联网 发布:js图片上传原理 编辑:程序博客网 时间:2024/04/27 16:07

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!)。