《Programming WPF》翻译 第4章 3.绑定到数据列表

目前为止,你已经看到一些示例将控件绑定到一个单独的对象。然而,更复杂的使用是绑定到一个对象列表。例如,想象一下,我们的对象数据源可以创建一个新类型表示Person对象的列表,正如示例4-19:

示例4-19

using System.Collections.Generic; // List<T>

namespace PersonBinding {
  
// XAML doesn't (yet) have a syntax
  
// for generic class instantiation
  class People : List<Person> {}
}

我们可以挂起这个新的数据源列表,按照同样的方式绑定到它,就像绑定到一个单独的对象数据源上,如示例4-20

示例4-20

<!-- Window1.xaml -->
<?Mapping XmlNamespace="local" ClrNamespace="PersonBinding" ?>
<Window  xmlns:local="local">
  
<Window.Resources>
    
<local:People x:Key="Family">
      
<local:Person Name="Tom" Age="9" />
      
<local:Person Name="John" Age="11" />
      
<local:Person Name="Melissa" Age="36" />
    
</local:People>
    
<local:AgeToForegroundConverter
      
x:Key="AgeToForegroundConverter" />
  
</Window.Resources>
  
<Grid DataContext="{StaticResource Family}">
    
    
<TextBlock >Name:</TextBlock>
    
<TextBox Text="{Binding Path=Name}"  />
    
<TextBox
      
Text="{Binding Path=Age}"
      Foreground
="{Binding Path=Age, Converter=}"  />
    
<Button >Birthday</Button>
  
</Grid>
</Window>

在示例4-20中,我们创建了一个People集合的示例而且通过三个Person对象导入它。然而,运行它将会如图4-6

4.3.1当前项

尽管文本框属性每次仅能被绑定到一个单独的对象上,在可能的被绑定到的对象列表中,绑定引擎提供了一个名为当前项的概念,正如图4-6所解释的。

缺省地,列表的第一项作为当前项的开始。由于我们列表示例的第一项与我们之前绑定的单独对象一样,所以看起来和图4-11显示的一样——Birthday按钮除外。

4-11



4.3.1.1获取当前项

回想当前Birthday按钮的click事件句柄(示例4-21)。

示例4-21

public partial class Window1 : Window {
  
  
void birthdayButton_Click(object sender, RoutedEventArgs e) {
    Person person 
= (Person)this.FindResource("Tom"));
    
++person.Age;
    MessageBox.Show();
  }

}

我们的Birthday按钮应该总是产生向当前人士祝贺生日的效果,但是到目前为止,当前人士却总是一样的,因此我们只能简化事情为直接到达单独的Person对象。既然我们已经得到了对象的列表,这个机制就不再使用了(除非你认为一个包含单词“InvalidCastException”消息框是可以接受的方式)。进一步而言,转换到People,我们的集合类,不会告诉我们那一个Person对象会在当前UI中显示,因为它不知道这些事情(也不需要知道)。由于这一点,我们将要必须建立“经纪人”在数据绑定的控件和集合项上,这个“经纪人”在这里被称为视图。

视图的工作是在数据之上提供服务,包括排序,过滤,以及此刻对于我们的意图来说最重要的:控制当前项。视图是详细数据的接口实现,在我们这种情形,就是ICollectionView接口。我们可以通过BindingOperations类的静态GetDefaultView方法访问这个数据上的视图,正如示例4-22所示:

示例4-22

public partial class Window1 : Window {
  
  
void birthdayButton_Click(object sender, RoutedEventArgs e) {
    People people 
= (People)this.FindResource("Family");
    ICollectionView view 
=
      BindingOperations.GetDefaultView(people);
    Person person 
= (Person)view.CurrentItem;

    
++person.Age;
    MessageBox.Show();
  }

}

为了取回联合了Family集合的视图,示例4-22BindingOperationsGetDefaultView方法进行了一次调用,提供了一个ICollectionView接口的实现。基于此,我们可以得到当前项,将它从集合中的一项转换为我们需要的对象(CurrentItem属性返回一个object对象),以及用它来显示。

4.3.1.2在数据项中导航

出了获取当前项外,我们也能改变当前项的位置,通过ICollectionView接口的MoveCurrentToXX方法,正如示例4-23所示。

示例4-23

 

public partial class Window1 : Window {
  
