• 《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中最上面的窗体显示了没有过滤器,中间的窗体显示了过滤了初始的列表,底部的窗体显示了添加一个成年人,过滤器仍然在恰当的位置。

  • 相关阅读:
    《大型网站技术架构:核心原理与案分析》阅读笔记05
    软件体系结构(1)
    《大型网站技术架构:核心原理与案分析》阅读笔记04
    C/C++
    NIO蔚来自动驾驶实习生技术一面
    Intern Day86
    面试常考
    中国赛宝实验室C++技术一面
    Intern Day85
    Intern Day85
  • 原文地址:https://www.cnblogs.com/Jax/p/1137290.html
Copyright © 2020-2023  润新知