  ICollectionView GetFamilyView(  ) 
{
    People people 
= (People)this.FindResource("Family");
    
return BindingOperations.GetDefaultView(people);
  }


  
void birthdayButton_Click(object sender, RoutedEventArgs e) {
    ICollectionView view 
= GetFamilyView(  );
    Person person 
= (Person)view.CurrentItem;

    
++person.Age;
    MessageBox.Show();
  }


  
void backButton_Click(object sender, RoutedEventArgs e) {
    ICollectionView view 
= GetFamilyView(  );
    view.MoveCurrentToPrevious(  );
    
if( view.IsCurrentBeforeFirst ) {
      view.MoveCurrentToFirst(  );
    }

  }


  
void forwardButton_Click(object sender, RoutedEventArgs e) {
    ICollectionView view 
= GetFamilyView(  );
    view.MoveCurrentToNext(  );
    
if( view.IsCurrentAfterLast ) {
      view.MoveCurrentToLast(  );
    }

  }

}

ICollectionView接口的MoveCurrentToPrevious方法和MoveCurrentToNext方法,通过在集合中向后和向前的动作改变当前的选中项。如果我们沿着一个方向移动到列表的尽头或另一个尽头,IsCurrentBeforeFirstIsCurrentAfterLast属性将会告诉我们这一点。MoveCurrentToFirstMoveCurrentToLast方法帮助我们复原在到达列表的尽头之后,对于在途4-12中实现BackForward按钮,这将是很有用的。同样适用于FirstLast两个按钮(这将是你的一个机会,将学到的运用上去)。

4-12显示了从集合中第一个Person元素开始,向前移动的效果,包括基于Person对象的Age属性导致的颜色改变(这仍然以同样的方式工作)。

4-12



4.3.2数据列表目标

当然,目前为止,我们仅能做的是把用户列表数据推出来,而没有为这些数据提供一个控件可以准确的一次性显示多条数据,正如示例4-24中的ListBox控件。

示例4-24

 

<!-- Window1.xaml -->
<?Mapping XmlNamespace="local" ClrNamespace="PersonBinding" ?>
<Window  xmlns:local="local">
  
<Window.Resources>
    
<local:People x:Key="Family"></local:People>
    
<local:AgeToForegroundConverter
      
x:Key="AgeToForegroundConverter" />
  
</Window.Resources>
  
<Grid DataContext="{StaticResource Family}">
    
    
<ListBox
      
ItemsSource="{Binding}"
      IsSynchronizedWithCurrentItem
="True"  />
    
<TextBlock >Name:</TextBlock>
    
<TextBox Text="{Binding Path=Name}"  />
    
</Window>

在示例4-24中,ListBoxItemSource属性没有绑定到路径,等于是说:绑定到当前整个对象。注意到,这里也没有源,因此绑定会从找到的第一个非空的数据上下文开始工作。在这种情形中,第一个非空的数据上下文来自Grid,就是那个在nameage的文本框中共享的上下文。我们还设置了IsSynchronizedWithCurrentItem属性为true,以确保listbox中的选中项也能发生改变——这会在视图中更新当前项;反之亦然。

4-13



正如你可能看到的,图
4-13中的每件事物都很完美。所发生的是,当你绑定一个完整对象时,数据绑定尽其所能显示每一个Person对象。无需特殊的指令,它会使用一个类型转换器来得到一个字符串表示。对于nameage,都是内嵌类型,具有内嵌转换,这将工作良好;但是也有不能很好工作的时候,对于一个不具备可视化生成的自定义类型,正如Person类型这种情形。

4.3.3数据模板

正确解决这个问题的做法是使用数据模板。数据模板是一棵元素树,可以在特定的上下文扩展。例如,对于每一个Person对象,我们希望能够像以下方式将nameage连接在一起:

    Tom(age: 9)

我们可以把它想象成一个合乎逻辑的模板,如下:

    Name(age: Age)

为了在listbox中为数据项定义模板,我们创建了一个DataElement元素,正如示例4-25

示例4-25

<ListBox  ItemsSource="{Binding}">
  
<ListBox.ItemTemplate>
    
<DataTemplate>
      
<StackPanel Orientation="Horizontal">
        
<TextBlock TextContent="{Binding Path=Name}" />
        
<TextBlock TextContent=" (age: " />
        
<TextBlock
          
TextContent="{Binding Path=Age}"
          Foreground
="
            {Binding
              Path=Age,
              Converter={StaticResource AgeToForegroundConverter}}"
 />
        
<TextBlock TextContent=")" />
      
</StackPanel>
    
</DataTemplate>
  
</ListBox.ItemTemplate>
</ListBox>

在这种情形中, ListBox控件有一个ItemTemplate属性,它接受一个DataTemplate对象示例。DataTemplate允许我们详细指出一个单独的子元素,用于绑定重复显示在ListBox控件的每一个数据项。在我们的例子中,使用了StackPanel将四个TextBlock控件放在一行中:2个绑定到每个Person对象的属性,两个是常量文本。注意到,我们使用AgeToForegroundConverter已经将Foreground绑定到Age属性,为了Age属性显示为黑色或红色,为了列表框和age文本框是一致的。

通过使用数据模板,我们经历了从图4-13到图4-14

4-14



注意到,列表框显示了集合中所有的条目,而且保持了视图同步于当前条目,当选择向前或向后的按钮按下时(实际上,你并不会从图
4-14的部分截图真正“注意到”,但是相信我,确实是发生了)。此外,当Person对象的数据改变的时候,列表框以及文本框会保持同步,还包括Age的颜色。

4.3.1类型化数据模板

在示例4-25中,我们显示地为ListBox列表设置了数据模板。然而,如果一个Person对象显示在一个按钮或是其它什么元素中,我们最好分别详细指出那些Person对象的数据模板。另一方面,如果你想要Person对象有一个特殊的模板而不论其显示在哪里,你可以通过类型化的数据模板来实现。

示例4-26

<Window.Resources>
  
<local:AgeToForegroundConverter
    
x:Key="AgeToForegroundConverter" />
  
<local:People x:Key="Family"></local:People>
  
<DataTemplate DataType="{x:Type local:Person}">
    
<StackPanel Orientation="Horizontal">
      
<TextBlock TextContent="{Binding Path=Name}" />
      
<TextBlock TextContent=" (age: " />
      
<TextBlock TextContent="{Binding Path=Age}"  />
      
<TextBlock TextContent=")" />
    
</StackPanel>
  
</DataTemplate>
</Window.Resources>

<!-- no need for an ItemTemplate setting -->
<ListBox ItemsSource="{Binding}" >

在示例4-26中,我们将数据模板的定义提升到资源模块,并且使用标签的DataType属性标志这个数据模板是类型化的。现在,除非另外通知,每当WPF看到Person对象的一个实例,就会应用相应的数据模板。这是一条便利之路,保证数据以一致的方式显示,遍及于你的应用程序,而不用担心显示的位置。

4.3.4列表的改变

迄今,我们已经得到一个对象的列表,我们可以适当的进行编辑,以及在其中建立导航,甚至轻而易举地高亮显示某些数据,以及提供了一个自动搜索,表现那些没有装载的来自厂商的数据。考虑到我们已经到达的程度,你可能怀疑提供一个Add按钮是一件轻而易举的事情,正如示例4-27所示。

示例4-27

 

public partial class Window1 : Window {
  
  
void addButton_Click(object sender, RoutedEventArgs e) {
    People people 
= (People)this.FindResource("Family");
    people.Add(
new Person("Chris"35));
  }

}

这个实现的问题在于,尽管视图可以判断出新条目的存在当你移动到这里的时候,而列表框本身却并不知道新增加的集合中的条目,正如图4-15

4-15



为了与图
4-15显示的应用程序状态交互,我运行了这个程序,点击了Add按钮并使用Forward按钮导航到图中所示。然而,即使新人显示在文本框中,列表框仍然不知道添加了什么事物。同样地,如果有对象被删除,它也不会知道。就像数据绑定需要事先INotifyPropertyChanged接口,使用数据绑定的列表需要实现INotifyPropertyChanged这个接口,正如示例4-28

示例4-28

 

namespace System.Collections.Specialized {
  
public interface INotifyCollectionChanged {
    
event NotifyCollectionChangedEventHandler CollectionChanged;
  }

}

INotifyCollectionChanged接口用于通知数据绑定控件,有条目在绑定列表中添加或删除。尽管在你的自定义类型中实现INotifyPropertyChanged,从而支持两种方式的数据绑定在你的类型化属性上——这很普通;不普通的是实现你自己的集合类,这些类给你很少的机会实现INotifyCollectionChanged接口。取代之,你更加更能依赖于集合类的一项在.NET 框架类库中,用来实现INotifyCollectionChanged。这样的类数量很少,而且不幸的是,我们使用的保持着Person对象的集合类,并不在其中。当你受欢迎的度过你的夜晚和周末实现了INotifyPropertyChangedWPF提供了ObservableCollection<T>类,用于我们那些紧迫的职责,如示例4-29所示。

示例4-29

namespace System.Windows.Data {
  
public class ObservableCollection<T> :
    Collection
<T>, INotifyCollectionChanged, INotifyPropertyChanged {
    
  }

}

既然ObservableCollection<T>派生于Collection<T>,而且实现了INotifyCollectionChanged接口,我们可以使用它代替List<T>作为我们的Person集合,正如示例4-30

示例4-30

namespace PersonBinding {
  
class Person : INotifyPropertyChanged {}
  
class People : ObservableCollection<Person> {}
}

现在,当一个条目添加到或删除自Person集合,这些变化将要在数据绑定列表中反映出来,正如图4-6所示。

4-16



4.3.5排序

一旦我们适当地使数据目标每次显示多于一个事物,一个年轻人的爱好变得更多,当然,是喜欢的事物,正如对数据视图排序或者过滤。回忆视图经常位于数据绑定目标和数据源之间。这意味着可以越过我们不要显示的数据(被称为过滤,而且可以被直接覆盖),而且可以改变数据显示的顺序,又名排序。最简单的排序方法是通过操作视图的Sort属性,正如示例4-31所示。

示例4-31

public partial class Window1 : Window {
  
  ICollectionView GetFamilyView(  ) 
{
    People people 
= (People)this.FindResource("Family");
    
return BindingOperations.GetDefaultView(people);
  }


  
void sortButton_Click(object sender, RoutedEventArgs e) {
    ICollectionView view 
= GetFamilyView(  );
    
if( view.Sort.Count == 0 ) {
      view.Sort.Add(
        
new SortDescription("Name", ListSortDirection.Ascending));
      view.Sort.Add(
        
new SortDescription("Age", ListSortDirection.Descending));
    }

    
else {
      view.Sort.Clear(  );
    }

  }

}

这里我们通过检测SortDescriptionCollection暴露在外的ICollectionView.Sort属性,将排序视图和未排序视图拴在一起。如果没有排序方式的描述,我们首先对Name属性按上升方式排序,然后对Age属性按下降方式排序。如果有排序方式的描述,我们将其清除,重新排序——无论之前是如何排序的。虽然排序描述在适当的位置,任意新添加到集合中的对象将被添加到它们已经排好序的适当位置,正如4-17所示。

一个SortDescription对象集合应该覆盖大多数的情形,但是如果你需要更多一点的控件,你可以提供自定义排序对象的视图,通过实现IComparer接口,正如示例4-32

4-17



示例
4-32

class PersonSorter : IComparer {
  
public int Compare(object x, object y) {
    Person lhs 
= (Person)x;
    Person rhs 
= (Person)y;

    
// Sort Name ascending and Age descending
    int nameCompare = lhs.Name.CompareTo(rhs.Name);
    
if( nameCompare != 0 ) return nameCompare;
    
return rhs.Age - lhs.Age;
 }

}


public partial class Window1 : Window {
  
  ICollectionView GetFamilyView(  ) 
{
    People people 
= (People)this.FindResource("Family");
    
return BindingOperations.GetDefaultView(people);
  }


  
void sortButton_Click(object sender, RoutedEventArgs e) {
    ListCollectionView view 
= (ListCollectionView)GetFamilyView(  );
    
if( view.CustomSort == null ) {
      view.CustomSort 
= new PersonSorter(  );
    }

    
else {
      view.CustomSort 
= null;
    }

  }

}

在设置了自定义排序的情况,我们必须做一个假设——详细明确地实现了ICollectionView,这里使用的是ListCollectionView,是WPF包装在IList的实现(由ObserverableCollection提供),来提供视图的功能性。此外还有其它没有提供自定义排序的ICollectionView接口实现,因此你要在想*使用这段代码前先测试一下。

希望你在使用前也测试一下其它代码,但是指出这些事情并没有什么危害。

尽管我肯定,当我们使用WPF1.0时,这将变得更好。从现在开始,视图实现了联合详细数据特征,正如在ListCollectionViewIList间进行匹配并没有文本化(至少现在我这么说)。这看起来有点有趣,CustomSort是视图实现类的一部分,并不是ICollectionView接口的一部分,因此让我们为之祈祷:Microsoft发布新的WPF版本改变这一点。

4.3.6过滤

正因为所有的对象按顺序显示使你快乐,这并不意味着你想要显示所有的对象。对于这些没用的出现在数据中的对象,却不属于这个视图,我们需要提供这个实现了CollectionFilterCallback委托*的视图,需要一个单独的对象作为参数并返回一个Boolean值表明这个对象是否应该被显示,正如示例4-33

排序使用一个单方法的接口实现,是由于历史原因;而过滤使用一个委托,是因为在C#2.0中另外使用匿名委托机制,这是一个很流行的机制。

示例4-33

public partial class Window1 : Window {
  
  ICollectionView GetFamilyView(  ) 
{
    People people 
= (People)this.FindResource("Family");
    
return BindingOperations.GetDefaultView(people);
  }


  
void filterButton_Click(object sender, RoutedEventArgs e) {
    ICollectionView view 
= GetFamilyView(  );
    
if( view.Filter == null ) {
       view.Filter 
= delegate(object item) {
        
return ((Person)item).Age >= 18;
      }
;
    }

    
else {
      view.Filter 
= null;
    }

  }

}

正如排序,通过使用一个恰当的过滤器,新条目被适当的过滤掉了,正如图4-18所示。

4-18


4-18中最上面的窗体显示了没有过滤器,中间的窗体显示了过滤了初始的列表,底部的窗体显示了添加一个成年人,过滤器仍然在恰当的位置。

posted @ 2008-04-04 12:36  包建强  Views(1526)  Comments(1Edit  收藏  举报